From 61364e20c21de0fcc63568d42c655d2f7ef692c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:07:37 +0000 Subject: build(deps): bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to '.github/workflows') diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 30b13f7f..283c2c4f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -45,7 +45,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -72,6 +72,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" -- cgit v1.2.3-70-g09d2 From be8de118db913711eb72ae5187d26e54a0055727 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 15 Dec 2023 09:11:47 +0800 Subject: refactor(docs): optmst `docs` dir & `deps` --- .github/workflows/pages.yml | 3 +- .pdm-python | 2 +- docs/requirements.txt | 17 + docs/src/.gitignore | 2 + docs/src/__init__.py | 21 + docs/src/extensions/__init__.py | 19 + docs/src/extensions/emoji.py | 98 ++ .../assets/javascripts/components/_/index.ts | 104 ++ .../javascripts/components/iconsearch/_/index.ts | 94 ++ .../javascripts/components/iconsearch/index.ts | 25 + .../components/iconsearch/query/index.ts | 96 ++ .../components/iconsearch/result/index.ts | 237 ++++ .../assets/javascripts/components/index.ts | 25 + .../javascripts/components/sponsorship/index.ts | 149 +++ docs/src/overrides/assets/javascripts/custom.ts | 55 + .../javascripts/integrations/analytics/index.ts | 42 + .../assets/javascripts/integrations/index.ts | 23 + .../javascripts/templates/iconsearch/index.tsx | 95 ++ .../assets/javascripts/templates/index.ts | 24 + .../javascripts/templates/sponsorship/index.tsx | 67 + docs/src/overrides/assets/stylesheets/custom.scss | 44 + .../assets/stylesheets/custom/_typeset.scss | 294 +++++ .../assets/stylesheets/custom/layout/_banner.scss | 66 + .../assets/stylesheets/custom/layout/_hero.scss | 123 ++ .../stylesheets/custom/layout/_iconsearch.scss | 136 +++ .../stylesheets/custom/layout/_sponsorship.scss | 128 ++ docs/src/overrides/home.html | 106 ++ docs/src/overrides/hooks/shortcodes.py | 283 +++++ docs/src/overrides/hooks/translations.html | 54 + docs/src/overrides/hooks/translations.py | 193 +++ docs/src/overrides/main.html | 59 + docs/src/plugins/__init__.py | 19 + docs/src/plugins/blog/__init__.py | 19 + docs/src/plugins/blog/author.py | 38 + docs/src/plugins/blog/config.py | 88 ++ docs/src/plugins/blog/plugin.py | 884 ++++++++++++++ docs/src/plugins/blog/readtime/__init__.py | 51 + docs/src/plugins/blog/readtime/parser.py | 45 + docs/src/plugins/blog/structure/__init__.py | 292 +++++ docs/src/plugins/blog/structure/config.py | 37 + docs/src/plugins/blog/structure/markdown.py | 58 + docs/src/plugins/blog/structure/options.py | 87 ++ docs/src/plugins/blog/templates/__init__.py | 42 + docs/src/plugins/group/__init__.py | 19 + docs/src/plugins/group/config.py | 33 + docs/src/plugins/group/plugin.py | 151 +++ docs/src/plugins/info/__init__.py | 19 + docs/src/plugins/info/config.py | 35 + docs/src/plugins/info/plugin.py | 245 ++++ docs/src/plugins/offline/__init__.py | 19 + docs/src/plugins/offline/config.py | 30 + docs/src/plugins/offline/plugin.py | 69 ++ docs/src/plugins/search/__init__.py | 19 + docs/src/plugins/search/config.py | 58 + docs/src/plugins/search/plugin.py | 580 +++++++++ docs/src/plugins/social/__init__.py | 19 + docs/src/plugins/social/config.py | 48 + docs/src/plugins/social/plugin.py | 516 ++++++++ docs/src/plugins/tags/__init__.py | 27 + docs/src/plugins/tags/config.py | 38 + docs/src/plugins/tags/plugin.py | 182 +++ docs/src/templates/.icons/logo.afdesign | Bin 0 -> 31465 bytes docs/src/templates/.icons/logo.svg | 6 + docs/src/templates/404.html | 28 + docs/src/templates/__init__.py | 19 + docs/src/templates/assets/images/favicon.png | Bin 0 -> 1870 bytes docs/src/templates/assets/javascripts/_/index.ts | 148 +++ .../assets/javascripts/browser/document/index.ts | 48 + .../assets/javascripts/browser/element/_/.eslintrc | 6 + .../assets/javascripts/browser/element/_/index.ts | 120 ++ .../javascripts/browser/element/focus/index.ts | 81 ++ .../assets/javascripts/browser/element/index.ts | 27 + .../javascripts/browser/element/offset/_/index.ts | 86 ++ .../browser/element/offset/content/index.ts | 76 ++ .../javascripts/browser/element/offset/index.ts | 24 + .../javascripts/browser/element/size/_/index.ts | 151 +++ .../browser/element/size/content/index.ts | 67 + .../javascripts/browser/element/size/index.ts | 24 + .../browser/element/visibility/index.ts | 131 ++ .../templates/assets/javascripts/browser/index.ts | 32 + .../assets/javascripts/browser/keyboard/index.ts | 148 +++ .../assets/javascripts/browser/location/_/index.ts | 85 ++ .../javascripts/browser/location/hash/index.ts | 104 ++ .../assets/javascripts/browser/location/index.ts | 24 + .../assets/javascripts/browser/media/index.ts | 95 ++ .../assets/javascripts/browser/request/index.ts | 141 +++ .../assets/javascripts/browser/script/index.ts | 70 ++ .../assets/javascripts/browser/toggle/index.ts | 102 ++ .../assets/javascripts/browser/viewport/_/index.ts | 69 ++ .../javascripts/browser/viewport/at/index.ts | 84 ++ .../assets/javascripts/browser/viewport/index.ts | 26 + .../javascripts/browser/viewport/offset/index.ts | 78 ++ .../javascripts/browser/viewport/size/index.ts | 71 ++ .../assets/javascripts/browser/worker/index.ts | 112 ++ docs/src/templates/assets/javascripts/bundle.ts | 316 +++++ .../assets/javascripts/components/_/index.ts | 138 +++ .../javascripts/components/announce/index.ts | 110 ++ .../assets/javascripts/components/consent/index.ts | 116 ++ .../javascripts/components/content/_/index.ts | 125 ++ .../components/content/annotation/_/index.ts | 272 +++++ .../components/content/annotation/block/index.ts | 88 ++ .../components/content/annotation/index.ts | 25 + .../components/content/annotation/list/index.ts | 209 ++++ .../javascripts/components/content/code/_/index.ts | 238 ++++ .../javascripts/components/content/code/index.ts | 23 + .../components/content/details/index.ts | 138 +++ .../assets/javascripts/components/content/index.ts | 28 + .../components/content/mermaid/index.css | 430 +++++++ .../components/content/mermaid/index.ts | 133 ++ .../javascripts/components/content/table/index.ts | 70 ++ .../javascripts/components/content/tabs/index.ts | 265 ++++ .../assets/javascripts/components/dialog/index.ts | 128 ++ .../javascripts/components/header/_/index.ts | 200 +++ .../assets/javascripts/components/header/index.ts | 24 + .../javascripts/components/header/title/index.ts | 144 +++ .../assets/javascripts/components/index.ts | 37 + .../assets/javascripts/components/main/index.ts | 125 ++ .../assets/javascripts/components/palette/index.ts | 180 +++ .../javascripts/components/progress/index.ts | 87 ++ .../javascripts/components/search/_/index.ts | 239 ++++ .../components/search/highlight/.eslintrc | 5 + .../components/search/highlight/index.ts | 115 ++ .../assets/javascripts/components/search/index.ts | 28 + .../javascripts/components/search/query/index.ts | 206 ++++ .../javascripts/components/search/result/index.ts | 197 +++ .../javascripts/components/search/share/index.ts | 135 +++ .../javascripts/components/search/suggest/index.ts | 154 +++ .../assets/javascripts/components/sidebar/index.ts | 227 ++++ .../javascripts/components/source/_/index.ts | 142 +++ .../javascripts/components/source/facts/_/index.ts | 88 ++ .../components/source/facts/github/index.ts | 103 ++ .../components/source/facts/gitlab/index.ts | 61 + .../javascripts/components/source/facts/index.ts | 25 + .../assets/javascripts/components/source/index.ts | 24 + .../assets/javascripts/components/tabs/index.ts | 144 +++ .../assets/javascripts/components/toc/index.ts | 379 ++++++ .../assets/javascripts/components/top/index.ts | 184 +++ .../javascripts/integrations/clipboard/index.ts | 99 ++ .../assets/javascripts/integrations/index.ts | 27 + .../javascripts/integrations/instant/.eslintrc | 6 + .../javascripts/integrations/instant/index.ts | 446 +++++++ .../javascripts/integrations/search/_/index.ts | 332 +++++ .../integrations/search/config/index.ts | 115 ++ .../integrations/search/highlighter/index.ts | 93 ++ .../javascripts/integrations/search/index.ts | 27 + .../integrations/search/internal/.eslintrc | 6 + .../integrations/search/internal/_/index.ts | 74 ++ .../integrations/search/internal/extract/index.ts | 107 ++ .../search/internal/highlight/index.ts | 162 +++ .../integrations/search/internal/index.ts | 26 + .../integrations/search/internal/tokenize/index.ts | 136 +++ .../integrations/search/query/.eslintrc | 6 + .../integrations/search/query/_/index.ts | 172 +++ .../javascripts/integrations/search/query/index.ts | 25 + .../integrations/search/query/segment/index.ts | 81 ++ .../integrations/search/query/transform/index.ts | 99 ++ .../integrations/search/worker/_/index.ts | 95 ++ .../integrations/search/worker/index.ts | 24 + .../integrations/search/worker/main/.eslintrc | 6 + .../integrations/search/worker/main/index.ts | 192 +++ .../integrations/search/worker/message/index.ts | 112 ++ .../javascripts/integrations/sitemap/index.ts | 107 ++ .../javascripts/integrations/version/.eslintrc | 5 + .../javascripts/integrations/version/index.ts | 186 +++ .../javascripts/patches/indeterminate/index.ts | 85 ++ .../templates/assets/javascripts/patches/index.ts | 25 + .../assets/javascripts/patches/scrollfix/index.ts | 100 ++ .../assets/javascripts/patches/scrolllock/index.ts | 89 ++ .../assets/javascripts/polyfills/index.ts | 96 ++ .../javascripts/templates/annotation/index.tsx | 65 + .../javascripts/templates/clipboard/index.tsx | 45 + .../assets/javascripts/templates/index.ts | 29 + .../assets/javascripts/templates/search/index.tsx | 170 +++ .../assets/javascripts/templates/source/index.tsx | 47 + .../assets/javascripts/templates/tabbed/index.tsx | 56 + .../assets/javascripts/templates/table/index.tsx | 44 + .../assets/javascripts/templates/tooltip/index.tsx | 42 + .../assets/javascripts/templates/version/index.tsx | 92 ++ .../assets/javascripts/utilities/h/.eslintrc | 7 + .../assets/javascripts/utilities/h/index.ts | 132 ++ .../assets/javascripts/utilities/index.ts | 24 + .../assets/javascripts/utilities/round/index.ts | 50 + .../templates/assets/javascripts/workers/search.ts | 23 + docs/src/templates/assets/stylesheets/_config.scss | 42 + docs/src/templates/assets/stylesheets/main.scss | 86 ++ .../templates/assets/stylesheets/main/_colors.scss | 153 +++ .../templates/assets/stylesheets/main/_icons.scss | 37 + .../assets/stylesheets/main/_modifiers.scss | 48 + .../templates/assets/stylesheets/main/_resets.scss | 118 ++ .../assets/stylesheets/main/_typeset.scss | 603 +++++++++ .../stylesheets/main/components/_author.scss | 86 ++ .../stylesheets/main/components/_banner.scss | 68 ++ .../assets/stylesheets/main/components/_base.scss | 182 +++ .../stylesheets/main/components/_clipboard.scss | 102 ++ .../stylesheets/main/components/_consent.scss | 127 ++ .../stylesheets/main/components/_content.scss | 97 ++ .../stylesheets/main/components/_dialog.scss | 65 + .../stylesheets/main/components/_feedback.scss | 110 ++ .../stylesheets/main/components/_footer.scss | 201 +++ .../assets/stylesheets/main/components/_form.scss | 83 ++ .../stylesheets/main/components/_header.scss | 270 +++++ .../assets/stylesheets/main/components/_meta.scss | 67 + .../assets/stylesheets/main/components/_nav.scss | 761 ++++++++++++ .../stylesheets/main/components/_pagination.scss | 85 ++ .../assets/stylesheets/main/components/_post.scss | 196 +++ .../stylesheets/main/components/_progress.scss | 53 + .../stylesheets/main/components/_search.scss | 707 +++++++++++ .../stylesheets/main/components/_select.scss | 115 ++ .../stylesheets/main/components/_sidebar.scss | 209 ++++ .../stylesheets/main/components/_source.scss | 182 +++ .../stylesheets/main/components/_status.scss | 73 ++ .../assets/stylesheets/main/components/_tabs.scss | 133 ++ .../assets/stylesheets/main/components/_tag.scss | 105 ++ .../stylesheets/main/components/_tooltip.scss | 292 +++++ .../assets/stylesheets/main/components/_top.scss | 83 ++ .../stylesheets/main/components/_version.scss | 150 +++ .../main/extensions/markdown/_admonition.scss | 195 +++ .../main/extensions/markdown/_footnotes.scss | 146 +++ .../stylesheets/main/extensions/markdown/_toc.scss | 92 ++ .../main/extensions/pymdownx/_arithmatex.scss | 52 + .../main/extensions/pymdownx/_critic.scss | 76 ++ .../main/extensions/pymdownx/_details.scss | 121 ++ .../main/extensions/pymdownx/_emoji.scss | 43 + .../main/extensions/pymdownx/_highlight.scss | 382 ++++++ .../main/extensions/pymdownx/_keys.scss | 115 ++ .../main/extensions/pymdownx/_tabbed.scss | 400 ++++++ .../main/extensions/pymdownx/_tasklist.scss | 78 ++ .../stylesheets/main/integrations/_mermaid.scss | 67 + docs/src/templates/assets/stylesheets/palette.scss | 40 + .../assets/stylesheets/palette/_accent.scss | 61 + .../assets/stylesheets/palette/_primary.scss | 203 ++++ .../assets/stylesheets/palette/_scheme.scss | 145 +++ .../assets/stylesheets/utilities/_break.scss | 219 ++++ .../assets/stylesheets/utilities/_convert.scss | 79 ++ docs/src/templates/base.html | 445 +++++++ docs/src/templates/blog-post.html | 164 +++ docs/src/templates/blog.html | 48 + docs/src/templates/main.html | 23 + docs/src/templates/mkdocs_theme.yml | 50 + docs/src/templates/partials/actions.html | 54 + docs/src/templates/partials/alternate.html | 49 + docs/src/templates/partials/comments.html | 23 + docs/src/templates/partials/consent.html | 107 ++ docs/src/templates/partials/content.html | 54 + docs/src/templates/partials/copyright.html | 39 + docs/src/templates/partials/feedback.html | 79 ++ docs/src/templates/partials/footer.html | 98 ++ docs/src/templates/partials/header.html | 112 ++ docs/src/templates/partials/icons.html | 72 ++ .../templates/partials/integrations/analytics.html | 49 + .../partials/integrations/analytics/google.html | 97 ++ .../templates/partials/javascripts/announce.html | 31 + docs/src/templates/partials/javascripts/base.html | 48 + .../templates/partials/javascripts/consent.html | 61 + .../templates/partials/javascripts/content.html | 39 + .../templates/partials/javascripts/outdated.html | 29 + .../templates/partials/javascripts/palette.html | 29 + docs/src/templates/partials/language.html | 28 + docs/src/templates/partials/languages/af.html | 76 ++ docs/src/templates/partials/languages/ar.html | 77 ++ docs/src/templates/partials/languages/be.html | 77 ++ docs/src/templates/partials/languages/bg.html | 76 ++ docs/src/templates/partials/languages/bn.html | 76 ++ docs/src/templates/partials/languages/ca.html | 75 ++ docs/src/templates/partials/languages/cs.html | 75 ++ docs/src/templates/partials/languages/da.html | 76 ++ docs/src/templates/partials/languages/de.html | 76 ++ docs/src/templates/partials/languages/el.html | 74 ++ docs/src/templates/partials/languages/en.html | 79 ++ docs/src/templates/partials/languages/eo.html | 49 + docs/src/templates/partials/languages/es.html | 76 ++ docs/src/templates/partials/languages/et.html | 43 + docs/src/templates/partials/languages/eu.html | 75 ++ docs/src/templates/partials/languages/fa.html | 77 ++ docs/src/templates/partials/languages/fi.html | 44 + docs/src/templates/partials/languages/fr.html | 76 ++ docs/src/templates/partials/languages/gl.html | 56 + docs/src/templates/partials/languages/he.html | 77 ++ docs/src/templates/partials/languages/hi.html | 76 ++ docs/src/templates/partials/languages/hr.html | 75 ++ docs/src/templates/partials/languages/hu.html | 76 ++ docs/src/templates/partials/languages/hy.html | 76 ++ docs/src/templates/partials/languages/id.html | 76 ++ docs/src/templates/partials/languages/is.html | 75 ++ docs/src/templates/partials/languages/it.html | 76 ++ docs/src/templates/partials/languages/ja.html | 78 ++ docs/src/templates/partials/languages/ka.html | 49 + docs/src/templates/partials/languages/kn.html | 75 ++ docs/src/templates/partials/languages/ko.html | 76 ++ docs/src/templates/partials/languages/ku-IQ.html | 64 + docs/src/templates/partials/languages/lb.html | 76 ++ docs/src/templates/partials/languages/lt.html | 76 ++ docs/src/templates/partials/languages/lv.html | 55 + docs/src/templates/partials/languages/mk.html | 56 + docs/src/templates/partials/languages/mn.html | 51 + docs/src/templates/partials/languages/ms.html | 55 + docs/src/templates/partials/languages/my.html | 49 + docs/src/templates/partials/languages/nb.html | 76 ++ docs/src/templates/partials/languages/nl.html | 76 ++ docs/src/templates/partials/languages/nn.html | 62 + docs/src/templates/partials/languages/pl.html | 76 ++ docs/src/templates/partials/languages/pt-BR.html | 76 ++ docs/src/templates/partials/languages/pt.html | 76 ++ docs/src/templates/partials/languages/ro.html | 76 ++ docs/src/templates/partials/languages/ru.html | 76 ++ docs/src/templates/partials/languages/sa.html | 75 ++ docs/src/templates/partials/languages/sh.html | 70 ++ docs/src/templates/partials/languages/si.html | 51 + docs/src/templates/partials/languages/sk.html | 43 + docs/src/templates/partials/languages/sl.html | 76 ++ docs/src/templates/partials/languages/sr.html | 57 + docs/src/templates/partials/languages/sv.html | 76 ++ docs/src/templates/partials/languages/te.html | 75 ++ docs/src/templates/partials/languages/th.html | 76 ++ docs/src/templates/partials/languages/tl.html | 57 + docs/src/templates/partials/languages/tr.html | 76 ++ docs/src/templates/partials/languages/uk.html | 75 ++ docs/src/templates/partials/languages/ur.html | 77 ++ docs/src/templates/partials/languages/uz.html | 76 ++ docs/src/templates/partials/languages/vi.html | 76 ++ docs/src/templates/partials/languages/zh-Hant.html | 77 ++ docs/src/templates/partials/languages/zh-TW.html | 77 ++ docs/src/templates/partials/languages/zh.html | 77 ++ docs/src/templates/partials/logo.html | 29 + docs/src/templates/partials/nav-item.html | 249 ++++ docs/src/templates/partials/nav.html | 69 ++ docs/src/templates/partials/pagination.html | 42 + docs/src/templates/partials/palette.html | 55 + docs/src/templates/partials/post.html | 99 ++ docs/src/templates/partials/progress.html | 24 + docs/src/templates/partials/search.html | 109 ++ docs/src/templates/partials/social.html | 48 + docs/src/templates/partials/source-file.html | 44 + docs/src/templates/partials/source.html | 37 + docs/src/templates/partials/tabs-item.html | 71 ++ docs/src/templates/partials/tabs.html | 38 + docs/src/templates/partials/tags.html | 52 + docs/src/templates/partials/toc-item.html | 39 + docs/src/templates/partials/toc.html | 56 + docs/src/templates/partials/top.html | 28 + docs/src/templates/redirect.html | 41 + docs/src/test.py | 4 + examples/rule-pypackage/__init__.py | 0 examples/rule-pypackage/config.py | 0 examples/rule-singlefile.py | 10 - mkdocs.yml | 4 +- pdm.lock | 1281 +------------------- pyproject.toml | 17 +- requirements.txt | 14 - src/.gitignore | 2 - src/__init__.py | 21 - src/extensions/__init__.py | 19 - src/extensions/emoji.py | 98 -- src/hydrorollcore/__init__.py | 6 - src/hydrorollcore/cli.py | 45 - src/hydrorollcore/consts/__init__.py | 0 src/hydrorollcore/consts/templates.py | 69 -- src/hydrorollcore/event.py | 43 - src/hydrorollcore/exceptions.py | 6 - src/hydrorollcore/logging.py | 40 - src/hydrorollcore/manager.py | 18 - src/hydrorollcore/rule.py | 75 -- src/hydrorollcore/settings.py | 1 - src/hydrorollcore/typing.py | 41 - src/infini/__init__.py | 6 + src/infini/cli.py | 45 + src/infini/consts/__init__.py | 0 src/infini/consts/templates.py | 69 ++ src/infini/event.py | 43 + src/infini/exceptions.py | 6 + src/infini/logging.py | 40 + src/infini/manager.py | 18 + src/infini/rule.py | 75 ++ src/infini/settings.py | 1 + src/infini/typing.py | 41 + .../assets/javascripts/components/_/index.ts | 104 -- .../javascripts/components/iconsearch/_/index.ts | 94 -- .../javascripts/components/iconsearch/index.ts | 25 - .../components/iconsearch/query/index.ts | 96 -- .../components/iconsearch/result/index.ts | 237 ---- .../assets/javascripts/components/index.ts | 25 - .../javascripts/components/sponsorship/index.ts | 149 --- src/overrides/assets/javascripts/custom.ts | 55 - .../javascripts/integrations/analytics/index.ts | 42 - .../assets/javascripts/integrations/index.ts | 23 - .../javascripts/templates/iconsearch/index.tsx | 95 -- .../assets/javascripts/templates/index.ts | 24 - .../javascripts/templates/sponsorship/index.tsx | 67 - src/overrides/assets/stylesheets/custom.scss | 44 - .../assets/stylesheets/custom/_typeset.scss | 294 ----- .../assets/stylesheets/custom/layout/_banner.scss | 66 - .../assets/stylesheets/custom/layout/_hero.scss | 123 -- .../stylesheets/custom/layout/_iconsearch.scss | 136 --- .../stylesheets/custom/layout/_sponsorship.scss | 128 -- src/overrides/home.html | 106 -- src/overrides/hooks/shortcodes.py | 283 ----- src/overrides/hooks/translations.html | 54 - src/overrides/hooks/translations.py | 193 --- src/overrides/main.html | 59 - src/plugins/__init__.py | 19 - src/plugins/blog/__init__.py | 19 - src/plugins/blog/author.py | 38 - src/plugins/blog/config.py | 88 -- src/plugins/blog/plugin.py | 884 -------------- src/plugins/blog/readtime/__init__.py | 51 - src/plugins/blog/readtime/parser.py | 45 - src/plugins/blog/structure/__init__.py | 292 ----- src/plugins/blog/structure/config.py | 37 - src/plugins/blog/structure/markdown.py | 58 - src/plugins/blog/structure/options.py | 87 -- src/plugins/blog/templates/__init__.py | 42 - src/plugins/group/__init__.py | 19 - src/plugins/group/config.py | 33 - src/plugins/group/plugin.py | 151 --- src/plugins/info/__init__.py | 19 - src/plugins/info/config.py | 35 - src/plugins/info/plugin.py | 245 ---- src/plugins/offline/__init__.py | 19 - src/plugins/offline/config.py | 30 - src/plugins/offline/plugin.py | 69 -- src/plugins/search/__init__.py | 19 - src/plugins/search/config.py | 58 - src/plugins/search/plugin.py | 580 --------- src/plugins/social/__init__.py | 19 - src/plugins/social/config.py | 48 - src/plugins/social/plugin.py | 516 -------- src/plugins/tags/__init__.py | 27 - src/plugins/tags/config.py | 38 - src/plugins/tags/plugin.py | 182 --- src/templates/.icons/logo.afdesign | Bin 31465 -> 0 bytes src/templates/.icons/logo.svg | 6 - src/templates/404.html | 28 - src/templates/__init__.py | 19 - src/templates/assets/images/favicon.png | Bin 1870 -> 0 bytes src/templates/assets/javascripts/_/index.ts | 148 --- .../assets/javascripts/browser/document/index.ts | 48 - .../assets/javascripts/browser/element/_/.eslintrc | 6 - .../assets/javascripts/browser/element/_/index.ts | 120 -- .../javascripts/browser/element/focus/index.ts | 81 -- .../assets/javascripts/browser/element/index.ts | 27 - .../javascripts/browser/element/offset/_/index.ts | 86 -- .../browser/element/offset/content/index.ts | 76 -- .../javascripts/browser/element/offset/index.ts | 24 - .../javascripts/browser/element/size/_/index.ts | 151 --- .../browser/element/size/content/index.ts | 67 - .../javascripts/browser/element/size/index.ts | 24 - .../browser/element/visibility/index.ts | 131 -- src/templates/assets/javascripts/browser/index.ts | 32 - .../assets/javascripts/browser/keyboard/index.ts | 148 --- .../assets/javascripts/browser/location/_/index.ts | 85 -- .../javascripts/browser/location/hash/index.ts | 104 -- .../assets/javascripts/browser/location/index.ts | 24 - .../assets/javascripts/browser/media/index.ts | 95 -- .../assets/javascripts/browser/request/index.ts | 141 --- .../assets/javascripts/browser/script/index.ts | 70 -- .../assets/javascripts/browser/toggle/index.ts | 102 -- .../assets/javascripts/browser/viewport/_/index.ts | 69 -- .../javascripts/browser/viewport/at/index.ts | 84 -- .../assets/javascripts/browser/viewport/index.ts | 26 - .../javascripts/browser/viewport/offset/index.ts | 78 -- .../javascripts/browser/viewport/size/index.ts | 71 -- .../assets/javascripts/browser/worker/index.ts | 112 -- src/templates/assets/javascripts/bundle.ts | 316 ----- .../assets/javascripts/components/_/index.ts | 138 --- .../javascripts/components/announce/index.ts | 110 -- .../assets/javascripts/components/consent/index.ts | 116 -- .../javascripts/components/content/_/index.ts | 125 -- .../components/content/annotation/_/index.ts | 272 ----- .../components/content/annotation/block/index.ts | 88 -- .../components/content/annotation/index.ts | 25 - .../components/content/annotation/list/index.ts | 209 ---- .../javascripts/components/content/code/_/index.ts | 238 ---- .../javascripts/components/content/code/index.ts | 23 - .../components/content/details/index.ts | 138 --- .../assets/javascripts/components/content/index.ts | 28 - .../components/content/mermaid/index.css | 430 ------- .../components/content/mermaid/index.ts | 133 -- .../javascripts/components/content/table/index.ts | 70 -- .../javascripts/components/content/tabs/index.ts | 265 ---- .../assets/javascripts/components/dialog/index.ts | 128 -- .../javascripts/components/header/_/index.ts | 200 --- .../assets/javascripts/components/header/index.ts | 24 - .../javascripts/components/header/title/index.ts | 144 --- .../assets/javascripts/components/index.ts | 37 - .../assets/javascripts/components/main/index.ts | 125 -- .../assets/javascripts/components/palette/index.ts | 180 --- .../javascripts/components/progress/index.ts | 87 -- .../javascripts/components/search/_/index.ts | 239 ---- .../components/search/highlight/.eslintrc | 5 - .../components/search/highlight/index.ts | 115 -- .../assets/javascripts/components/search/index.ts | 28 - .../javascripts/components/search/query/index.ts | 206 ---- .../javascripts/components/search/result/index.ts | 197 --- .../javascripts/components/search/share/index.ts | 135 --- .../javascripts/components/search/suggest/index.ts | 154 --- .../assets/javascripts/components/sidebar/index.ts | 227 ---- .../javascripts/components/source/_/index.ts | 142 --- .../javascripts/components/source/facts/_/index.ts | 88 -- .../components/source/facts/github/index.ts | 103 -- .../components/source/facts/gitlab/index.ts | 61 - .../javascripts/components/source/facts/index.ts | 25 - .../assets/javascripts/components/source/index.ts | 24 - .../assets/javascripts/components/tabs/index.ts | 144 --- .../assets/javascripts/components/toc/index.ts | 379 ------ .../assets/javascripts/components/top/index.ts | 184 --- .../javascripts/integrations/clipboard/index.ts | 99 -- .../assets/javascripts/integrations/index.ts | 27 - .../javascripts/integrations/instant/.eslintrc | 6 - .../javascripts/integrations/instant/index.ts | 446 ------- .../javascripts/integrations/search/_/index.ts | 332 ----- .../integrations/search/config/index.ts | 115 -- .../integrations/search/highlighter/index.ts | 93 -- .../javascripts/integrations/search/index.ts | 27 - .../integrations/search/internal/.eslintrc | 6 - .../integrations/search/internal/_/index.ts | 74 -- .../integrations/search/internal/extract/index.ts | 107 -- .../search/internal/highlight/index.ts | 162 --- .../integrations/search/internal/index.ts | 26 - .../integrations/search/internal/tokenize/index.ts | 136 --- .../integrations/search/query/.eslintrc | 6 - .../integrations/search/query/_/index.ts | 172 --- .../javascripts/integrations/search/query/index.ts | 25 - .../integrations/search/query/segment/index.ts | 81 -- .../integrations/search/query/transform/index.ts | 99 -- .../integrations/search/worker/_/index.ts | 95 -- .../integrations/search/worker/index.ts | 24 - .../integrations/search/worker/main/.eslintrc | 6 - .../integrations/search/worker/main/index.ts | 192 --- .../integrations/search/worker/message/index.ts | 112 -- .../javascripts/integrations/sitemap/index.ts | 107 -- .../javascripts/integrations/version/.eslintrc | 5 - .../javascripts/integrations/version/index.ts | 186 --- .../javascripts/patches/indeterminate/index.ts | 85 -- src/templates/assets/javascripts/patches/index.ts | 25 - .../assets/javascripts/patches/scrollfix/index.ts | 100 -- .../assets/javascripts/patches/scrolllock/index.ts | 89 -- .../assets/javascripts/polyfills/index.ts | 96 -- .../javascripts/templates/annotation/index.tsx | 65 - .../javascripts/templates/clipboard/index.tsx | 45 - .../assets/javascripts/templates/index.ts | 29 - .../assets/javascripts/templates/search/index.tsx | 170 --- .../assets/javascripts/templates/source/index.tsx | 47 - .../assets/javascripts/templates/tabbed/index.tsx | 56 - .../assets/javascripts/templates/table/index.tsx | 44 - .../assets/javascripts/templates/tooltip/index.tsx | 42 - .../assets/javascripts/templates/version/index.tsx | 92 -- .../assets/javascripts/utilities/h/.eslintrc | 7 - .../assets/javascripts/utilities/h/index.ts | 132 -- .../assets/javascripts/utilities/index.ts | 24 - .../assets/javascripts/utilities/round/index.ts | 50 - src/templates/assets/javascripts/workers/search.ts | 23 - src/templates/assets/stylesheets/_config.scss | 42 - src/templates/assets/stylesheets/main.scss | 86 -- src/templates/assets/stylesheets/main/_colors.scss | 153 --- src/templates/assets/stylesheets/main/_icons.scss | 37 - .../assets/stylesheets/main/_modifiers.scss | 48 - src/templates/assets/stylesheets/main/_resets.scss | 118 -- .../assets/stylesheets/main/_typeset.scss | 603 --------- .../stylesheets/main/components/_author.scss | 86 -- .../stylesheets/main/components/_banner.scss | 68 -- .../assets/stylesheets/main/components/_base.scss | 182 --- .../stylesheets/main/components/_clipboard.scss | 102 -- .../stylesheets/main/components/_consent.scss | 127 -- .../stylesheets/main/components/_content.scss | 97 -- .../stylesheets/main/components/_dialog.scss | 65 - .../stylesheets/main/components/_feedback.scss | 110 -- .../stylesheets/main/components/_footer.scss | 201 --- .../assets/stylesheets/main/components/_form.scss | 83 -- .../stylesheets/main/components/_header.scss | 270 ----- .../assets/stylesheets/main/components/_meta.scss | 67 - .../assets/stylesheets/main/components/_nav.scss | 761 ------------ .../stylesheets/main/components/_pagination.scss | 85 -- .../assets/stylesheets/main/components/_post.scss | 196 --- .../stylesheets/main/components/_progress.scss | 53 - .../stylesheets/main/components/_search.scss | 707 ----------- .../stylesheets/main/components/_select.scss | 115 -- .../stylesheets/main/components/_sidebar.scss | 209 ---- .../stylesheets/main/components/_source.scss | 182 --- .../stylesheets/main/components/_status.scss | 73 -- .../assets/stylesheets/main/components/_tabs.scss | 133 -- .../assets/stylesheets/main/components/_tag.scss | 105 -- .../stylesheets/main/components/_tooltip.scss | 292 ----- .../assets/stylesheets/main/components/_top.scss | 83 -- .../stylesheets/main/components/_version.scss | 150 --- .../main/extensions/markdown/_admonition.scss | 195 --- .../main/extensions/markdown/_footnotes.scss | 146 --- .../stylesheets/main/extensions/markdown/_toc.scss | 92 -- .../main/extensions/pymdownx/_arithmatex.scss | 52 - .../main/extensions/pymdownx/_critic.scss | 76 -- .../main/extensions/pymdownx/_details.scss | 121 -- .../main/extensions/pymdownx/_emoji.scss | 43 - .../main/extensions/pymdownx/_highlight.scss | 382 ------ .../main/extensions/pymdownx/_keys.scss | 115 -- .../main/extensions/pymdownx/_tabbed.scss | 400 ------ .../main/extensions/pymdownx/_tasklist.scss | 78 -- .../stylesheets/main/integrations/_mermaid.scss | 67 - src/templates/assets/stylesheets/palette.scss | 40 - .../assets/stylesheets/palette/_accent.scss | 61 - .../assets/stylesheets/palette/_primary.scss | 203 ---- .../assets/stylesheets/palette/_scheme.scss | 145 --- .../assets/stylesheets/utilities/_break.scss | 219 ---- .../assets/stylesheets/utilities/_convert.scss | 79 -- src/templates/base.html | 445 ------- src/templates/blog-post.html | 164 --- src/templates/blog.html | 48 - src/templates/main.html | 23 - src/templates/mkdocs_theme.yml | 50 - src/templates/partials/actions.html | 54 - src/templates/partials/alternate.html | 49 - src/templates/partials/comments.html | 23 - src/templates/partials/consent.html | 107 -- src/templates/partials/content.html | 54 - src/templates/partials/copyright.html | 39 - src/templates/partials/feedback.html | 79 -- src/templates/partials/footer.html | 98 -- src/templates/partials/header.html | 112 -- src/templates/partials/icons.html | 72 -- src/templates/partials/integrations/analytics.html | 49 - .../partials/integrations/analytics/google.html | 97 -- src/templates/partials/javascripts/announce.html | 31 - src/templates/partials/javascripts/base.html | 48 - src/templates/partials/javascripts/consent.html | 61 - src/templates/partials/javascripts/content.html | 39 - src/templates/partials/javascripts/outdated.html | 29 - src/templates/partials/javascripts/palette.html | 29 - src/templates/partials/language.html | 28 - src/templates/partials/languages/af.html | 76 -- src/templates/partials/languages/ar.html | 77 -- src/templates/partials/languages/be.html | 77 -- src/templates/partials/languages/bg.html | 76 -- src/templates/partials/languages/bn.html | 76 -- src/templates/partials/languages/ca.html | 75 -- src/templates/partials/languages/cs.html | 75 -- src/templates/partials/languages/da.html | 76 -- src/templates/partials/languages/de.html | 76 -- src/templates/partials/languages/el.html | 74 -- src/templates/partials/languages/en.html | 79 -- src/templates/partials/languages/eo.html | 49 - src/templates/partials/languages/es.html | 76 -- src/templates/partials/languages/et.html | 43 - src/templates/partials/languages/eu.html | 75 -- src/templates/partials/languages/fa.html | 77 -- src/templates/partials/languages/fi.html | 44 - src/templates/partials/languages/fr.html | 76 -- src/templates/partials/languages/gl.html | 56 - src/templates/partials/languages/he.html | 77 -- src/templates/partials/languages/hi.html | 76 -- src/templates/partials/languages/hr.html | 75 -- src/templates/partials/languages/hu.html | 76 -- src/templates/partials/languages/hy.html | 76 -- src/templates/partials/languages/id.html | 76 -- src/templates/partials/languages/is.html | 75 -- src/templates/partials/languages/it.html | 76 -- src/templates/partials/languages/ja.html | 78 -- src/templates/partials/languages/ka.html | 49 - src/templates/partials/languages/kn.html | 75 -- src/templates/partials/languages/ko.html | 76 -- src/templates/partials/languages/ku-IQ.html | 64 - src/templates/partials/languages/lb.html | 76 -- src/templates/partials/languages/lt.html | 76 -- src/templates/partials/languages/lv.html | 55 - src/templates/partials/languages/mk.html | 56 - src/templates/partials/languages/mn.html | 51 - src/templates/partials/languages/ms.html | 55 - src/templates/partials/languages/my.html | 49 - src/templates/partials/languages/nb.html | 76 -- src/templates/partials/languages/nl.html | 76 -- src/templates/partials/languages/nn.html | 62 - src/templates/partials/languages/pl.html | 76 -- src/templates/partials/languages/pt-BR.html | 76 -- src/templates/partials/languages/pt.html | 76 -- src/templates/partials/languages/ro.html | 76 -- src/templates/partials/languages/ru.html | 76 -- src/templates/partials/languages/sa.html | 75 -- src/templates/partials/languages/sh.html | 70 -- src/templates/partials/languages/si.html | 51 - src/templates/partials/languages/sk.html | 43 - src/templates/partials/languages/sl.html | 76 -- src/templates/partials/languages/sr.html | 57 - src/templates/partials/languages/sv.html | 76 -- src/templates/partials/languages/te.html | 75 -- src/templates/partials/languages/th.html | 76 -- src/templates/partials/languages/tl.html | 57 - src/templates/partials/languages/tr.html | 76 -- src/templates/partials/languages/uk.html | 75 -- src/templates/partials/languages/ur.html | 77 -- src/templates/partials/languages/uz.html | 76 -- src/templates/partials/languages/vi.html | 76 -- src/templates/partials/languages/zh-Hant.html | 77 -- src/templates/partials/languages/zh-TW.html | 77 -- src/templates/partials/languages/zh.html | 77 -- src/templates/partials/logo.html | 29 - src/templates/partials/nav-item.html | 249 ---- src/templates/partials/nav.html | 69 -- src/templates/partials/pagination.html | 42 - src/templates/partials/palette.html | 55 - src/templates/partials/post.html | 99 -- src/templates/partials/progress.html | 24 - src/templates/partials/search.html | 109 -- src/templates/partials/social.html | 48 - src/templates/partials/source-file.html | 44 - src/templates/partials/source.html | 37 - src/templates/partials/tabs-item.html | 71 -- src/templates/partials/tabs.html | 38 - src/templates/partials/tags.html | 52 - src/templates/partials/toc-item.html | 39 - src/templates/partials/toc.html | 56 - src/templates/partials/top.html | 28 - src/templates/redirect.html | 41 - src/test.py | 4 - 710 files changed, 34905 insertions(+), 36191 deletions(-) create mode 100644 docs/requirements.txt create mode 100644 docs/src/.gitignore create mode 100644 docs/src/__init__.py create mode 100644 docs/src/extensions/__init__.py create mode 100644 docs/src/extensions/emoji.py create mode 100644 docs/src/overrides/assets/javascripts/components/_/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/iconsearch/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/index.ts create mode 100644 docs/src/overrides/assets/javascripts/components/sponsorship/index.ts create mode 100644 docs/src/overrides/assets/javascripts/custom.ts create mode 100644 docs/src/overrides/assets/javascripts/integrations/analytics/index.ts create mode 100644 docs/src/overrides/assets/javascripts/integrations/index.ts create mode 100644 docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx create mode 100644 docs/src/overrides/assets/javascripts/templates/index.ts create mode 100644 docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx create mode 100644 docs/src/overrides/assets/stylesheets/custom.scss create mode 100644 docs/src/overrides/assets/stylesheets/custom/_typeset.scss create mode 100644 docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss create mode 100644 docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss create mode 100644 docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss create mode 100644 docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss create mode 100644 docs/src/overrides/home.html create mode 100644 docs/src/overrides/hooks/shortcodes.py create mode 100644 docs/src/overrides/hooks/translations.html create mode 100644 docs/src/overrides/hooks/translations.py create mode 100644 docs/src/overrides/main.html create mode 100644 docs/src/plugins/__init__.py create mode 100644 docs/src/plugins/blog/__init__.py create mode 100644 docs/src/plugins/blog/author.py create mode 100644 docs/src/plugins/blog/config.py create mode 100644 docs/src/plugins/blog/plugin.py create mode 100644 docs/src/plugins/blog/readtime/__init__.py create mode 100644 docs/src/plugins/blog/readtime/parser.py create mode 100644 docs/src/plugins/blog/structure/__init__.py create mode 100644 docs/src/plugins/blog/structure/config.py create mode 100644 docs/src/plugins/blog/structure/markdown.py create mode 100644 docs/src/plugins/blog/structure/options.py create mode 100644 docs/src/plugins/blog/templates/__init__.py create mode 100644 docs/src/plugins/group/__init__.py create mode 100644 docs/src/plugins/group/config.py create mode 100644 docs/src/plugins/group/plugin.py create mode 100644 docs/src/plugins/info/__init__.py create mode 100644 docs/src/plugins/info/config.py create mode 100644 docs/src/plugins/info/plugin.py create mode 100644 docs/src/plugins/offline/__init__.py create mode 100644 docs/src/plugins/offline/config.py create mode 100644 docs/src/plugins/offline/plugin.py create mode 100644 docs/src/plugins/search/__init__.py create mode 100644 docs/src/plugins/search/config.py create mode 100644 docs/src/plugins/search/plugin.py create mode 100644 docs/src/plugins/social/__init__.py create mode 100644 docs/src/plugins/social/config.py create mode 100644 docs/src/plugins/social/plugin.py create mode 100644 docs/src/plugins/tags/__init__.py create mode 100644 docs/src/plugins/tags/config.py create mode 100644 docs/src/plugins/tags/plugin.py create mode 100644 docs/src/templates/.icons/logo.afdesign create mode 100644 docs/src/templates/.icons/logo.svg create mode 100644 docs/src/templates/404.html create mode 100644 docs/src/templates/__init__.py create mode 100644 docs/src/templates/assets/images/favicon.png create mode 100644 docs/src/templates/assets/javascripts/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/document/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/_/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/browser/element/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/focus/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/offset/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/offset/content/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/offset/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/size/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/size/content/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/size/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/element/visibility/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/keyboard/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/location/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/location/hash/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/location/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/media/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/request/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/script/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/toggle/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/viewport/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/viewport/at/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/viewport/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/viewport/offset/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/viewport/size/index.ts create mode 100644 docs/src/templates/assets/javascripts/browser/worker/index.ts create mode 100644 docs/src/templates/assets/javascripts/bundle.ts create mode 100644 docs/src/templates/assets/javascripts/components/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/announce/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/consent/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/annotation/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/annotation/block/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/annotation/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/annotation/list/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/code/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/code/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/details/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/mermaid/index.css create mode 100644 docs/src/templates/assets/javascripts/components/content/mermaid/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/table/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/content/tabs/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/dialog/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/header/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/header/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/header/title/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/main/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/palette/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/progress/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/highlight/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/components/search/highlight/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/query/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/result/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/share/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/search/suggest/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/sidebar/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/facts/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/facts/github/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/facts/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/source/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/tabs/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/toc/index.ts create mode 100644 docs/src/templates/assets/javascripts/components/top/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/clipboard/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/instant/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/integrations/instant/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/config/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/highlighter/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/query/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/integrations/search/query/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/query/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/query/segment/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/query/transform/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/worker/_/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/worker/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/integrations/search/worker/main/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/search/worker/message/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/sitemap/index.ts create mode 100644 docs/src/templates/assets/javascripts/integrations/version/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/integrations/version/index.ts create mode 100644 docs/src/templates/assets/javascripts/patches/indeterminate/index.ts create mode 100644 docs/src/templates/assets/javascripts/patches/index.ts create mode 100644 docs/src/templates/assets/javascripts/patches/scrollfix/index.ts create mode 100644 docs/src/templates/assets/javascripts/patches/scrolllock/index.ts create mode 100644 docs/src/templates/assets/javascripts/polyfills/index.ts create mode 100644 docs/src/templates/assets/javascripts/templates/annotation/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/clipboard/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/index.ts create mode 100644 docs/src/templates/assets/javascripts/templates/search/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/source/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/tabbed/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/table/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/tooltip/index.tsx create mode 100644 docs/src/templates/assets/javascripts/templates/version/index.tsx create mode 100644 docs/src/templates/assets/javascripts/utilities/h/.eslintrc create mode 100644 docs/src/templates/assets/javascripts/utilities/h/index.ts create mode 100644 docs/src/templates/assets/javascripts/utilities/index.ts create mode 100644 docs/src/templates/assets/javascripts/utilities/round/index.ts create mode 100644 docs/src/templates/assets/javascripts/workers/search.ts create mode 100644 docs/src/templates/assets/stylesheets/_config.scss create mode 100644 docs/src/templates/assets/stylesheets/main.scss create mode 100644 docs/src/templates/assets/stylesheets/main/_colors.scss create mode 100644 docs/src/templates/assets/stylesheets/main/_icons.scss create mode 100644 docs/src/templates/assets/stylesheets/main/_modifiers.scss create mode 100644 docs/src/templates/assets/stylesheets/main/_resets.scss create mode 100644 docs/src/templates/assets/stylesheets/main/_typeset.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_author.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_banner.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_base.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_clipboard.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_consent.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_content.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_dialog.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_feedback.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_footer.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_form.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_header.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_meta.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_nav.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_pagination.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_post.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_progress.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_search.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_select.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_sidebar.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_source.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_status.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_tabs.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_tag.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_tooltip.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_top.scss create mode 100644 docs/src/templates/assets/stylesheets/main/components/_version.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss create mode 100644 docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss create mode 100644 docs/src/templates/assets/stylesheets/main/integrations/_mermaid.scss create mode 100644 docs/src/templates/assets/stylesheets/palette.scss create mode 100644 docs/src/templates/assets/stylesheets/palette/_accent.scss create mode 100644 docs/src/templates/assets/stylesheets/palette/_primary.scss create mode 100644 docs/src/templates/assets/stylesheets/palette/_scheme.scss create mode 100644 docs/src/templates/assets/stylesheets/utilities/_break.scss create mode 100644 docs/src/templates/assets/stylesheets/utilities/_convert.scss create mode 100644 docs/src/templates/base.html create mode 100644 docs/src/templates/blog-post.html create mode 100644 docs/src/templates/blog.html create mode 100644 docs/src/templates/main.html create mode 100644 docs/src/templates/mkdocs_theme.yml create mode 100644 docs/src/templates/partials/actions.html create mode 100644 docs/src/templates/partials/alternate.html create mode 100644 docs/src/templates/partials/comments.html create mode 100644 docs/src/templates/partials/consent.html create mode 100644 docs/src/templates/partials/content.html create mode 100644 docs/src/templates/partials/copyright.html create mode 100644 docs/src/templates/partials/feedback.html create mode 100644 docs/src/templates/partials/footer.html create mode 100644 docs/src/templates/partials/header.html create mode 100644 docs/src/templates/partials/icons.html create mode 100644 docs/src/templates/partials/integrations/analytics.html create mode 100644 docs/src/templates/partials/integrations/analytics/google.html create mode 100644 docs/src/templates/partials/javascripts/announce.html create mode 100644 docs/src/templates/partials/javascripts/base.html create mode 100644 docs/src/templates/partials/javascripts/consent.html create mode 100644 docs/src/templates/partials/javascripts/content.html create mode 100644 docs/src/templates/partials/javascripts/outdated.html create mode 100644 docs/src/templates/partials/javascripts/palette.html create mode 100644 docs/src/templates/partials/language.html create mode 100644 docs/src/templates/partials/languages/af.html create mode 100644 docs/src/templates/partials/languages/ar.html create mode 100644 docs/src/templates/partials/languages/be.html create mode 100644 docs/src/templates/partials/languages/bg.html create mode 100644 docs/src/templates/partials/languages/bn.html create mode 100644 docs/src/templates/partials/languages/ca.html create mode 100644 docs/src/templates/partials/languages/cs.html create mode 100644 docs/src/templates/partials/languages/da.html create mode 100644 docs/src/templates/partials/languages/de.html create mode 100644 docs/src/templates/partials/languages/el.html create mode 100644 docs/src/templates/partials/languages/en.html create mode 100644 docs/src/templates/partials/languages/eo.html create mode 100644 docs/src/templates/partials/languages/es.html create mode 100644 docs/src/templates/partials/languages/et.html create mode 100644 docs/src/templates/partials/languages/eu.html create mode 100644 docs/src/templates/partials/languages/fa.html create mode 100644 docs/src/templates/partials/languages/fi.html create mode 100644 docs/src/templates/partials/languages/fr.html create mode 100644 docs/src/templates/partials/languages/gl.html create mode 100644 docs/src/templates/partials/languages/he.html create mode 100644 docs/src/templates/partials/languages/hi.html create mode 100644 docs/src/templates/partials/languages/hr.html create mode 100644 docs/src/templates/partials/languages/hu.html create mode 100644 docs/src/templates/partials/languages/hy.html create mode 100644 docs/src/templates/partials/languages/id.html create mode 100644 docs/src/templates/partials/languages/is.html create mode 100644 docs/src/templates/partials/languages/it.html create mode 100644 docs/src/templates/partials/languages/ja.html create mode 100644 docs/src/templates/partials/languages/ka.html create mode 100644 docs/src/templates/partials/languages/kn.html create mode 100644 docs/src/templates/partials/languages/ko.html create mode 100644 docs/src/templates/partials/languages/ku-IQ.html create mode 100644 docs/src/templates/partials/languages/lb.html create mode 100644 docs/src/templates/partials/languages/lt.html create mode 100644 docs/src/templates/partials/languages/lv.html create mode 100644 docs/src/templates/partials/languages/mk.html create mode 100644 docs/src/templates/partials/languages/mn.html create mode 100644 docs/src/templates/partials/languages/ms.html create mode 100644 docs/src/templates/partials/languages/my.html create mode 100644 docs/src/templates/partials/languages/nb.html create mode 100644 docs/src/templates/partials/languages/nl.html create mode 100644 docs/src/templates/partials/languages/nn.html create mode 100644 docs/src/templates/partials/languages/pl.html create mode 100644 docs/src/templates/partials/languages/pt-BR.html create mode 100644 docs/src/templates/partials/languages/pt.html create mode 100644 docs/src/templates/partials/languages/ro.html create mode 100644 docs/src/templates/partials/languages/ru.html create mode 100644 docs/src/templates/partials/languages/sa.html create mode 100644 docs/src/templates/partials/languages/sh.html create mode 100644 docs/src/templates/partials/languages/si.html create mode 100644 docs/src/templates/partials/languages/sk.html create mode 100644 docs/src/templates/partials/languages/sl.html create mode 100644 docs/src/templates/partials/languages/sr.html create mode 100644 docs/src/templates/partials/languages/sv.html create mode 100644 docs/src/templates/partials/languages/te.html create mode 100644 docs/src/templates/partials/languages/th.html create mode 100644 docs/src/templates/partials/languages/tl.html create mode 100644 docs/src/templates/partials/languages/tr.html create mode 100644 docs/src/templates/partials/languages/uk.html create mode 100644 docs/src/templates/partials/languages/ur.html create mode 100644 docs/src/templates/partials/languages/uz.html create mode 100644 docs/src/templates/partials/languages/vi.html create mode 100644 docs/src/templates/partials/languages/zh-Hant.html create mode 100644 docs/src/templates/partials/languages/zh-TW.html create mode 100644 docs/src/templates/partials/languages/zh.html create mode 100644 docs/src/templates/partials/logo.html create mode 100644 docs/src/templates/partials/nav-item.html create mode 100644 docs/src/templates/partials/nav.html create mode 100644 docs/src/templates/partials/pagination.html create mode 100644 docs/src/templates/partials/palette.html create mode 100644 docs/src/templates/partials/post.html create mode 100644 docs/src/templates/partials/progress.html create mode 100644 docs/src/templates/partials/search.html create mode 100644 docs/src/templates/partials/social.html create mode 100644 docs/src/templates/partials/source-file.html create mode 100644 docs/src/templates/partials/source.html create mode 100644 docs/src/templates/partials/tabs-item.html create mode 100644 docs/src/templates/partials/tabs.html create mode 100644 docs/src/templates/partials/tags.html create mode 100644 docs/src/templates/partials/toc-item.html create mode 100644 docs/src/templates/partials/toc.html create mode 100644 docs/src/templates/partials/top.html create mode 100644 docs/src/templates/redirect.html create mode 100644 docs/src/test.py delete mode 100644 examples/rule-pypackage/__init__.py delete mode 100644 examples/rule-pypackage/config.py delete mode 100644 examples/rule-singlefile.py delete mode 100644 requirements.txt delete mode 100644 src/.gitignore delete mode 100644 src/__init__.py delete mode 100644 src/extensions/__init__.py delete mode 100644 src/extensions/emoji.py delete mode 100644 src/hydrorollcore/__init__.py delete mode 100644 src/hydrorollcore/cli.py delete mode 100644 src/hydrorollcore/consts/__init__.py delete mode 100644 src/hydrorollcore/consts/templates.py delete mode 100644 src/hydrorollcore/event.py delete mode 100644 src/hydrorollcore/exceptions.py delete mode 100644 src/hydrorollcore/logging.py delete mode 100644 src/hydrorollcore/manager.py delete mode 100644 src/hydrorollcore/rule.py delete mode 100644 src/hydrorollcore/settings.py delete mode 100644 src/hydrorollcore/typing.py create mode 100644 src/infini/__init__.py create mode 100644 src/infini/cli.py create mode 100644 src/infini/consts/__init__.py create mode 100644 src/infini/consts/templates.py create mode 100644 src/infini/event.py create mode 100644 src/infini/exceptions.py create mode 100644 src/infini/logging.py create mode 100644 src/infini/manager.py create mode 100644 src/infini/rule.py create mode 100644 src/infini/settings.py create mode 100644 src/infini/typing.py delete mode 100644 src/overrides/assets/javascripts/components/_/index.ts delete mode 100644 src/overrides/assets/javascripts/components/iconsearch/_/index.ts delete mode 100644 src/overrides/assets/javascripts/components/iconsearch/index.ts delete mode 100644 src/overrides/assets/javascripts/components/iconsearch/query/index.ts delete mode 100644 src/overrides/assets/javascripts/components/iconsearch/result/index.ts delete mode 100644 src/overrides/assets/javascripts/components/index.ts delete mode 100644 src/overrides/assets/javascripts/components/sponsorship/index.ts delete mode 100644 src/overrides/assets/javascripts/custom.ts delete mode 100644 src/overrides/assets/javascripts/integrations/analytics/index.ts delete mode 100644 src/overrides/assets/javascripts/integrations/index.ts delete mode 100644 src/overrides/assets/javascripts/templates/iconsearch/index.tsx delete mode 100644 src/overrides/assets/javascripts/templates/index.ts delete mode 100644 src/overrides/assets/javascripts/templates/sponsorship/index.tsx delete mode 100644 src/overrides/assets/stylesheets/custom.scss delete mode 100644 src/overrides/assets/stylesheets/custom/_typeset.scss delete mode 100644 src/overrides/assets/stylesheets/custom/layout/_banner.scss delete mode 100644 src/overrides/assets/stylesheets/custom/layout/_hero.scss delete mode 100644 src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss delete mode 100644 src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss delete mode 100644 src/overrides/home.html delete mode 100644 src/overrides/hooks/shortcodes.py delete mode 100644 src/overrides/hooks/translations.html delete mode 100644 src/overrides/hooks/translations.py delete mode 100644 src/overrides/main.html delete mode 100644 src/plugins/__init__.py delete mode 100644 src/plugins/blog/__init__.py delete mode 100644 src/plugins/blog/author.py delete mode 100644 src/plugins/blog/config.py delete mode 100644 src/plugins/blog/plugin.py delete mode 100644 src/plugins/blog/readtime/__init__.py delete mode 100644 src/plugins/blog/readtime/parser.py delete mode 100644 src/plugins/blog/structure/__init__.py delete mode 100644 src/plugins/blog/structure/config.py delete mode 100644 src/plugins/blog/structure/markdown.py delete mode 100644 src/plugins/blog/structure/options.py delete mode 100644 src/plugins/blog/templates/__init__.py delete mode 100644 src/plugins/group/__init__.py delete mode 100644 src/plugins/group/config.py delete mode 100644 src/plugins/group/plugin.py delete mode 100644 src/plugins/info/__init__.py delete mode 100644 src/plugins/info/config.py delete mode 100644 src/plugins/info/plugin.py delete mode 100644 src/plugins/offline/__init__.py delete mode 100644 src/plugins/offline/config.py delete mode 100644 src/plugins/offline/plugin.py delete mode 100644 src/plugins/search/__init__.py delete mode 100644 src/plugins/search/config.py delete mode 100644 src/plugins/search/plugin.py delete mode 100644 src/plugins/social/__init__.py delete mode 100644 src/plugins/social/config.py delete mode 100644 src/plugins/social/plugin.py delete mode 100644 src/plugins/tags/__init__.py delete mode 100644 src/plugins/tags/config.py delete mode 100644 src/plugins/tags/plugin.py delete mode 100644 src/templates/.icons/logo.afdesign delete mode 100644 src/templates/.icons/logo.svg delete mode 100644 src/templates/404.html delete mode 100644 src/templates/__init__.py delete mode 100644 src/templates/assets/images/favicon.png delete mode 100644 src/templates/assets/javascripts/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/document/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/_/.eslintrc delete mode 100644 src/templates/assets/javascripts/browser/element/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/focus/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/offset/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/offset/content/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/offset/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/size/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/size/content/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/size/index.ts delete mode 100644 src/templates/assets/javascripts/browser/element/visibility/index.ts delete mode 100644 src/templates/assets/javascripts/browser/index.ts delete mode 100644 src/templates/assets/javascripts/browser/keyboard/index.ts delete mode 100644 src/templates/assets/javascripts/browser/location/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/location/hash/index.ts delete mode 100644 src/templates/assets/javascripts/browser/location/index.ts delete mode 100644 src/templates/assets/javascripts/browser/media/index.ts delete mode 100644 src/templates/assets/javascripts/browser/request/index.ts delete mode 100644 src/templates/assets/javascripts/browser/script/index.ts delete mode 100644 src/templates/assets/javascripts/browser/toggle/index.ts delete mode 100644 src/templates/assets/javascripts/browser/viewport/_/index.ts delete mode 100644 src/templates/assets/javascripts/browser/viewport/at/index.ts delete mode 100644 src/templates/assets/javascripts/browser/viewport/index.ts delete mode 100644 src/templates/assets/javascripts/browser/viewport/offset/index.ts delete mode 100644 src/templates/assets/javascripts/browser/viewport/size/index.ts delete mode 100644 src/templates/assets/javascripts/browser/worker/index.ts delete mode 100644 src/templates/assets/javascripts/bundle.ts delete mode 100644 src/templates/assets/javascripts/components/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/announce/index.ts delete mode 100644 src/templates/assets/javascripts/components/consent/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/annotation/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/annotation/block/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/annotation/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/annotation/list/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/code/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/code/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/details/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/mermaid/index.css delete mode 100644 src/templates/assets/javascripts/components/content/mermaid/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/table/index.ts delete mode 100644 src/templates/assets/javascripts/components/content/tabs/index.ts delete mode 100644 src/templates/assets/javascripts/components/dialog/index.ts delete mode 100644 src/templates/assets/javascripts/components/header/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/header/index.ts delete mode 100644 src/templates/assets/javascripts/components/header/title/index.ts delete mode 100644 src/templates/assets/javascripts/components/index.ts delete mode 100644 src/templates/assets/javascripts/components/main/index.ts delete mode 100644 src/templates/assets/javascripts/components/palette/index.ts delete mode 100644 src/templates/assets/javascripts/components/progress/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/highlight/.eslintrc delete mode 100644 src/templates/assets/javascripts/components/search/highlight/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/query/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/result/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/share/index.ts delete mode 100644 src/templates/assets/javascripts/components/search/suggest/index.ts delete mode 100644 src/templates/assets/javascripts/components/sidebar/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/facts/_/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/facts/github/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/facts/gitlab/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/facts/index.ts delete mode 100644 src/templates/assets/javascripts/components/source/index.ts delete mode 100644 src/templates/assets/javascripts/components/tabs/index.ts delete mode 100644 src/templates/assets/javascripts/components/toc/index.ts delete mode 100644 src/templates/assets/javascripts/components/top/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/clipboard/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/instant/.eslintrc delete mode 100644 src/templates/assets/javascripts/integrations/instant/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/_/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/config/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/highlighter/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/.eslintrc delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/_/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/extract/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/query/.eslintrc delete mode 100644 src/templates/assets/javascripts/integrations/search/query/_/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/query/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/query/segment/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/query/transform/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/worker/_/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/worker/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc delete mode 100644 src/templates/assets/javascripts/integrations/search/worker/main/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/search/worker/message/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/sitemap/index.ts delete mode 100644 src/templates/assets/javascripts/integrations/version/.eslintrc delete mode 100644 src/templates/assets/javascripts/integrations/version/index.ts delete mode 100644 src/templates/assets/javascripts/patches/indeterminate/index.ts delete mode 100644 src/templates/assets/javascripts/patches/index.ts delete mode 100644 src/templates/assets/javascripts/patches/scrollfix/index.ts delete mode 100644 src/templates/assets/javascripts/patches/scrolllock/index.ts delete mode 100644 src/templates/assets/javascripts/polyfills/index.ts delete mode 100644 src/templates/assets/javascripts/templates/annotation/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/clipboard/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/index.ts delete mode 100644 src/templates/assets/javascripts/templates/search/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/source/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/tabbed/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/table/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/tooltip/index.tsx delete mode 100644 src/templates/assets/javascripts/templates/version/index.tsx delete mode 100644 src/templates/assets/javascripts/utilities/h/.eslintrc delete mode 100644 src/templates/assets/javascripts/utilities/h/index.ts delete mode 100644 src/templates/assets/javascripts/utilities/index.ts delete mode 100644 src/templates/assets/javascripts/utilities/round/index.ts delete mode 100644 src/templates/assets/javascripts/workers/search.ts delete mode 100644 src/templates/assets/stylesheets/_config.scss delete mode 100644 src/templates/assets/stylesheets/main.scss delete mode 100644 src/templates/assets/stylesheets/main/_colors.scss delete mode 100644 src/templates/assets/stylesheets/main/_icons.scss delete mode 100644 src/templates/assets/stylesheets/main/_modifiers.scss delete mode 100644 src/templates/assets/stylesheets/main/_resets.scss delete mode 100644 src/templates/assets/stylesheets/main/_typeset.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_author.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_banner.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_base.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_clipboard.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_consent.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_content.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_dialog.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_feedback.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_footer.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_form.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_header.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_meta.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_nav.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_pagination.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_post.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_progress.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_search.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_select.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_sidebar.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_source.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_status.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_tabs.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_tag.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_tooltip.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_top.scss delete mode 100644 src/templates/assets/stylesheets/main/components/_version.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss delete mode 100644 src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss delete mode 100644 src/templates/assets/stylesheets/main/integrations/_mermaid.scss delete mode 100644 src/templates/assets/stylesheets/palette.scss delete mode 100644 src/templates/assets/stylesheets/palette/_accent.scss delete mode 100644 src/templates/assets/stylesheets/palette/_primary.scss delete mode 100644 src/templates/assets/stylesheets/palette/_scheme.scss delete mode 100644 src/templates/assets/stylesheets/utilities/_break.scss delete mode 100644 src/templates/assets/stylesheets/utilities/_convert.scss delete mode 100644 src/templates/base.html delete mode 100644 src/templates/blog-post.html delete mode 100644 src/templates/blog.html delete mode 100644 src/templates/main.html delete mode 100644 src/templates/mkdocs_theme.yml delete mode 100644 src/templates/partials/actions.html delete mode 100644 src/templates/partials/alternate.html delete mode 100644 src/templates/partials/comments.html delete mode 100644 src/templates/partials/consent.html delete mode 100644 src/templates/partials/content.html delete mode 100644 src/templates/partials/copyright.html delete mode 100644 src/templates/partials/feedback.html delete mode 100644 src/templates/partials/footer.html delete mode 100644 src/templates/partials/header.html delete mode 100644 src/templates/partials/icons.html delete mode 100644 src/templates/partials/integrations/analytics.html delete mode 100644 src/templates/partials/integrations/analytics/google.html delete mode 100644 src/templates/partials/javascripts/announce.html delete mode 100644 src/templates/partials/javascripts/base.html delete mode 100644 src/templates/partials/javascripts/consent.html delete mode 100644 src/templates/partials/javascripts/content.html delete mode 100644 src/templates/partials/javascripts/outdated.html delete mode 100644 src/templates/partials/javascripts/palette.html delete mode 100644 src/templates/partials/language.html delete mode 100644 src/templates/partials/languages/af.html delete mode 100644 src/templates/partials/languages/ar.html delete mode 100644 src/templates/partials/languages/be.html delete mode 100644 src/templates/partials/languages/bg.html delete mode 100644 src/templates/partials/languages/bn.html delete mode 100644 src/templates/partials/languages/ca.html delete mode 100644 src/templates/partials/languages/cs.html delete mode 100644 src/templates/partials/languages/da.html delete mode 100644 src/templates/partials/languages/de.html delete mode 100644 src/templates/partials/languages/el.html delete mode 100644 src/templates/partials/languages/en.html delete mode 100644 src/templates/partials/languages/eo.html delete mode 100644 src/templates/partials/languages/es.html delete mode 100644 src/templates/partials/languages/et.html delete mode 100644 src/templates/partials/languages/eu.html delete mode 100644 src/templates/partials/languages/fa.html delete mode 100644 src/templates/partials/languages/fi.html delete mode 100644 src/templates/partials/languages/fr.html delete mode 100644 src/templates/partials/languages/gl.html delete mode 100644 src/templates/partials/languages/he.html delete mode 100644 src/templates/partials/languages/hi.html delete mode 100644 src/templates/partials/languages/hr.html delete mode 100644 src/templates/partials/languages/hu.html delete mode 100644 src/templates/partials/languages/hy.html delete mode 100644 src/templates/partials/languages/id.html delete mode 100644 src/templates/partials/languages/is.html delete mode 100644 src/templates/partials/languages/it.html delete mode 100644 src/templates/partials/languages/ja.html delete mode 100644 src/templates/partials/languages/ka.html delete mode 100644 src/templates/partials/languages/kn.html delete mode 100644 src/templates/partials/languages/ko.html delete mode 100644 src/templates/partials/languages/ku-IQ.html delete mode 100644 src/templates/partials/languages/lb.html delete mode 100644 src/templates/partials/languages/lt.html delete mode 100644 src/templates/partials/languages/lv.html delete mode 100644 src/templates/partials/languages/mk.html delete mode 100644 src/templates/partials/languages/mn.html delete mode 100644 src/templates/partials/languages/ms.html delete mode 100644 src/templates/partials/languages/my.html delete mode 100644 src/templates/partials/languages/nb.html delete mode 100644 src/templates/partials/languages/nl.html delete mode 100644 src/templates/partials/languages/nn.html delete mode 100644 src/templates/partials/languages/pl.html delete mode 100644 src/templates/partials/languages/pt-BR.html delete mode 100644 src/templates/partials/languages/pt.html delete mode 100644 src/templates/partials/languages/ro.html delete mode 100644 src/templates/partials/languages/ru.html delete mode 100644 src/templates/partials/languages/sa.html delete mode 100644 src/templates/partials/languages/sh.html delete mode 100644 src/templates/partials/languages/si.html delete mode 100644 src/templates/partials/languages/sk.html delete mode 100644 src/templates/partials/languages/sl.html delete mode 100644 src/templates/partials/languages/sr.html delete mode 100644 src/templates/partials/languages/sv.html delete mode 100644 src/templates/partials/languages/te.html delete mode 100644 src/templates/partials/languages/th.html delete mode 100644 src/templates/partials/languages/tl.html delete mode 100644 src/templates/partials/languages/tr.html delete mode 100644 src/templates/partials/languages/uk.html delete mode 100644 src/templates/partials/languages/ur.html delete mode 100644 src/templates/partials/languages/uz.html delete mode 100644 src/templates/partials/languages/vi.html delete mode 100644 src/templates/partials/languages/zh-Hant.html delete mode 100644 src/templates/partials/languages/zh-TW.html delete mode 100644 src/templates/partials/languages/zh.html delete mode 100644 src/templates/partials/logo.html delete mode 100644 src/templates/partials/nav-item.html delete mode 100644 src/templates/partials/nav.html delete mode 100644 src/templates/partials/pagination.html delete mode 100644 src/templates/partials/palette.html delete mode 100644 src/templates/partials/post.html delete mode 100644 src/templates/partials/progress.html delete mode 100644 src/templates/partials/search.html delete mode 100644 src/templates/partials/social.html delete mode 100644 src/templates/partials/source-file.html delete mode 100644 src/templates/partials/source.html delete mode 100644 src/templates/partials/tabs-item.html delete mode 100644 src/templates/partials/tabs.html delete mode 100644 src/templates/partials/tags.html delete mode 100644 src/templates/partials/toc-item.html delete mode 100644 src/templates/partials/toc.html delete mode 100644 src/templates/partials/top.html delete mode 100644 src/templates/redirect.html delete mode 100644 src/test.py (limited to '.github/workflows') diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4787517b..281b8465 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -28,8 +28,7 @@ jobs: mkdocs-material- - run: | cp CHANGELOG.md docs/changelog.md - pip install -r requirements.txt - pip install pdm mike mkdocstrings + pip install -r docs/requirements.txt pdm install - name: Build Documentation diff --git a/.pdm-python b/.pdm-python index 4f78b6a5..71fb9b87 100644 --- a/.pdm-python +++ b/.pdm-python @@ -1 +1 @@ -D:/Git Project/HydroRoll-Team/HydroRollCore/.venv/Scripts/python.EXE \ No newline at end of file +D:/GitProject/HydroRoll-Team/infini/.venv/Scripts/python.exe \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..78aa26fc --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,17 @@ +mkdocs +mkdocs-material +mkdocs-blog-plugin +mkdocs-git-revision-date-localized-plugin>=1.0 +mkdocs-git-authors-plugin +mkdocs-material-extensions>=1.1 +neoteroi-mkdocs +pillow<11 +cairosvg>=2.5 +mkdocs-git-committers-plugin-2>=1.1.1 +lxml +mkdocs-minify-plugin>=0.3 +mkdocs-rss-plugin>=1.1 +mkdocs-redirects>=1.0 +pdm +mike +mkdocstrings \ No newline at end of file diff --git a/docs/src/.gitignore b/docs/src/.gitignore new file mode 100644 index 00000000..3119f231 --- /dev/null +++ b/docs/src/.gitignore @@ -0,0 +1,2 @@ +logs/ +test/ \ No newline at end of file diff --git a/docs/src/__init__.py b/docs/src/__init__.py new file mode 100644 index 00000000..f875ee63 --- /dev/null +++ b/docs/src/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +__version__ = "$md-version$" diff --git a/docs/src/extensions/__init__.py b/docs/src/extensions/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/extensions/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/extensions/emoji.py b/docs/src/extensions/emoji.py new file mode 100644 index 00000000..c8c955cc --- /dev/null +++ b/docs/src/extensions/emoji.py @@ -0,0 +1,98 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import codecs +import functools +import material +import os + +from glob import iglob +from inspect import getfile +from markdown import Markdown +from pymdownx import emoji, twemoji_db +from xml.etree.ElementTree import Element + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + +# Create twemoji index +def twemoji(options: object, md: Markdown): + paths = options.get("custom_icons", [])[:] + return _load_twemoji_index(tuple(paths)) + +# Create emoji or icon +def to_svg( + index: str, shortname: str, alias: str, uc: str | None, alt: str, + title: str, category: str, options: object, md: Markdown +): + if not uc: + icons = md.inlinePatterns["emoji"].emoji_index["emoji"] + + # Create and return element to host icon + el = Element("span", { "class": options.get("classes", index) }) + el.text = md.htmlStash.store(_load(icons[shortname]["path"])) + return el + + # Delegate to `pymdownx.emoji` extension + return emoji.to_svg( + index, shortname, alias, uc, alt, title, category, options, md + ) + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + +# Load icon +@functools.lru_cache(maxsize = None) +def _load(file: str): + with codecs.open(file, encoding = "utf-8") as f: + return f.read() + +# Load twemoji index and add icons +@functools.lru_cache(maxsize = None) +def _load_twemoji_index(paths): + index = { + "name": "twemoji", + "emoji": twemoji_db.emoji, + "aliases": twemoji_db.aliases + } + + # Compute path to theme root and traverse all icon directories + root = os.path.dirname(getfile(material)) + root = os.path.join(root, "templates", ".icons") + for path in [*paths, root]: + base = os.path.normpath(path) + + # Index icons provided by the theme and via custom icons + glob = os.path.join(base, "**", "*.svg") + glob = iglob(os.path.normpath(glob), recursive = True) + for file in glob: + icon = file[len(base) + 1:-4].replace(os.path.sep, "-") + + # Add icon to index + name = f":{icon}:" + if not any(name in index[key] for key in ["emoji", "aliases"]): + index["emoji"][name] = { "name": name, "path": file } + + # Return index + return index diff --git a/docs/src/overrides/assets/javascripts/components/_/index.ts b/docs/src/overrides/assets/javascripts/components/_/index.ts new file mode 100644 index 00000000..3cb4c18e --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/_/index.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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( + 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( + type: T, node: ParentNode = document +): ComponentTypeMap[T][] { + return getElements(`[data-mdx-component=${type}]`, node) +} diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts new file mode 100644 index 00000000..f509a6f9 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/_/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* 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> { + const config = configuration() + const index$ = requestJSON( + new URL("assets/javascripts/iconsearch_index.json", config.base) + ) + + /* Retrieve query and result components */ + const query = getComponentElement("iconsearch-query", el) + const result = getComponentElement("iconsearch-result", el) + + /* Create and return component */ + const query$ = mountIconSearchQuery(query) + const result$ = mountIconSearchResult(result, { index$, query$ }) + return merge(query$, result$) +} diff --git a/docs/src/overrides/assets/javascripts/components/iconsearch/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/index.ts new file mode 100644 index 00000000..9d856774 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts new file mode 100644 index 00000000..03a3daad --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/query/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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> { + + /* 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/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts b/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts new file mode 100644 index 00000000..2b9d97fb --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/iconsearch/result/index.ts @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* Search index observable */ + query$: Observable /* Search query observable */ +} + +/** + * Mount options + */ +interface MountOptions { + index$: Observable /* Search index observable */ + query$: Observable /* 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 { + 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(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(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> { + const push$ = new Subject() + 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/docs/src/overrides/assets/javascripts/components/index.ts b/docs/src/overrides/assets/javascripts/components/index.ts new file mode 100644 index 00000000..ec6c9dce --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts b/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts new file mode 100644 index 00000000..711f423a --- /dev/null +++ b/docs/src/overrides/assets/javascripts/components/sponsorship/index.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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> { + const sponsorship$ = requestJSON( + "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/docs/src/overrides/assets/javascripts/custom.ts b/docs/src/overrides/assets/javascripts/custom.ts new file mode 100644 index 00000000..7c3c3847 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/custom.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts b/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts new file mode 100644 index 00000000..658add2a --- /dev/null +++ b/docs/src/overrides/assets/javascripts/integrations/analytics/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/integrations/index.ts b/docs/src/overrides/assets/javascripts/integrations/index.ts new file mode 100644 index 00000000..9179f2a2 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/integrations/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx b/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx new file mode 100644 index 00000000..13cafa6d --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/iconsearch/index.tsx @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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: "", + tagClose: "" + } + }) +} + +/* ---------------------------------------------------------------------------- + * 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 ( +
  • + + + + +
  • + ) +} diff --git a/docs/src/overrides/assets/javascripts/templates/index.ts b/docs/src/overrides/assets/javascripts/templates/index.ts new file mode 100644 index 00000000..02376b3d --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx b/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx new file mode 100644 index 00000000..7891c2e0 --- /dev/null +++ b/docs/src/overrides/assets/javascripts/templates/sponsorship/index.tsx @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( + + + + ) +} + +/** + * Render private sponsor + * + * @param count - Number of private sponsors + * + * @returns Element + */ +export function renderPrivateSponsor( + count: number +): HTMLElement { + return ( + + +{count} + + ) +} diff --git a/docs/src/overrides/assets/stylesheets/custom.scss b/docs/src/overrides/assets/stylesheets/custom.scss new file mode 100644 index 00000000..8235e7d0 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom.scss @@ -0,0 +1,44 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Dependencies +// ---------------------------------------------------------------------------- + +@import "material-color"; +@import "material-shadows"; + +// ---------------------------------------------------------------------------- +// Local imports +// ---------------------------------------------------------------------------- + +@import "utilities/break"; +@import "utilities/convert"; + +@import "config"; + +@import "custom/typeset"; + +@import "custom/layout/banner"; +@import "custom/layout/hero"; +@import "custom/layout/iconsearch"; +@import "custom/layout/sponsorship"; diff --git a/docs/src/overrides/assets/stylesheets/custom/_typeset.scss b/docs/src/overrides/assets/stylesheets/custom/_typeset.scss new file mode 100644 index 00000000..bef30073 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/_typeset.scss @@ -0,0 +1,294 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Pumping heart animation +@keyframes heart { + 0%, + 40%, + 80%, + 100% { + transform: scale(1); + } + + 20%, + 60% { + transform: scale(1.15); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Twitter icon + .twitter { + color: #00acee; + } + + // Mastodon icon - it's not the exact brand color, because that doesn't work + // well on dark backgrounds, so we lightened it up a bit. + .mastodon { + color: #897ff8; + } + + // Insiders video + .mdx-video { + width: auto; + + // Insiders video container + &__inner { + position: relative; + width: 100%; + height: 0; + padding-bottom: 56.138%; + } + + // Insiders video iframe + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + border: none; + } + } + + // Pumping heart + .mdx-heart { + animation: heart 1000ms infinite; + } + + // Insiders color (for links, etc.) // remove + .mdx-insiders { + color: $clr-pink-500; + } + + // BETA ##################################################################### + + // Badge + .mdx-badge { + font-size: 0.85em; + + // Badge with heart + &--heart { + --md-typeset-a-color: hsla(#{hex2hsl($clr-pink-500)}, 1); + --md-accent-fg-color: hsla(#{hex2hsl($clr-pink-a200)}, 1); + --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-pink-500)}, 0.1); + + // Animate icon + .twemoji { + animation: heart 1000ms infinite; + } + } + + // Badge moved to the right + &--right { + float: right; + margin-left: 0.35em; + } + + // Badge icon + &__icon { + padding: px2rem(4px); + background: var(--md-accent-fg-color--transparent); + border-start-start-radius: px2rem(2px); + border-end-start-radius: px2rem(2px); + + // If icon is alone, round corners + &:last-child { + border-radius: px2rem(2px); + } + } + + // Badge text + &__text { + padding: px2rem(4px) px2rem(6px); + border-start-end-radius: px2rem(2px); + border-end-end-radius: px2rem(2px); + box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent); + } + } + + // BETA ##################################################################### + + // Switch buttons + .mdx-switch button { + cursor: pointer; + transition: opacity 250ms; + + // Button on focus/hover + &:is(:focus, :hover) { + opacity: 0.75; + } + + // Code block + > code { + display: block; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + } + } + + // Two-column layout + .mdx-columns { + + // Column + ol, + ul { + columns: 2; + + // [mobile portrait -]: Reset columns on mobile + @include break-to-device(mobile portrait) { + columns: initial; + } + } + + // Column item + li { + break-inside: avoid; + } + } + + // Language list + .mdx-flags { + margin: 2em auto; + + // Language list + ol { + list-style: none; + + // Language list item + li { + margin-bottom: 1em; + } + } + + // Language item + &__item { + display: flex; + gap: px2rem(12px); + } + + // Language content + &__content { + display: flex; + flex: 1; + flex-direction: column; + + // Language name + span { + display: inline-flex; + align-items: baseline; + justify-content: space-between; + } + + // Language link + > span:nth-child(2) { + font-size: 80%; + } + + // Language code + code { + float: right; + } + } + } + + // Social card + .mdx-social { + position: relative; + height: min(#{px2rem(540px)}, 80vw); + + // Social card image on hover + &:hover .mdx-social__image { + background-color: rgba(228, 228, 228, 0.05); + } + + // Social card layer + &__layer { + position: absolute; + margin-top: px2rem(80px); + transition: 250ms cubic-bezier(0.7, 0, 0.3, 1); + transform-style: preserve-3d; + + // Social card layer on hover + &:hover { + + // Social card label + .mdx-social__label { + opacity: 1; + } + + // Social card image + .mdx-social__image { + background-color: rgba(127, 127, 127, 0.99); + } + + // Hide top layers + ~ .mdx-social__layer { + opacity: 0; + } + } + } + + // Social card image + &__image { + box-shadow: + px2rem(-5px) px2rem(5px) px2rem(10px) + rgba(0, 0, 0, 0.05); + transition: all 250ms; + transform: rotate(-40deg) skew(15deg, 15deg) scale(0.7); + + // Actual image + img { + display: block; + } + } + + // Social card label + &__label { + position: absolute; + display: block; + padding: px2rem(4px) px2rem(8px); + color: var(--md-default-bg-color); + background-color: var(--md-default-fg-color--light); + opacity: 0; + transition: all 250ms; + } + + // Transform on hover + @for $i from 6 through 0 { + &:hover .mdx-social__layer:nth-child(#{$i}) { + transform: translateY(#{($i - 3) * -10}px); + } + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss new file mode 100644 index 00000000..b67d7fff --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_banner.scss @@ -0,0 +1,66 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Banner for announcements and warnings +.md-banner { + color: var(--md-footer-fg-color--lighter); + + // Don't wrap name of blog article + strong { + color: var(--md-footer-fg-color); + white-space: nowrap; + } + + a { + color: var(--md-footer-fg-color); + + &:focus, + &:hover { + color: currentcolor; + + .twemoji { + background-color: var(--md-footer-fg-color); + box-shadow: none; + } + } + } + + .twemoji { + display: inline-block; + width: px2rem(24px); + height: px2rem(24px); + padding: px2rem(5px); + vertical-align: bottom; + border-radius: 100%; + box-shadow: 0 0 0 px2rem(1px) currentcolor inset; + transition: all 250ms; + + svg { + display: block; + max-height: initial; + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss new file mode 100644 index 00000000..428cd37e --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_hero.scss @@ -0,0 +1,123 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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,") 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,") no-repeat bottom, + linear-gradient( + to bottom, + var(--md-primary-fg-color), + hsla(230, 15%, 25%, 1) 99%, + var(--md-default-bg-color) 99% + ); + } +} + +// Landing page hero +.mdx-hero { + margin: 0 px2rem(16px); + color: var(--md-primary-bg-color); + + // Hero headline + h1 { + margin-bottom: px2rem(20px); + font-weight: 700; + color: currentcolor; + + // [mobile portrait -]: Larger hero headline + @include break-to-device(mobile portrait) { + font-size: px2rem(28px); + } + } + + // Hero content + &__content { + padding-bottom: px2rem(120px); + } + + // [tablet landscape +]: Columnar display + @include break-from-device(tablet landscape) { + display: flex; + align-items: stretch; + + // Adjust spacing and set dimensions + &__content { + max-width: px2rem(380px); + padding-bottom: 14vw; + margin-top: px2rem(70px); + } + + // Hero image + &__image { + order: 1; + width: px2rem(760px); + transform: translateX(#{px2rem(80px)}); + } + } + + // [screen +]: Columnar display and adjusted spacing + @include break-from-device(screen) { + + // Hero image + &__image { + transform: translateX(#{px2rem(160px)}); + } + } + + // Button + .md-button { + margin-top: px2rem(10px); + margin-right: px2rem(10px); + color: var(--md-primary-bg-color); + + // Button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + } + + // Primary button + &--primary { + color: hsla(280, 37%, 48%, 1); + background-color: var(--md-primary-bg-color); + border-color: var(--md-primary-bg-color); + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss new file mode 100644 index 00000000..651c4135 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss @@ -0,0 +1,136 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Icon search + .mdx-iconsearch { + position: relative; + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z1); + transition: box-shadow 125ms; + + // Icon search on focus/hover + &:is(:focus-within, :hover) { + box-shadow: var(--md-shadow-z2); + } + + // Icon search input + .md-input { + background: var(--md-default-bg-color); + box-shadow: none; + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] & { + background: var(--md-code-bg-color); + } + } + } + + // Icon search result + .mdx-iconsearch-result { + max-height: 50vh; + overflow-y: auto; + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + touch-action: pan-y; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Icon search result inside tooltip + .md-tooltip & { + max-height: px2rem(205px); + } + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + + // Icon search result metadata + &__meta { + position: absolute; + top: px2rem(8px); + right: px2rem(12px); + font-size: px2rem(12.8px); + color: var(--md-default-fg-color--lighter); + } + + // Icon search result list + &__list { + padding: 0; + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + list-style: none; + } + + // Icon search result item + &__item { + padding: px2rem(4px) px2rem(12px); + margin: 0; + // Hack: necessary because of increased specificity due to the PostCSS + // plugin which prefixes this with `[dir=...]` selectors. + margin-inline-start: 0; + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // Omit border on last child + &:last-child { + border-bottom: none; + } + + // Item content + > * { + margin-right: px2rem(12px); + } + + // Set icon dimensions to fit + img { + width: px2rem(18px); + height: px2rem(18px); + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] &[src*="squidfunk"] { + filter: invert(1); /* stylelint-disable-line */ + } + } + } + } +} diff --git a/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss b/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss new file mode 100644 index 00000000..e2b16570 --- /dev/null +++ b/docs/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss @@ -0,0 +1,128 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Premium sponsors + .mdx-premium { + + // Paragraphs + p { + margin: 2em 0; + text-align: center; + } + + // Premium sponsor image + img { + height: px2rem(65px); + } + + // Premium sponsor list + p:last-child { + display: flex; + flex-wrap: wrap; + justify-content: center; + + // Premium sponsor link + > a { + display: block; + flex-shrink: 0; + } + } + } + + // Sponsorship + .mdx-sponsorship { + + // Sponsorship list + &__list { + margin: 2em 0; + + // Clearfix, because we can't use overflow: auto + &::after { + display: block; + clear: both; + content: ""; + } + } + + // Sponsorship item + &__item { + display: block; + float: inline-start; + width: px2rem(32px); + height: px2rem(32px); + margin: px2rem(4px); + overflow: hidden; + border-radius: 100%; + transition: + color 125ms, + transform 125ms; + transform: scale(1); + + // Sponsor item on focus/hover + &:is(:focus, :hover) { + transform: scale(1.1); + + // Sponsor avatar + img { + filter: grayscale(0%); + } + } + + // Private sponsor + &--private { + font-size: px2rem(12px); + font-weight: 700; + line-height: px2rem(32px); + color: var(--md-default-fg-color--lighter); + text-align: center; + background: var(--md-default-fg-color--lightest); + } + + // Sponsor avatar + img { + display: block; + width: 100%; + height: auto; + filter: grayscale(100%) opacity(75%); + transition: filter 125ms; + } + } + } + + // Sponsorship button + .mdx-sponsorship-button { + font-weight: 400; + } + + // Sponsorship count and total + .mdx-sponsorship-count, + .mdx-sponsorship-total { + font-weight: 700; + } +} diff --git a/docs/src/overrides/home.html b/docs/src/overrides/home.html new file mode 100644 index 00000000..3f54ca82 --- /dev/null +++ b/docs/src/overrides/home.html @@ -0,0 +1,106 @@ + + +{% extends "main.html" %} + + +{% block tabs %} + {{ super() }} + + + + + +
    +
    +
    + + +
    + +
    + + +
    +

    Technical documentation that just works

    +

    {{ config.site_description }}. Set up in 5 minutes.

    + + Quick start + + + Get Insiders + +
    +
    +
    +
    +{% endblock %} + + +{% block content %}{% endblock %} + + +{% block footer %}{% endblock %} diff --git a/docs/src/overrides/hooks/shortcodes.py b/docs/src/overrides/hooks/shortcodes.py new file mode 100644 index 00000000..5b02e3cf --- /dev/null +++ b/docs/src/overrides/hooks/shortcodes.py @@ -0,0 +1,283 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import posixpath +import re + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import File, Files +from mkdocs.structure.pages import Page +from re import Match + +# ----------------------------------------------------------------------------- +# Hooks +# ----------------------------------------------------------------------------- + +# @todo +def on_page_markdown( + markdown: str, *, page: Page, config: MkDocsConfig, files: Files +): + + # Replace callback + def replace(match: Match): + type, args = match.groups() + args = args.strip() + if type == "version": + if args.startswith("insiders-"): + return _badge_for_version_insiders(args, page, files) + else: + return _badge_for_version(args, page, files) + elif type == "sponsors": return _badge_for_sponsors(page, files) + elif type == "flag": return flag(args, page, files) + elif type == "option": return option(args) + elif type == "setting": return setting(args) + elif type == "feature": return _badge_for_feature(args, page, files) + elif type == "plugin": return _badge_for_plugin(args, page, files) + elif type == "extension": return _badge_for_extension(args, page, files) + elif type == "utility": return _badge_for_utility(args, page, files) + elif type == "example": return _badge_for_example(args, page, files) + elif type == "default": + if args == "none": return _badge_for_default_none(page, files) + elif args == "computed": return _badge_for_default_computed(page, files) + else: return _badge_for_default(args, page, files) + + # Otherwise, raise an error + raise RuntimeError(f"Unknown shortcode: {type}") + + # Find and replace all external asset URLs in current page + return re.sub( + r"", + replace, markdown, flags = re.I | re.M + ) + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + +# Create a flag of a specific type +def flag(args: str, page: Page, files: Files): + type, *_ = args.split(" ", 1) + if type == "experimental": return _badge_for_experimental(page, files) + elif type == "required": return _badge_for_required(page, files) + elif type == "customization": return _badge_for_customization(page, files) + elif type == "metadata": return _badge_for_metadata(page, files) + elif type == "multiple": return _badge_for_multiple(page, files) + raise RuntimeError(f"Unknown type: {type}") + +# Create a linkable option +def option(type: str): + _, *_, name = re.split(r"[.:]", type) + return f"[`{name}`](#+{type}){{ #+{type} }}\n\n" + +# Create a linkable setting - @todo append them to the bottom of the page +def setting(type: str): + _, *_, name = re.split(r"[.*]", type) + return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n" + +# ----------------------------------------------------------------------------- + +# Resolve path of file relative to given page - the posixpath always includes +# one additional level of `..` which we need to remove +def _resolve_path(path: str, page: Page, files: Files): + path, anchor, *_ = f"{path}#".split("#") + path = _resolve(files.get_file_from_path(path), page) + return "#".join([path, anchor]) if anchor else path + +# Resolve path of file relative to given page - the posixpath always includes +# one additional level of `..` which we need to remove +def _resolve(file: File, page: Page): + path = posixpath.relpath(file.src_uri, page.file.src_uri) + return posixpath.sep.join(path.split(posixpath.sep)[1:]) + +# ----------------------------------------------------------------------------- + +# Create badge +def _badge(icon: str, text: str = "", type: str = ""): + classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge" + return "".join([ + f"", + *([f"{icon}"] if icon else []), + *([f"{text}"] if text else []), + f"", + ]) + +# Create sponsors badge +def _badge_for_sponsors(page: Page, files: Files): + icon = "material-heart" + href = _resolve_path("insiders/index.md", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Sponsors only')", + type = "heart" + ) + +# Create badge for version +def _badge_for_version(text: str, page: Page, files: Files): + spec = text + path = f"changelog/index.md#{spec}" + + # Return badge + icon = "material-tag-outline" + href = _resolve_path("conventions.md#version", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Minimum version')", + text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" + ) + +# Create badge for version of Insiders +def _badge_for_version_insiders(text: str, page: Page, files: Files): + spec = text.replace("insiders-", "") + path = f"insiders/changelog/index.md#{spec}" + + # Return badge + icon = "material-tag-heart-outline" + href = _resolve_path("conventions.md#version-insiders", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Minimum version')", + text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" + ) + +# Create badge for feature +def _badge_for_feature(text: str, page: Page, files: Files): + icon = "material-toggle-switch" + href = _resolve_path("conventions.md#feature", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Optional feature')", + text = text + ) + +# Create badge for plugin +def _badge_for_plugin(text: str, page: Page, files: Files): + icon = "material-floppy" + href = _resolve_path("conventions.md#plugin", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Plugin')", + text = text + ) + +# Create badge for extension +def _badge_for_extension(text: str, page: Page, files: Files): + icon = "material-language-markdown" + href = _resolve_path("conventions.md#extension", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Markdown extension')", + text = text + ) + +# Create badge for utility +def _badge_for_utility(text: str, page: Page, files: Files): + icon = "material-package-variant" + href = _resolve_path("conventions.md#utility", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Third-party utility')", + text = text + ) + +# Create badge for example +def _badge_for_example(text: str, page: Page, files: Files): + return "\n".join([ + _badge_for_example_download(text, page, files), + _badge_for_example_view(text, page, files) + ]) + +# Create badge for example view +def _badge_for_example_view(text: str, page: Page, files: Files): + icon = "material-folder-eye" + href = f"https://mkdocs-material.github.io/examples/{text}/" + return _badge( + icon = f"[:{icon}:]({href} 'View example')", + type = "right" + ) + +# Create badge for example download +def _badge_for_example_download(text: str, page: Page, files: Files): + icon = "material-folder-download" + href = f"https://mkdocs-material.github.io/examples/{text}.zip" + return _badge( + icon = f"[:{icon}:]({href} 'Download example')", + text = f"[`.zip`]({href})", + type = "right" + ) + +# Create badge for default value +def _badge_for_default(text: str, page: Page, files: Files): + icon = "material-water" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value')", + text = text + ) + +# Create badge for empty default value +def _badge_for_default_none(page: Page, files: Files): + icon = "material-water-outline" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value is empty')" + ) + +# Create badge for computed default value +def _badge_for_default_computed(page: Page, files: Files): + icon = "material-water-check" + href = _resolve_path("conventions.md#default", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Default value is computed')" + ) + +# Create badge for metadata property flag +def _badge_for_metadata(page: Page, files: Files): + icon = "material-list-box-outline" + href = _resolve_path("conventions.md#metadata", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Metadata property')" + ) + +# Create badge for required value flag +def _badge_for_required(page: Page, files: Files): + icon = "material-alert" + href = _resolve_path("conventions.md#required", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Required value')" + ) + +# Create badge for customization flag +def _badge_for_customization(page: Page, files: Files): + icon = "material-brush-variant" + href = _resolve_path("conventions.md#customization", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Customization')" + ) + +# Create badge for multiple instance flag +def _badge_for_multiple(page: Page, files: Files): + icon = "material-inbox-multiple" + href = _resolve_path("conventions.md#multiple-instances", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Multiple instances')" + ) + +# Create badge for experimental flag +def _badge_for_experimental(page: Page, files: Files): + icon = "material-flask-outline" + href = _resolve_path("conventions.md#experimental", page, files) + return _badge( + icon = f"[:{icon}:]({href} 'Experimental')" + ) diff --git a/docs/src/overrides/hooks/translations.html b/docs/src/overrides/hooks/translations.html new file mode 100644 index 00000000..ab41c77d --- /dev/null +++ b/docs/src/overrides/hooks/translations.html @@ -0,0 +1,54 @@ + + + +{% macro render_language(language) %} +
    + :flag_{{ language.flag }}:{ .lg .middle } + + + {{ language.name }} + {{ language.code }} + + {% if language.miss %} + + + {{ language.miss | length }} translations missing + + + {% else %} + Complete + {% endif %} + +
    +{% endmacro %} + + +{% macro render(translations, start = 1) %} +
    +
      + {% for language in translations %} +
    1. {{ render_language(language) }}
    2. + {% endfor %} +
    +
    +{% endmacro %} diff --git a/docs/src/overrides/hooks/translations.py b/docs/src/overrides/hooks/translations.py new file mode 100644 index 00000000..661fd18e --- /dev/null +++ b/docs/src/overrides/hooks/translations.py @@ -0,0 +1,193 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import os +import re + +from glob import iglob +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.pages import Page +from urllib.parse import urlencode, urlparse + +# ----------------------------------------------------------------------------- +# Hooks +# ----------------------------------------------------------------------------- + +# Determine missing translations and render language overview in the setup +# guide, including links to provide missing translations. +def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): + issue_url = "https://github.com/squidfunk/mkdocs-material/issues/new" + if page.file.src_uri != "setup/changing-the-language.md": + return + + # Collect all existing languages + names: dict[str, str] = {} + known: dict[str, dict[str, str]] = {} + for path in iglob("src/templates/partials/languages/*.html"): + with open(path, "r", encoding = "utf-8") as f: + data = f.read() + + # Extract language code and name + name, = re.findall(r"", data) + code, _ = os.path.splitext(os.path.basename(path)) + + # Map names and available translations + names[code] = name + known[code] = dict(re.findall( + r"^ \"([^\"]+)\": \"([^\"]*)\"(?:,|$)?", data, + re.MULTILINE + )) + + # Remove technical stuff + for key in [ + "direction", + "search.config.pipeline", + "search.config.lang", + "search.config.separator" + ]: + if key in known[code]: + del known[code][key] + + # Traverse all languages and compute missing translations + languages = [] + reference = set(known["en"]) + for code, name in names.items(): + miss = reference - set(known[code]) + + # Check each translations + translations: list[str] = [] + for key, value in known["en"].items(): + if key in known[code]: + translations.append( + f" \"{key}\": \"{known[code][key]}\"" + ) + else: + translations.append( + f" \"{key}\": \"{value} ⬅️\"" + ) + + # Assemble GitHub issue URL + link = urlparse(issue_url) + link = link._replace(query = urlencode({ + "template": "04-add-translations.yml", + "title": f"Update {name} translations", + "translations": "\n".join([ + "{% macro t(key) %}{{ {", + ",\n".join(translations), + "}[key] }}{% endmacro %}" + ]), + "country-flag": f":flag_{countries[code]}:" + })) + + # Add translation + languages.append({ + "flag": countries[code], + "code": code, + "name": name, + "link": link.geturl(), + "miss": miss + }) + + # Load template and render translations + env = config.theme.get_env() + template = env.get_template( "hooks/translations.html") + translations = template.module.render( + sorted(languages, key = lambda language: language["name"]) + ) + + # Replace translation marker + return markdown.replace( + "", "\n".join( + [line.lstrip() for line in translations.split("\n") + ] + )) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Map ISO 639-1 (languages) to ISO 3166 (countries) +countries = dict({ + "af": "za", + "ar": "ae", + "be": "by", + "bg": "bg", + "bn": "bd", + "ca": "es", + "cs": "cz", + "da": "dk", + "de": "de", + "el": "gr", + "en": "us", + "eo": "eu", + "es": "es", + "et": "ee", + "eu": "es", + "fa": "ir", + "fi": "fi", + "fr": "fr", + "gl": "es", + "he": "il", + "hi": "in", + "hr": "hr", + "hu": "hu", + "hy": "am", + "id": "id", + "is": "is", + "it": "it", + "ja": "jp", + "ka": "ge", + "kn": "in", + "ko": "kr", + "ku-IQ": "iq", + "lb": "lu", + "lt": "lt", + "lv": "lv", + "mk": "mk", + "mn": "mn", + "ms": "my", + "my": "mm", + "nb": "no", + "nl": "nl", + "nn": "no", + "pl": "pl", + "pt-BR": "br", + "pt": "pt", + "ro": "ro", + "ru": "ru", + "sa": "in", + "sh": "rs", + "si": "lk", + "sk": "sk", + "sl": "si", + "sr": "rs", + "sv": "se", + "te": "in", + "th": "th", + "tl": "ph", + "tr": "tr", + "uk": "ua", + "ur": "pk", + "uz": "uz", + "vi": "vn", + "zh": "cn", + "zh-Hant": "cn", + "zh-TW": "tw" +}) diff --git a/docs/src/overrides/main.html b/docs/src/overrides/main.html new file mode 100644 index 00000000..39b68b5a --- /dev/null +++ b/docs/src/overrides/main.html @@ -0,0 +1,59 @@ + + +{% extends "base.html" %} + + +{% block extrahead %} + + + +{% endblock %} + + +{% block announce %} + For updates follow @squidfunk on + + + {% include ".icons/fontawesome/brands/mastodon.svg" %} + + Fosstodon + + and + + + Twitter + +{% endblock %} + + +{% block scripts %} + {{ super() }} + + + +{% endblock %} diff --git a/docs/src/plugins/__init__.py b/docs/src/plugins/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/blog/__init__.py b/docs/src/plugins/blog/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/blog/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/blog/author.py b/docs/src/plugins/blog/author.py new file mode 100644 index 00000000..1dcfc2de --- /dev/null +++ b/docs/src/plugins/blog/author.py @@ -0,0 +1,38 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.base import Config +from mkdocs.config.config_options import DictOfItems, SubConfig, Type + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Author +class Author(Config): + name = Type(str) + description = Type(str) + avatar = Type(str) + +# ----------------------------------------------------------------------------- + +# Authors +class Authors(Config): + authors = DictOfItems(SubConfig(Author), default = {}) diff --git a/docs/src/plugins/blog/config.py b/docs/src/plugins/blog/config.py new file mode 100644 index 00000000..c7a85095 --- /dev/null +++ b/docs/src/plugins/blog/config.py @@ -0,0 +1,88 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from functools import partial +from markdown.extensions.toc import slugify +from mkdocs.config.config_options import Choice, Deprecated, Optional, Type +from mkdocs.config.base import Config + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Blog plugin configuration +class BlogConfig(Config): + enabled = Type(bool, default = True) + + # Settings for blog + blog_dir = Type(str, default = "blog") + blog_toc = Type(bool, default = False) + + # Settings for posts + post_dir = Type(str, default = "{blog}/posts") + post_date_format = Type(str, default = "long") + post_url_date_format = Type(str, default = "yyyy/MM/dd") + post_url_format = Type(str, default = "{date}/{slug}") + post_url_max_categories = Type(int, default = 1) + post_slugify = Type((type(slugify), partial), default = slugify) + post_slugify_separator = Type(str, default = "-") + post_excerpt = Choice(["optional", "required"], default = "optional") + post_excerpt_max_authors = Type(int, default = 1) + post_excerpt_max_categories = Type(int, default = 5) + post_excerpt_separator = Type(str, default = "") + post_readtime = Type(bool, default = True) + post_readtime_words_per_minute = Type(int, default = 265) + + # Settings for archive + archive = Type(bool, default = True) + archive_name = Type(str, default = "blog.archive") + archive_date_format = Type(str, default = "yyyy") + archive_url_date_format = Type(str, default = "yyyy") + archive_url_format = Type(str, default = "archive/{date}") + archive_toc = Optional(Type(bool)) + + # Settings for categories + categories = Type(bool, default = True) + categories_name = Type(str, default = "blog.categories") + categories_url_format = Type(str, default = "category/{slug}") + categories_slugify = Type((type(slugify), partial), default = slugify) + categories_slugify_separator = Type(str, default = "-") + categories_allowed = Type(list, default = []) + categories_toc = Optional(Type(bool)) + + # Settings for pagination + pagination = Type(bool, default = True) + pagination_per_page = Type(int, default = 10) + pagination_url_format = Type(str, default = "page/{page}") + pagination_format = Type(str, default = "~2~") + pagination_if_single_page = Type(bool, default = False) + pagination_keep_content = Type(bool, default = False) + + # Settings for authors + authors = Type(bool, default = True) + authors_file = Type(str, default = "{blog}/.authors.yml") + + # Settings for drafts + draft = Type(bool, default = False) + draft_on_serve = Type(bool, default = True) + draft_if_future_date = Type(bool, default = False) + + # Deprecated settings + pagination_template = Deprecated(moved_to = "pagination_format") diff --git a/docs/src/plugins/blog/plugin.py b/docs/src/plugins/blog/plugin.py new file mode 100644 index 00000000..375b8cfe --- /dev/null +++ b/docs/src/plugins/blog/plugin.py @@ -0,0 +1,884 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import logging +import os +import posixpath +import yaml + +from babel.dates import format_date +from datetime import datetime +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.exceptions import PluginError +from mkdocs.plugins import BasePlugin, event_priority +from mkdocs.structure import StructureItem +from mkdocs.structure.files import File, Files, InclusionLevel +from mkdocs.structure.nav import Navigation, Section +from mkdocs.structure.pages import Page +from mkdocs.utils import copy_file, get_relative_url +from paginate import Page as Pagination +from shutil import rmtree +from tempfile import mkdtemp +from yaml import SafeLoader + +from .author import Authors +from .config import BlogConfig +from .readtime import readtime +from .structure import Archive, Category, Excerpt, Post, View +from .templates import url_filter + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Blog plugin +class BlogPlugin(BasePlugin[BlogConfig]): + supports_multiple_instances = True + + # Initialize plugin + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Initialize incremental builds + self.is_serve = False + self.is_dirty = False + + # Initialize temporary directory + self.temp_dir = mkdtemp() + + # Determine whether we're serving the site + def on_startup(self, *, command, dirty): + self.is_serve = command == "serve" + self.is_dirty = dirty + + # Initialize authors and set defaults + def on_config(self, config): + if not self.config.enabled: + return + + # Initialize entrypoint + self.blog: View + + # Initialize and resolve authors, if enabled + if self.config.authors: + self.authors = self._resolve_authors(config) + + # Initialize table of contents settings + if not isinstance(self.config.archive_toc, bool): + self.config.archive_toc = self.config.blog_toc + if not isinstance(self.config.categories_toc, bool): + self.config.categories_toc = self.config.blog_toc + + # By default, drafts are rendered when the documentation is served, + # but not when it is built, for a better user experience + if self.is_serve and self.config.draft_on_serve: + self.config.draft = True + + # Resolve and load posts and generate views (run later) - we want to allow + # other plugins to add generated posts or views, so we run this plugin as + # late as possible. We also need to remove the posts from the navigation + # before navigation is constructed, as the entrypoint should be considered + # to be the active page for each post. The URLs of posts are computed before + # Markdown processing, so that when linking to and from posts, behavior is + # exactly the same as with regular documentation pages. We create all pages + # related to posts as part of this plugin, so we control the entire process. + @event_priority(-50) + def on_files(self, files, *, config): + if not self.config.enabled: + return + + # Resolve path to entrypoint and site directory + root = posixpath.normpath(self.config.blog_dir) + site = config.site_dir + + # Compute path to posts directory + path = self.config.post_dir.format(blog = root) + path = posixpath.normpath(path) + + # Adjust destination paths for media files + for file in files.media_files(): + if not file.src_uri.startswith(path): + continue + + # We need to adjust destination paths for assets to remove the + # purely functional posts directory prefix when building + file.dest_uri = file.dest_uri.replace(path, root) + file.abs_dest_path = os.path.join(site, file.dest_path) + file.url = file.url.replace(path, root) + + # Resolve entrypoint and posts sorted by descending date - if the posts + # directory or entrypoint do not exist, they are automatically created + self.blog = self._resolve(files, config) + self.blog.posts = sorted( + self._resolve_posts(files, config), + key = lambda post: post.config.date.created, + reverse = True + ) + + # Generate views for archive + if self.config.archive: + views = self._generate_archive(config, files) + self.blog.views.extend(views) + + # Generate views for categories + if self.config.categories: + views = self._generate_categories(config, files) + self.blog.views.extend(views) + + # Generate pages for views + if self.config.pagination: + for view in self._resolve_views(self.blog): + for page in self._generate_pages(view, config, files): + page.file.inclusion = InclusionLevel.EXCLUDED + view.pages.append(page) + + # Ensure that entrypoint is always included in navigation + self.blog.file.inclusion = InclusionLevel.INCLUDED + + # Attach posts and views to navigation (run later) - again, we allow other + # plugins to alter the navigation before we start to attach posts and views + # generated by this plugin at the correct locations in the navigation. Also, + # we make sure to correct links to the parent and siblings of each page. + @event_priority(-50) + def on_nav(self, nav, *, config, files): + if not self.config.enabled: + return + + # If we're not building a standalone blog, the entrypoint will always + # have a parent when it is included in the navigation. The parent is + # essential to correctly resolve the location where the archive and + # category views are attached. If the entrypoint doesn't have a parent, + # we know that the author did not include it in the navigation, so we + # explicitly mark it as not included. + if not self.blog.parent and self.config.blog_dir != ".": + self.blog.file.inclusion = InclusionLevel.NOT_IN_NAV + + # Attach posts to entrypoint without adding them to the navigation, so + # that the entrypoint is considered to be the active page for each post + self._attach(self.blog, [None, *reversed(self.blog.posts), None]) + for post in self.blog.posts: + post.file.inclusion = InclusionLevel.NOT_IN_NAV + + # Revert temporary exclusion of views from navigation + for view in self._resolve_views(self.blog): + for page in view.pages: + page.file.inclusion = self.blog.file.inclusion + + # Attach views for archive + if self.config.archive: + title = self._translate(self.config.archive_name, config) + views = [_ for _ in self.blog.views if isinstance(_, Archive)] + + # Attach and link views for archive + if self.blog.file.inclusion.is_in_nav(): + self._attach_to(self.blog, Section(title, views), nav) + + # Attach views for categories + if self.config.categories: + title = self._translate(self.config.categories_name, config) + views = [_ for _ in self.blog.views if isinstance(_, Category)] + + # Attach and link views for categories, if any + if self.blog.file.inclusion.is_in_nav() and views: + self._attach_to(self.blog, Section(title, views), nav) + + # Attach pages for views + if self.config.pagination: + for view in self._resolve_views(self.blog): + for at in range(1, len(view.pages)): + self._attach_at(view.parent, view, view.pages[at]) + + # Prepare post for rendering (run later) - allow other plugins to alter + # the contents or metadata of a post before it is rendered and make sure + # that the post includes a separator, which is essential for rendering + # excerpts that should be included in views + @event_priority(-50) + def on_page_markdown(self, markdown, *, page, config, files): + if not self.config.enabled: + return + + # Skip if page is not a post managed by this instance - this plugin has + # support for multiple instances, which is why this check is necessary + if page not in self.blog.posts: + if not self.config.pagination: + return + + # We set the contents of the view to its title if pagination should + # not keep the content of the original view on paginated views + if not self.config.pagination_keep_content: + view = self._resolve_original(page) + if view in self._resolve_views(self.blog): + + # If the current view is paginated, use the rendered title + # of the original view in case the author set the title in + # the page's contents, or it would be overridden with the + # one set in mkdocs.yml, leading to inconsistent headings + assert isinstance(view, View) + if view != page: + name = view._title_from_render or view.title + return f"# {name}" + + # Nothing more to be done for views + return + + # Extract and assign authors to post, if enabled + if self.config.authors: + for name in page.config.authors: + if name not in self.authors: + raise PluginError(f"Couldn't find author '{name}'") + + # Append to list of authors + page.authors.append(self.authors[name]) + + # Extract settings for excerpts + separator = self.config.post_excerpt_separator + max_authors = self.config.post_excerpt_max_authors + max_categories = self.config.post_excerpt_max_categories + + # Ensure presence of separator and throw, if its absent and required - + # we append the separator to the end of the contents of the post, if it + # is not already present, so we can remove footnotes or other content + # from the excerpt without affecting the content of the excerpt + if separator not in page.markdown: + path = page.file.src_path + if self.config.post_excerpt == "required": + raise PluginError( + f"Couldn't find '{separator}' separator in '{path}'" + ) + else: + page.markdown += f"\n\n{separator}" + + # Create excerpt for post and inherit authors and categories - excerpts + # can contain a subset of the authors and categories of the post + page.excerpt = Excerpt(page, config, files) + page.excerpt.authors = page.authors[:max_authors] + page.excerpt.categories = page.categories[:max_categories] + + # Process posts + def on_page_content(self, html, *, page, config, files): + if not self.config.enabled: + return + + # Skip if page is not a post managed by this instance - this plugin has + # support for multiple instances, which is why this check is necessary + if page not in self.blog.posts: + return + + # Compute readtime of post, if enabled and not explicitly set + if self.config.post_readtime: + words_per_minute = self.config.post_readtime_words_per_minute + if not page.config.readtime: + page.config.readtime = readtime(html, words_per_minute) + + # Register template filters for plugin + def on_env(self, env, *, config, files): + if not self.config.enabled: + return + + # Filter for formatting dates related to posts + def date_filter(date: datetime): + return self._format_date_for_post(date, config) + + # Register custom template filters + env.filters["date"] = date_filter + env.filters["url"] = url_filter + + # Prepare view for rendering (run latest) - views are rendered last, as we + # need to mutate the navigation to account for pagination. The main problem + # is that we need to replace the view in the navigation, because otherwise + # the view would not be considered active. + @event_priority(-100) + def on_page_context(self, context, *, page, config, nav): + if not self.config.enabled: + return + + # Skip if page is not a view managed by this instance - this plugin has + # support for multiple instances, which is why this check is necessary + view = self._resolve_original(page) + if view not in self._resolve_views(self.blog): + return + + # If the current view is paginated, replace and rewire it - the current + # view temporarily becomes the main view, and is reset after rendering + assert isinstance(view, View) + if view != page: + prev = view.pages[view.pages.index(page) - 1] + + # Replace previous page with current page + items = self._resolve_siblings(view, nav) + items[items.index(prev)] = page + + # Render excerpts and prepare pagination + posts, pagination = self._render(page) + + # Render pagination links + def pager(args: object): + return pagination.pager( + format = self.config.pagination_format, + show_if_single_page = self.config.pagination_if_single_page, + **args + ) + + # Assign posts and pagination to context + context["posts"] = posts + context["pagination"] = pager if pagination else None + + # After rendering a paginated view, replace the URL of the paginated view + # with the URL of the original view - since we need to replace the original + # view with a paginated view in `on_page_context` for correct resolution of + # the active state, we must fix the paginated view URLs after rendering + def on_post_page(self, output, *, page, config): + if not self.config.enabled: + return + + # Skip if page is not a view managed by this instance - this plugin has + # support for multiple instances, which is why this check is necessary + view = self._resolve_original(page) + if view not in self._resolve_views(self.blog): + return + + # If the current view is paginated, replace the URL of the paginated + # view with the URL of the original view - see https://t.ly/Yeh-P + assert isinstance(view, View) + if view != page: + page.file.url = view.file.url + + # Remove temporary directory on shutdown + def on_shutdown(self): + rmtree(self.temp_dir) + + # ------------------------------------------------------------------------- + + # Check if the given post is excluded + def _is_excluded(self, post: Post): + if self.config.draft: + return False + + # If a post was not explicitly marked or unmarked as draft, and the + # date should be taken into account, we automatically mark it as draft + # if the publishing date is in the future. This, of course, is opt-in + # and must be explicitly enabled by the author. + if not isinstance(post.config.draft, bool): + if self.config.draft_if_future_date: + return post.config.date.created > datetime.now() + + # Post might be a draft + return bool(post.config.draft) + + # ------------------------------------------------------------------------- + + # Resolve entrypoint - the entrypoint of the blog must have been created + # if it did not exist before, and hosts all posts sorted by descending date + def _resolve(self, files: Files, config: MkDocsConfig): + path = os.path.join(self.config.blog_dir, "index.md") + path = os.path.normpath(path) + + # Create entrypoint, if it does not exist - note that the entrypoint is + # created in the docs directory, not in the temporary directory + docs = os.path.relpath(config.docs_dir) + name = os.path.join(docs, path) + if not os.path.isfile(name): + file = self._path_to_file(path, config, temp = False) + files.append(file) + + # Create file in docs directory + self._save_to_file(file.abs_src_path, "# Blog\n\n") + + # Create and return entrypoint + file = files.get_file_from_path(path) + return View(None, file, config) + + # Resolve post - the caller must make sure that the given file points to an + # actual post (and not a page), or behavior might be unpredictable + def _resolve_post(self, file: File, config: MkDocsConfig): + post = Post(file, config) + + # Compute path and create a temporary file for path resolution + path = self._format_path_for_post(post, config) + temp = self._path_to_file(path, config, temp = False) + + # Replace destination file system path and URL + file.dest_uri = temp.dest_uri + file.abs_dest_path = temp.abs_dest_path + file.url = temp.url + + # Replace canonical URL and return post + post._set_canonical_url(config.site_url) + return post + + # Resolve posts from directory - traverse all documentation pages and filter + # and yield those that are located in the posts directory + def _resolve_posts(self, files: Files, config: MkDocsConfig): + path = self.config.post_dir.format(blog = self.config.blog_dir) + path = os.path.normpath(path) + + # Create posts directory, if it does not exist + docs = os.path.relpath(config.docs_dir) + name = os.path.join(docs, path) + if not os.path.isdir(name): + os.makedirs(name, exist_ok = True) + + # Filter posts from pages + for file in files.documentation_pages(): + if not file.src_path.startswith(path): + continue + + # Temporarily remove post from navigation + file.inclusion = InclusionLevel.EXCLUDED + + # Resolve post - in order to determine whether a post should be + # excluded, we must load it and analyze its metadata. All posts + # marked as drafts are excluded, except for when the author has + # configured drafts to be included in the navigation. + post = self._resolve_post(file, config) + if not self._is_excluded(post): + yield post + + # Resolve authors - check if there's an authors file at the configured + # location, and if one was found, load and validate it + def _resolve_authors(self, config: MkDocsConfig): + path = self.config.authors_file.format(blog = self.config.blog_dir) + path = os.path.normpath(path) + + # Resolve path relative to docs directory + docs = os.path.relpath(config.docs_dir) + file = os.path.join(docs, path) + + # If the authors file does not exist, return here + config: Authors = Authors() + if not os.path.isfile(file): + return config.authors + + # Open file and parse as YAML + with open(file, encoding = "utf-8") as f: + config.config_file_path = os.path.abspath(file) + try: + config.load_dict(yaml.load(f, SafeLoader) or {}) + + # The authors file could not be loaded because of a syntax error, + # which we display to the author with a nice error message + except Exception as e: + raise PluginError( + f"Error reading authors file '{path}' in '{docs}':\n" + f"{e}" + ) + + # Validate authors and throw if errors occurred + errors, warnings = config.validate() + if not config.authors and warnings: + log.warning( + f"Action required: the format of the authors file changed.\n" + f"All authors must now be located under the 'authors' key.\n" + f"Please adjust '{file}' to match:\n" + f"\n" + f"authors:\n" + f" squidfunk:\n" + f" avatar: https://avatars.githubusercontent.com/u/932156\n" + f" description: Creator\n" + f" name: Martin Donath\n" + f"\n" + ) + for _, w in warnings: + log.warning(w) + for _, e in errors: + raise PluginError( + f"Error reading authors file '{path}' in '{docs}':\n" + f"{e}" + ) + + # Return authors + return config.authors + + # Resolve views of the given view in pre-order + def _resolve_views(self, view: View): + yield view + + # Resolve views recursively + for page in view.views: + for next in self._resolve_views(page): + assert isinstance(next, View) + yield next + + # Resolve siblings of a navigation item + def _resolve_siblings(self, item: StructureItem, nav: Navigation): + if isinstance(item.parent, Section): + return item.parent.children + else: + return nav.items + + # Resolve original page or view (e.g. for paginated views) + def _resolve_original(self, page: Page): + if isinstance(page, View): + return page.pages[0] + else: + return page + + # ------------------------------------------------------------------------- + + # Generate views for archive - analyze posts and generate the necessary + # views, taking the date format provided by the author into account + def _generate_archive(self, config: MkDocsConfig, files: Files): + for post in self.blog.posts: + date = post.config.date.created + + # Compute name and path of archive view + name = self._format_date_for_archive(date, config) + path = self._format_path_for_archive(post, config) + + # Create file for view, if it does not exist + file = files.get_file_from_path(path) + if not file or self.temp_dir not in file.abs_src_path: + file = self._path_to_file(path, config) + files.append(file) + + # Create file in temporary directory + self._save_to_file(file.abs_src_path, f"# {name}") + + # Create and yield view - we don't explicitly set the title of + # the view, so authors can override them in the page's content + if not isinstance(file.page, Archive): + yield Archive(None, file, config) + + # Assign post to archive + assert isinstance(file.page, Archive) + file.page.posts.append(post) + + # Generate views for categories - analyze posts and generate the necessary + # views, taking the allowed categories as set by the author into account + def _generate_categories(self, config: MkDocsConfig, files: Files): + for post in self.blog.posts: + for name in post.config.categories: + path = self._format_path_for_category(name) + + # Ensure category is in non-empty allow list + categories = self.config.categories_allowed or [name] + if name not in categories: + docs = os.path.relpath(config.docs_dir) + path = os.path.relpath(post.file.abs_src_path, docs) + raise PluginError( + f"Error reading categories of post '{path}' in " + f"'{docs}': category '{name}' not in allow list" + ) + + # Create file for view, if it does not exist + file = files.get_file_from_path(path) + if not file or self.temp_dir not in file.abs_src_path: + file = self._path_to_file(path, config) + files.append(file) + + # Create file in temporary directory + self._save_to_file(file.abs_src_path, f"# {name}") + + # Create and yield view - we don't explicitly set the title of + # the view, so authors can override them in the page's content + if not isinstance(file.page, Category): + yield Category(None, file, config) + + # Assign post to category and vice versa + assert isinstance(file.page, Category) + file.page.posts.append(post) + post.categories.append(file.page) + + # Generate pages for pagination - analyze view and generate the necessary + # pages, creating a chain of views for simple rendering and replacement + def _generate_pages(self, view: View, config: MkDocsConfig, files: Files): + yield view + + # Compute pagination boundaries and create pages - pages are internally + # handled as copies of a view, as they map to the same source location + step = self.config.pagination_per_page + for at in range(step, len(view.posts), step): + path = self._format_path_for_pagination(view, 1 + at // step) + + # Create file for view, if it does not exist + file = files.get_file_from_path(path) + if not file or self.temp_dir not in file.abs_src_path: + file = self._path_to_file(path, config) + files.append(file) + + # Copy file to temporary directory + copy_file(view.file.abs_src_path, file.abs_src_path) + + # Create view and attach to previous page + if not isinstance(file.page, View): + yield View(None, file, config) + + # Assign pages and posts to view + assert isinstance(file.page, View) + file.page.pages = view.pages + file.page.posts = view.posts + + # ------------------------------------------------------------------------- + + # Attach a list of pages to each other and to the given parent item without + # explicitly adding them to the navigation, which can be done by the caller + def _attach(self, parent: StructureItem, pages: list[Page]): + for tail, page, head in zip(pages, pages[1:], pages[2:]): + + # Link page to parent and siblings + page.parent = parent + page.previous_page = tail + page.next_page = head + + # If the page is a view, we know that we generated it and need to + # link its siblings back to the view + if isinstance(page, View): + view = self._resolve_original(page) + if tail: tail.next_page = view + if head: head.previous_page = view + + # Attach a page to the given parent and link it to the previous and next + # page of the given host - this is exclusively used for paginated views + def _attach_at(self, parent: StructureItem, host: Page, page: Page): + self._attach(parent, [host.previous_page, page, host.next_page]) + + # Attach a section as a sibling to the given view, make sure its pages are + # part of the navigation, and ensure all pages are linked correctly + def _attach_to(self, view: View, section: Section, nav: Navigation): + section.parent = view.parent + + # Resolve siblings, which are the children of the parent section, or + # the top-level list of navigation items if the view is at the root of + # the project, and append the given section to it. It's currently not + # possible to chose the position of a section. + items = self._resolve_siblings(view, nav) + items.append(section) + + # Find last sibling that is a page, skipping sections, as we need to + # append the given section after all other pages + tail = next(item for item in reversed(items) if isinstance(item, Page)) + head = tail.next_page + + # Attach section to navigation and pages to each other + nav.pages.extend(section.children) + self._attach(section, [tail, *section.children, head]) + + # ------------------------------------------------------------------------- + + # Render excerpts and pagination for the given view + def _render(self, view: View): + posts, pagination = view.posts, None + + # Create pagination, if enabled + if self.config.pagination: + at = view.pages.index(view) + + # Compute pagination boundaries + step = self.config.pagination_per_page + p, q = at * step, at * step + step + + # Extract posts in pagination boundaries + posts = view.posts[p:q] + pagination = self._render_pagination(view, (p, q)) + + # Render excerpts for selected posts + posts = [ + self._render_post(post.excerpt, view) + for post in posts if post.excerpt + ] + + # Return posts and pagination + return posts, pagination + + # Render excerpt in the context of the given view + def _render_post(self, excerpt: Excerpt, view: View): + excerpt.render(view, self.config.post_excerpt_separator) + + # Determine whether to add posts to the table of contents of the view - + # note that those settings can be changed individually for each type of + # view, which is why we need to check the type of view and the table of + # contents setting for that type of view + toc = self.config.blog_toc + if isinstance(view, Archive): + toc = self.config.archive_toc + if isinstance(view, Category): + toc = self.config.categories_toc + + # Attach top-level table of contents item to view if it should be added + # and both, the view and excerpt contain table of contents items + if toc and excerpt.toc.items and view.toc.items: + view.toc.items[0].children.append(excerpt.toc.items[0]) + + # Return excerpt + return excerpt + + # Create pagination for the given view and range + def _render_pagination(self, view: View, range: tuple[int, int]): + p, q = range + + # Create URL from the given page to another page + def url_maker(n: int): + return get_relative_url(view.pages[n - 1].url, view.url) + + # Return pagination + return Pagination( + view.posts, page = q // (q - p), + items_per_page = q - p, + url_maker = url_maker + ) + + # ------------------------------------------------------------------------- + + # Format path for post + def _format_path_for_post(self, post: Post, config: MkDocsConfig): + categories = post.config.categories[:self.config.post_url_max_categories] + categories = [self._slugify_category(name) for name in categories] + + # Replace placeholders in format string + date = post.config.date.created + path = self.config.post_url_format.format( + categories = "/".join(categories), + date = self._format_date_for_post_url(date, config), + file = post.file.name, + slug = post.config.slug or self._slugify_post(post) + ) + + # Normalize path and strip slashes at the beginning and end + path = posixpath.normpath(path.strip("/")) + return posixpath.join(self.config.blog_dir, f"{path}.md") + + # Format path for archive + def _format_path_for_archive(self, post: Post, config: MkDocsConfig): + date = post.config.date.created + path = self.config.archive_url_format.format( + date = self._format_date_for_archive_url(date, config) + ) + + # Normalize path and strip slashes at the beginning and end + path = posixpath.normpath(path.strip("/")) + return posixpath.join(self.config.blog_dir, f"{path}.md") + + # Format path for category + def _format_path_for_category(self, name: str): + path = self.config.categories_url_format.format( + slug = self._slugify_category(name) + ) + + # Normalize path and strip slashes at the beginning and end + path = posixpath.normpath(path.strip("/")) + return posixpath.join(self.config.blog_dir, f"{path}.md") + + # Format path for pagination + def _format_path_for_pagination(self, view: View, page: int): + path = self.config.pagination_url_format.format( + page = page + ) + + # Compute base path for pagination - if the given view is an index file, + # we need to pop the file name from the base so it's not part of the URL + # and we need to append `index` to the path, so the paginated view is + # also an index page - see https://t.ly/71MKF + base, _ = posixpath.splitext(view.file.src_uri) + if view.is_index: + base = posixpath.dirname(base) + path = posixpath.join(path, "index") + + # Normalize path and strip slashes at the beginning and end + path = posixpath.normpath(path.strip("/")) + return posixpath.join(base, f"{path}.md") + + # ------------------------------------------------------------------------- + + # Format date + def _format_date(self, date: datetime, format: str, config: MkDocsConfig): + locale = config.theme["language"] + return format_date(date, format = format, locale = locale) + + # Format date for post + def _format_date_for_post(self, date: datetime, config: MkDocsConfig): + format = self.config.post_date_format + return self._format_date(date, format, config) + + # Format date for post URL + def _format_date_for_post_url(self, date: datetime, config: MkDocsConfig): + format = self.config.post_url_date_format + return self._format_date(date, format, config) + + # Format date for archive + def _format_date_for_archive(self, date: datetime, config: MkDocsConfig): + format = self.config.archive_date_format + return self._format_date(date, format, config) + + # Format date for archive URL + def _format_date_for_archive_url(self, date: datetime, config: MkDocsConfig): + format = self.config.archive_url_date_format + return self._format_date(date, format, config) + + # ------------------------------------------------------------------------- + + # Slugify post title + def _slugify_post(self, post: Post): + separator = self.config.post_slugify_separator + return self.config.post_slugify(post.title, separator) + + # Slugify category + def _slugify_category(self, name: str): + separator = self.config.categories_slugify_separator + return self.config.categories_slugify(name, separator) + + # ------------------------------------------------------------------------- + + # Create a file for the given path, which must point to a valid source file, + # either inside the temporary directory or the docs directory + def _path_to_file(self, path: str, config: MkDocsConfig, *, temp = True): + assert path.endswith(".md") + file = File( + path, + config.docs_dir if not temp else self.temp_dir, + config.site_dir, + config.use_directory_urls + ) + + # Hack: mark file as generated, so other plugins don't think it's part + # of the file system. This is more or less a new quasi-standard that + # still needs to be adopted by MkDocs, and was introduced by the + # git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx + if temp: + file.generated_by = "material/blog" + + # Return file + return file + + # Create a file with the given content on disk + def _save_to_file(self, path: str, content: str): + os.makedirs(os.path.dirname(path), exist_ok = True) + with open(path, "w", encoding = "utf-8") as f: + f.write(content) + + # ------------------------------------------------------------------------- + + # Translate the placeholder referenced by the given key + def _translate(self, key: str, config: MkDocsConfig) -> str: + env = config.theme.get_env() + template = env.get_template( + "partials/language.html", globals = { "config": config } + ) + + # Translate placeholder + return template.module.t(key) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.blog") diff --git a/docs/src/plugins/blog/readtime/__init__.py b/docs/src/plugins/blog/readtime/__init__.py new file mode 100644 index 00000000..a0c149b9 --- /dev/null +++ b/docs/src/plugins/blog/readtime/__init__.py @@ -0,0 +1,51 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import re + +from math import ceil + +from .parser import ReadtimeParser + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + +# Compute readtime - we first used the original readtime library, but the list +# of dependencies it brings with it increased the size of the Docker image by +# 20 MB (packed), which is an increase of 50%. For this reason, we adapt the +# original readtime algorithm to our needs - see https://t.ly/fPZ7L +def readtime(html: str, words_per_minute: int): + parser = ReadtimeParser() + parser.feed(html) + parser.close() + + # Extract words from text and compute readtime in seconds + words = len(re.split(r"\W+", "".join(parser.text))) + seconds = ceil(words / words_per_minute * 60) + + # Account for additional images + delta = 12 + for _ in range(parser.images): + seconds += delta + if delta > 3: delta -= 1 + + # Return readtime in minutes + return ceil(seconds / 60) diff --git a/docs/src/plugins/blog/readtime/parser.py b/docs/src/plugins/blog/readtime/parser.py new file mode 100644 index 00000000..b91a7b30 --- /dev/null +++ b/docs/src/plugins/blog/readtime/parser.py @@ -0,0 +1,45 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from html.parser import HTMLParser + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Readtime parser +class ReadtimeParser(HTMLParser): + + # Initialize parser + def __init__(self): + super().__init__(convert_charrefs = True) + + # Keep track of text and images + self.text = [] + self.images = 0 + + # Collect images + def handle_starttag(self, tag, attrs): + if tag == "img": + self.images += 1 + + # Collect text + def handle_data(self, data): + self.text.append(data) diff --git a/docs/src/plugins/blog/structure/__init__.py b/docs/src/plugins/blog/structure/__init__.py new file mode 100644 index 00000000..2fc541fe --- /dev/null +++ b/docs/src/plugins/blog/structure/__init__.py @@ -0,0 +1,292 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +import logging +import os +import yaml + +from copy import copy +from markdown import Markdown +from material.plugins.blog.author import Author +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.exceptions import PluginError +from mkdocs.structure.files import File, Files +from mkdocs.structure.nav import Section +from mkdocs.structure.pages import Page, _RelativePathTreeprocessor +from mkdocs.structure.toc import get_toc +from mkdocs.utils.meta import YAML_RE +from re import Match +from yaml import SafeLoader + +from .config import PostConfig +from .markdown import ExcerptTreeprocessor + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Post +class Post(Page): + + # Initialize post - posts are never listed in the navigation, which is why + # they will never include a title that was manually set, so we can omit it + def __init__(self, file: File, config: MkDocsConfig): + super().__init__(None, file, config) + + # Resolve path relative to docs directory + docs = os.path.relpath(config.docs_dir) + path = os.path.relpath(file.abs_src_path, docs) + + # Read contents and metadata immediately + with open(file.abs_src_path, encoding = "utf-8") as f: + self.markdown = f.read() + + # Sadly, MkDocs swallows any exceptions that occur during parsing. + # As we want to provide the best possible authoring experience, we + # need to catch errors early and display them nicely. We decided to + # drop support for MkDocs' MultiMarkdown syntax, because it is not + # correctly implemented anyway. When using MultiMarkdown syntax, all + # date formats are returned as strings and list are not properly + # supported. Thus, we just use the relevants parts of `get_data`. + match: Match = YAML_RE.match(self.markdown) + if not match: + raise PluginError( + f"Error reading metadata of post '{path}' in '{docs}':\n" + f"Expected metadata to be defined but found nothing" + ) + + # Extract metadata and parse as YAML + try: + self.meta = yaml.load(match.group(1), SafeLoader) or {} + self.markdown = self.markdown[match.end():].lstrip("\n") + + # The post's metadata could not be parsed because of a syntax error, + # which we display to the user with a nice error message + except Exception as e: + raise PluginError( + f"Error reading metadata of post '{path}' in '{docs}':\n" + f"{e}" + ) + + # Initialize post configuration, but remove all keys that this plugin + # doesn't care about, or they will be reported as invalid configuration + self.config: PostConfig = PostConfig(file.abs_src_path) + self.config.load_dict({ + key: self.meta[key] for key in ( + set(self.meta.keys()) & + set(self.config.keys()) + ) + }) + + # Validate configuration and throw if errors occurred + errors, warnings = self.config.validate() + for _, w in warnings: + log.warning(w) + for k, e in errors: + raise PluginError( + f"Error reading metadata '{k}' of post '{path}' in '{docs}':\n" + f"{e}" + ) + + # Excerpts are subsets of posts that are used in pages like archive and + # category views. They are not rendered as standalone pages, but are + # rendered in the context of a view. Each post has a dedicated excerpt + # instance which is reused when rendering views. + self.excerpt: Excerpt = None + + # Initialize authors and actegories + self.authors: list[Author] = [] + self.categories: list[Category] = [] + + # Ensure template is set or use default + self.meta.setdefault("template", "blog-post.html") + + # Ensure template hides navigation + self.meta["hide"] = self.meta.get("hide", []) + if "navigation" not in self.meta["hide"]: + self.meta["hide"].append("navigation") + + # The contents and metadata were already read in the constructor (and not + # in `read_source` as for pages), so this function must be set to a no-op + def read_source(self, config: MkDocsConfig): + pass + +# ----------------------------------------------------------------------------- + +# Excerpt +class Excerpt(Page): + + # Initialize an excerpt for the given post - we create the Markdown parser + # when intitializing the excerpt in order to improve rendering performance + # for excerpts, as they are reused across several different views, because + # posts might be referenced from multiple different locations + def __init__(self, post: Post, config: MkDocsConfig, files: Files): + self.file = copy(post.file) + self.post = post + + # Set canonical URL, or we can't print excerpts when debugging the + # blog plugin, as the `abs_url` property would be missing + self._set_canonical_url(config.site_url) + + # Initialize configuration and metadata + self.config = post.config + self.meta = post.meta + + # Initialize authors and categories - note that views usually contain + # subsets of those lists, which is why we need to manage them here + self.authors: list[Author] = [] + self.categories: list[Category] = [] + + # Initialize parser - note that we need to patch the configuration, + # more specifically the table of contents extension + config = _patch(config) + self.md = Markdown( + extensions = config.markdown_extensions, + extension_configs = config.mdx_configs, + ) + + # Register excerpt tree processor - this processor resolves anchors to + # posts from within views, so they point to the correct location + self.md.treeprocessors.register( + ExcerptTreeprocessor(post), + "excerpt", + 0 + ) + + # Register relative path tree processor - this processor resolves links + # to other pages and assets, and is used by MkDocs itself + self.md.treeprocessors.register( + _RelativePathTreeprocessor(self.file, files, config), + "relpath", + 1 + ) + + # Render an excerpt of the post on the given page - note that this is not + # thread-safe because excerpts are shared across views, as it cuts down on + # the cost of initialization. However, if in the future, we decide to render + # posts and views concurrently, we must change this behavior. + def render(self, page: Page, separator: str): + self.file.url = page.url + + # Retrieve excerpt tree processor and set page as base + at = self.md.treeprocessors.get_index_for_name("excerpt") + processor: ExcerptTreeprocessor = self.md.treeprocessors[at] + processor.base = page + + # Ensure that the excerpt includes a title in its content, since the + # title is linked to the post when rendering - see https://t.ly/5Gg2F + self.markdown = self.post.markdown + if not self.post._title_from_render: + self.markdown = "\n\n".join([f"# {self.post.title}", self.markdown]) + + # Convert Markdown to HTML and extract excerpt + self.content = self.md.convert(self.markdown) + self.content, *_ = self.content.split(separator, 1) + + # Extract table of contents and reset post URL - if we wouldn't reset + # the excerpt URL, linking to the excerpt from the view would not work + self.toc = get_toc(getattr(self.md, "toc_tokens", [])) + self.file.url = self.post.url + +# ----------------------------------------------------------------------------- + +# View +class View(Page): + + # Initialize view + def __init__(self, title: str | None, file: File, config: MkDocsConfig): + super().__init__(title, file, config) + self.parent: View | Section + + # Initialize posts and views + self.posts: list[Post] = [] + self.views: list[View] = [] + + # Initialize pages for pagination + self.pages: list[View] = [] + + # Set necessary metadata + def read_source(self, config: MkDocsConfig): + super().read_source(config) + + # Ensure template is set or use default + self.meta.setdefault("template", "blog.html") + +# ----------------------------------------------------------------------------- + +# Archive view +class Archive(View): + pass + +# ----------------------------------------------------------------------------- + +# Category view +class Category(View): + pass + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + +# Patch configuration +def _patch(config: MkDocsConfig): + config = copy(config) + + # Copy parts of configuration that needs to be patched + config.validation = copy(config.validation) + config.validation.links = copy(config.validation.links) + config.markdown_extensions = copy(config.markdown_extensions) + config.mdx_configs = copy(config.mdx_configs) + + # Make sure that the author did not add another instance of the table of + # contents extension to the configuration, as this leads to weird behavior + if "markdown.extensions.toc" in config.markdown_extensions: + config.markdown_extensions.remove("markdown.extensions.toc") + + # In order to render excerpts for posts, we need to make sure that the + # table of contents extension is appropriately configured + config.mdx_configs["toc"] = { + **config.mdx_configs.get("toc", {}), + **{ + "anchorlink": True, # Render headline as clickable + "baselevel": 2, # Render h1 as h2 and so forth + "permalink": False, # Remove permalinks + "toc_depth": 2 # Remove everything below h2 + } + } + + # Additionally, we disable link validation when rendering excerpts, because + # invalid links have already been reported when rendering the page + links = config.validation.links + links.not_found = logging.DEBUG + links.absolute_links = logging.DEBUG + links.unrecognized_links = logging.DEBUG + + # Return patched configuration + return config + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.blog") diff --git a/docs/src/plugins/blog/structure/config.py b/docs/src/plugins/blog/structure/config.py new file mode 100644 index 00000000..129491b9 --- /dev/null +++ b/docs/src/plugins/blog/structure/config.py @@ -0,0 +1,37 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.base import Config +from mkdocs.config.config_options import ListOfItems, Optional, Type + +from .options import PostDate + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Post configuration +class PostConfig(Config): + authors = ListOfItems(Type(str), default = []) + categories = ListOfItems(Type(str), default = []) + date = PostDate() + draft = Optional(Type(bool)) + readtime = Optional(Type(int)) + slug = Optional(Type(str)) diff --git a/docs/src/plugins/blog/structure/markdown.py b/docs/src/plugins/blog/structure/markdown.py new file mode 100644 index 00000000..64ade554 --- /dev/null +++ b/docs/src/plugins/blog/structure/markdown.py @@ -0,0 +1,58 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from markdown.treeprocessors import Treeprocessor +from mkdocs.structure.pages import Page +from mkdocs.utils import get_relative_url +from xml.etree.ElementTree import Element + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Excerpt tree processor +class ExcerptTreeprocessor(Treeprocessor): + + # Initialize excerpt tree processor + def __init__(self, page: Page, base: Page = None): + self.page = page + self.base = base + + # Transform HTML after Markdown processing + def run(self, root: Element): + main = True + + # We're only interested in anchors, which is why we continue when the + # link does not start with an anchor tag + for el in root.iter("a"): + anchor = el.get("href") + if not anchor.startswith("#"): + continue + + # The main headline should link to the post page, not to a specific + # anchor, which is why we remove the anchor in that case + path = get_relative_url(self.page.url, self.base.url) + if main: + el.set("href", path) + else: + el.set("href", path + anchor) + + # Main headline has been seen + main = False diff --git a/docs/src/plugins/blog/structure/options.py b/docs/src/plugins/blog/structure/options.py new file mode 100644 index 00000000..281dec9f --- /dev/null +++ b/docs/src/plugins/blog/structure/options.py @@ -0,0 +1,87 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from datetime import date, datetime, time +from mkdocs.config.base import BaseConfigOption, Config, ValidationError +from typing import Dict + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Date dictionary +class DateDict(Dict[str, datetime]): + + # Initialize date dictionary + def __init__(self, data: dict): + super().__init__(data) + + # Ensure presence of `date.created` + self.created: datetime = data["created"] + + # Allow attribute access + def __getattr__(self, name: str): + if name in self: + return self[name] + +# ----------------------------------------------------------------------------- + +# Post date option +class PostDate(BaseConfigOption[DateDict]): + + # Initialize post dates + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Normalize the supported types for post dates to datetime + def pre_validation(self, config: Config, key_name: str): + + # If the date points to a scalar value, convert it to a dictionary, + # since we want to allow the user to specify custom and arbitrary date + # values for posts. Currently, only the `created` date is mandatory, + # because it's needed to sort posts for views. + if not isinstance(config[key_name], dict): + config[key_name] = { "created": config[key_name] } + + # Convert all date values to datetime + for key, value in config[key_name].items(): + if isinstance(value, date): + config[key_name][key] = datetime.combine(value, time()) + + # Initialize date dictionary + config[key_name] = DateDict(config[key_name]) + + # Ensure each date value is of type datetime + def run_validation(self, value: DateDict): + for key in value: + if not isinstance(value[key], datetime): + raise ValidationError( + f"Expected type: {date} or {datetime} " + f"but received: {type(value[key])}" + ) + + # Ensure presence of `date.created` + if not value.created: + raise ValidationError( + "Expected 'created' date when using dictionary syntax" + ) + + # Return date dictionary + return value diff --git a/docs/src/plugins/blog/templates/__init__.py b/docs/src/plugins/blog/templates/__init__.py new file mode 100644 index 00000000..9f7d794b --- /dev/null +++ b/docs/src/plugins/blog/templates/__init__.py @@ -0,0 +1,42 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from jinja2 import pass_context +from jinja2.runtime import Context +from material.plugins.blog.structure import View +from mkdocs.utils.templates import url_filter as _url_filter + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + +# Filter for normalizing URLs with support for paginated views +@pass_context +def url_filter(context: Context, url: str): + page = context["page"] + + # If the current page is a view, check if the URL links to the page + # itself, and replace it with the URL of the main view + if isinstance(page, View): + if page.url == url: + url = page.pages[0].url + + # Forward to original template filter + return _url_filter(context, url) diff --git a/docs/src/plugins/group/__init__.py b/docs/src/plugins/group/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/group/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/group/config.py b/docs/src/plugins/group/config.py new file mode 100644 index 00000000..fb19222a --- /dev/null +++ b/docs/src/plugins/group/config.py @@ -0,0 +1,33 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from __future__ import annotations + +from mkdocs.config.config_options import Type +from mkdocs.config.base import Config + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Group plugin configuration +class GroupConfig(Config): + enabled = Type(bool, default = False) + plugins = Type(list | dict) diff --git a/docs/src/plugins/group/plugin.py b/docs/src/plugins/group/plugin.py new file mode 100644 index 00000000..4ab13dbf --- /dev/null +++ b/docs/src/plugins/group/plugin.py @@ -0,0 +1,151 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import logging + +from collections.abc import Callable +from mkdocs.config.config_options import Plugins +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.exceptions import PluginError +from mkdocs.plugins import BasePlugin, event_priority + +from .config import GroupConfig + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Group plugin +class GroupPlugin(BasePlugin[GroupConfig]): + supports_multiple_instances = True + + # Determine whether we're serving the site + def on_startup(self, *, command, dirty): + self.is_serve = command == "serve" + self.is_dirty = dirty + + # If the group is enabled, conditionally load plugins - at first, this might + # sound easier than it actually is, as we need to jump through some hoops to + # ensure correct ordering among plugins. We're effectively initializing the + # plugins that are part of the group after all MkDocs finished initializing + # all other plugins, so we need to patch the order of the methods. Moreover, + # we must use MkDocs existing plugin collection, or we might have collisions + # with other plugins that are not part of the group. As so often, this is a + # little hacky, but has huge potential making plugin configuration easier. + # There's one little caveat: the `__init__` and `on_startup` methods of the + # plugins that are part of the group are called after all other plugins, so + # the `event_priority` decorator for `on_startup` events and is effectively + # useless. However, the `on_startup` event is only intended to set up the + # plugin and doesn't receive anything else than the invoked command and + # whether we're running a dirty build, so there should be no problems. + @event_priority(150) + def on_config(self, config): + if not self.config.enabled: + return + + # Retrieve plugin collection from configuration + option: Plugins = dict(config._schema)["plugins"] + assert isinstance(option, Plugins) + + # Load all plugins in group + self.plugins: dict[str, BasePlugin] = {} + try: + for name, plugin in self._load(option): + self.plugins[name] = plugin + + # The plugin could not be loaded, likely because it's not installed or + # misconfigured, so we raise a plugin error for a nicer error message + except Exception as e: + raise PluginError(str(e)) + + # Patch order of plugin methods + for events in option.plugins.events.values(): + self._patch(events, config) + + # Invoke `on_startup` event for plugins in group + command = "serve" if self.is_serve else "build" + for method in option.plugins.events["startup"]: + plugin = self._get_plugin(method) + + # Ensure that we have a method bound to a plugin (and not a hook) + if plugin and plugin in self.plugins.values(): + method(command = command, dirty = self.is_dirty) + + # ------------------------------------------------------------------------- + + # Retrieve plugin instance for bound method or nothing + def _get_plugin(self, method: Callable): + return getattr(method, "__self__", None) + + # Retrieve priority of plugin method + def _get_priority(self, method: Callable): + return getattr(method, "mkdocs_priority", 0) + + # Retrieve position of plugin + def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int: + for at, (_, candidate) in enumerate(config.plugins.items()): + if plugin == candidate: + return at + + # ------------------------------------------------------------------------- + + # Load plugins that are part of the group + def _load(self, option: Plugins): + for name, data in option._parse_configs(self.config.plugins): + yield option.load_plugin_with_namespace(name, data) + + # ------------------------------------------------------------------------- + + # Patch order of plugin methods - all other plugin methods are already in + # the right order, so we only need to check those that are part of the group + # and bubble them up into the right location. Some plugin methods may define + # priorities, so we need to make sure to order correctly within those. + def _patch(self, methods: list[Callable], config: MkDocsConfig): + position = self._get_position(self, config) + for at in reversed(range(1, len(methods))): + tail = methods[at - 1] + head = methods[at] + + # Skip if the plugin is not part of the group + plugin = self._get_plugin(head) + if not plugin or plugin not in self.plugins.values(): + continue + + # Skip if the previous method has a higher priority than the current + # one, because we know we can't swap them anyway + if self._get_priority(tail) > self._get_priority(head): + continue + + # Ensure that we have a method bound to a plugin (and not a hook) + plugin = self._get_plugin(tail) + if not plugin: + continue + + # Both methods have the same priority, so we check if the ordering + # of both methods is violated, and if it is, swap them + if (position < self._get_position(plugin, config)): + methods[at], methods[at - 1] = tail, head + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.group") diff --git a/docs/src/plugins/info/__init__.py b/docs/src/plugins/info/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/info/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/info/config.py b/docs/src/plugins/info/config.py new file mode 100644 index 00000000..cbd64d4c --- /dev/null +++ b/docs/src/plugins/info/config.py @@ -0,0 +1,35 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.config_options import Type +from mkdocs.config.base import Config + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Info plugin configuration +class InfoConfig(Config): + enabled = Type(bool, default = True) + enabled_on_serve = Type(bool, default = False) + + # Settings for archive + archive = Type(bool, default = True) + archive_stop_on_violation = Type(bool, default = True) diff --git a/docs/src/plugins/info/plugin.py b/docs/src/plugins/info/plugin.py new file mode 100644 index 00000000..7c6fdc17 --- /dev/null +++ b/docs/src/plugins/info/plugin.py @@ -0,0 +1,245 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import json +import logging +import os +import platform +import requests +import sys + +from colorama import Fore, Style +from importlib.metadata import distributions, version +from io import BytesIO +from markdown.extensions.toc import slugify +from mkdocs.plugins import BasePlugin, event_priority +from mkdocs.structure.files import get_files +from mkdocs.utils import get_theme_dir +from zipfile import ZipFile, ZIP_DEFLATED + +from .config import InfoConfig + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Info plugin +class InfoPlugin(BasePlugin[InfoConfig]): + + # Initialize plugin + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Initialize incremental builds + self.is_serve = False + + # Determine whether we're serving the site + def on_startup(self, *, command, dirty): + self.is_serve = command == "serve" + + # Create a self-contained example (run earliest) - determine all files that + # are visible to MkDocs and are used to build the site, create an archive + # that contains all of them, and print a summary of the archive contents. + # The user must attach this archive to the bug report. + @event_priority(100) + def on_config(self, config): + if not self.config.enabled: + return + + # By default, the plugin is disabled when the documentation is served, + # but not when it is built. This should nicely align with the expected + # user experience when creating reproductions. + if not self.config.enabled_on_serve and self.is_serve: + return + + # Resolve latest version + url = "https://github.com/squidfunk/mkdocs-material/releases/latest" + res = requests.get(url, allow_redirects = False) + + # Check if we're running the latest version + _, current = res.headers.get("location").rsplit("/", 1) + present = version("mkdocs-material") + if not present.startswith(current): + log.error("Please upgrade to the latest version.") + self._help_on_versions_and_exit(present, current) + + # Exit if archive creation is disabled + if not self.config.archive: + sys.exit(1) + + # Print message that we're creating a bug report + log.info("Started archive creation for bug report") + + # Check that there are no overrides in place - we need to use a little + # hack to detect whether the custom_dir setting was used without parsing + # mkdocs.yml again - we check at which position the directory provided + # by the theme resides, and if it's not the first one, abort. + if config.theme.dirs.index(get_theme_dir(config.theme.name)): + log.error("Please remove 'custom_dir' setting.") + self._help_on_customizations_and_exit() + + # Check that there are no hooks in place - hooks can alter the behavior + # of MkDocs in unpredictable ways, which is why they must be considered + # being customizations. Thus, we can't offer support for debugging and + # must abort here. + if config.hooks: + log.error("Please remove 'hooks' setting.") + self._help_on_customizations_and_exit() + + # Create in-memory archive and prompt user to enter a short descriptive + # name for the archive, which is also used as the directory name. Note + # that the name is slugified for better readability and stripped of any + # file extension that the user might have entered. + archive = BytesIO() + example = input("\nPlease name your bug report (2-4 words): ") + example, _ = os.path.splitext(example) + example = "-".join([present, slugify(example, "-")]) + + # Create self-contained example from project + files: list[str] = [] + with ZipFile(archive, "a", ZIP_DEFLATED, False) as f: + for path in ["mkdocs.yml", "requirements.txt"]: + if os.path.isfile(path): + f.write(path, os.path.join(example, path)) + + # Append all files visible to MkDocs + for file in get_files(config): + path = os.path.relpath(file.abs_src_path, os.path.curdir) + f.write(path, os.path.join(example, path)) + + # Add information on installed packages + f.writestr( + os.path.join(example, "requirements.lock.txt"), + "\n".join(sorted([ + "==".join([package.name, package.version]) + for package in distributions() + ])) + ) + + # Add information on platform + f.writestr( + os.path.join(example, "platform.json"), + json.dumps( + { + "system": platform.platform(), + "python": platform.python_version() + }, + default = str, + indent = 2 + ) + ) + + # Retrieve list of processed files + for a in f.filelist: + files.append("".join([ + Fore.LIGHTBLACK_EX, a.filename, " ", + _size(a.compress_size) + ])) + + # Finally, write archive to disk + buffer = archive.getbuffer() + with open(f"{example}.zip", "wb") as f: + f.write(archive.getvalue()) + + # Print summary + log.info("Archive successfully created:") + print(Style.NORMAL) + + # Print archive file names + files.sort() + for file in files: + print(f" {file}") + + # Print archive name + print(Style.RESET_ALL) + print("".join([ + " ", f.name, " ", + _size(buffer.nbytes, 10) + ])) + + # Print warning when file size is excessively large + print(Style.RESET_ALL) + if buffer.nbytes > 1000000: + log.warning("Archive exceeds recommended maximum size of 1 MB") + + # Aaaaaand done + sys.exit(1) + + # ------------------------------------------------------------------------- + + # Print help on versions and exit + def _help_on_versions_and_exit(self, have, need): + print(Fore.RED) + print(" When reporting issues, please first upgrade to the latest") + print(" version of Material for MkDocs, as the problem might already") + print(" be fixed in the latest version. This helps reduce duplicate") + print(" efforts and saves us maintainers time.") + print(Style.NORMAL) + print(f" Please update from {have} to {need}.") + print(Style.RESET_ALL) + print(f" pip install --upgrade --force-reinstall mkdocs-material") + print(Style.NORMAL) + + # Exit, unless explicitly told not to + if self.config.archive_stop_on_violation: + sys.exit(1) + + # Print help on customizations and exit + def _help_on_customizations_and_exit(self): + print(Fore.RED) + print(" When reporting issues, you must remove all customizations") + print(" and check if the problem persists. If not, the problem is") + print(" caused by your overrides. Please understand that we can't") + print(" help you debug your customizations. Please remove:") + print(Style.NORMAL) + print(" - theme.custom_dir") + print(" - hooks") + print(Fore.YELLOW) + print(" Additionally, please remove all third-party JavaScript or") + print(" CSS not explicitly mentioned in our documentation:") + print(Style.NORMAL) + print(" - extra_css") + print(" - extra_javascript") + print(Style.RESET_ALL) + + # Exit, unless explicitly told not to + if self.config.archive_stop_on_violation: + sys.exit(1) + +# ----------------------------------------------------------------------------- +# Helper functions +# ----------------------------------------------------------------------------- + +# Print human-readable size +def _size(value, factor = 1): + color = Fore.GREEN + if value > 100000 * factor: color = Fore.RED + elif value > 25000 * factor: color = Fore.YELLOW + for unit in ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"]: + if abs(value) < 1000.0: + return f"{color}{value:3.1f} {unit}" + value /= 1000.0 + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.info") diff --git a/docs/src/plugins/offline/__init__.py b/docs/src/plugins/offline/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/offline/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/offline/config.py b/docs/src/plugins/offline/config.py new file mode 100644 index 00000000..49f51a94 --- /dev/null +++ b/docs/src/plugins/offline/config.py @@ -0,0 +1,30 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.config_options import Type +from mkdocs.config.base import Config + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Offline plugin configuration +class OfflineConfig(Config): + enabled = Type(bool, default = True) diff --git a/docs/src/plugins/offline/plugin.py b/docs/src/plugins/offline/plugin.py new file mode 100644 index 00000000..abcb2598 --- /dev/null +++ b/docs/src/plugins/offline/plugin.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import os + +from mkdocs.plugins import BasePlugin, event_priority + +from .config import OfflineConfig + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Offline plugin +class OfflinePlugin(BasePlugin[OfflineConfig]): + + # Set configuration for offline build + def on_config(self, config): + if not self.config.enabled: + return + + # Ensure correct resolution of links when viewing the site from the + # file system by disabling directory URLs + config.use_directory_urls = False + + # Append iframe-worker to polyfills/shims + config.extra["polyfills"] = config.extra.get("polyfills", []) + if not any("iframe-worker" in url for url in config.extra["polyfills"]): + script = "https://unpkg.com/iframe-worker/shim" + config.extra["polyfills"].append(script) + + # Add support for offline search (run latest) - the search index is copied + # and inlined into a script, so that it can be used without a server + @event_priority(-100) + def on_post_build(self, *, config): + if not self.config.enabled: + return + + # Ensure presence of search index + path = os.path.join(config.site_dir, "search") + file = os.path.join(path, "search_index.json") + if not os.path.isfile(file): + return + + # Obtain search index contents + with open(file, encoding = "utf-8") as f: + data = f.read() + + # Inline search index contents into script + file = os.path.join(path, "search_index.js") + with open(file, "w", encoding = "utf-8") as f: + f.write(f"var __index = {data}") diff --git a/docs/src/plugins/search/__init__.py b/docs/src/plugins/search/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/search/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/search/config.py b/docs/src/plugins/search/config.py new file mode 100644 index 00000000..e150fbb3 --- /dev/null +++ b/docs/src/plugins/search/config.py @@ -0,0 +1,58 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.config_options import ( + Choice, + Deprecated, + Optional, + ListOfItems, + Type +) +from mkdocs.config.base import Config +from mkdocs.contrib.search import LangOption + +# ----------------------------------------------------------------------------- +# Options +# ----------------------------------------------------------------------------- + +# Options for search pipeline +pipeline = ("stemmer", "stopWordFilter", "trimmer") + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Search plugin configuration +class SearchConfig(Config): + enabled = Type(bool, default = True) + + # Settings for search + lang = Optional(LangOption()) + separator = Optional(Type(str)) + pipeline = ListOfItems(Choice(pipeline), default = []) + + # Settings for text segmentation (Chinese) + jieba_dict = Optional(Type(str)) + jieba_dict_user = Optional(Type(str)) + + # Unsupported settings, originally implemented in MkDocs + indexing = Deprecated(message = "Unsupported option") + prebuild_index = Deprecated(message = "Unsupported option") + min_search_length = Deprecated(message = "Unsupported option") diff --git a/docs/src/plugins/search/plugin.py b/docs/src/plugins/search/plugin.py new file mode 100644 index 00000000..5c254e3f --- /dev/null +++ b/docs/src/plugins/search/plugin.py @@ -0,0 +1,580 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import json +import logging +import os +import regex as re + +from html import escape +from html.parser import HTMLParser +from mkdocs import utils +from mkdocs.plugins import BasePlugin + +from .config import SearchConfig + +try: + import jieba +except ImportError: + jieba = None + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Search plugin +class SearchPlugin(BasePlugin[SearchConfig]): + + # Initialize plugin + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Initialize incremental builds + self.is_dirtyreload = False + + # Initialize search index cache + self.search_index_prev = None + + # Determine whether we're serving the site + def on_startup(self, *, command, dirty): + self.is_dirty = dirty + + # Initialize plugin + def on_config(self, config): + if not self.config.enabled: + return + + # Retrieve default value for language + if not self.config.lang: + self.config.lang = [self._translate( + config, "search.config.lang" + )] + + # Retrieve default value for separator + if not self.config.separator: + self.config.separator = self._translate( + config, "search.config.separator" + ) + + # Retrieve default value for pipeline + if not self.config.pipeline: + self.config.pipeline = list(filter(len, re.split( + r"\s*,\s*", self._translate(config, "search.config.pipeline") + ))) + + # Initialize search index + self.search_index = SearchIndex(**self.config) + + # Set jieba dictionary, if given + if self.config.jieba_dict: + path = os.path.normpath(self.config.jieba_dict) + if os.path.isfile(path): + jieba.set_dictionary(path) + log.debug(f"Loading jieba dictionary: {path}") + else: + log.warning( + f"Configuration error for 'search.jieba_dict': " + f"'{self.config.jieba_dict}' does not exist." + ) + + # Set jieba user dictionary, if given + if self.config.jieba_dict_user: + path = os.path.normpath(self.config.jieba_dict_user) + if os.path.isfile(path): + jieba.load_userdict(path) + log.debug(f"Loading jieba user dictionary: {path}") + else: + log.warning( + f"Configuration error for 'search.jieba_dict_user': " + f"'{self.config.jieba_dict_user}' does not exist." + ) + + # Add page to search index + def on_page_context(self, context, *, page, config, nav): + if not self.config.enabled: + return + + # Index page + self.search_index.add_entry_from_context(page) + page.content = re.sub( + r"\s?data-search-\w+=\"[^\"]+\"", + "", + page.content + ) + + # Generate search index + def on_post_build(self, *, config): + if not self.config.enabled: + return + + # Write search index + base = os.path.join(config.site_dir, "search") + path = os.path.join(base, "search_index.json") + + # Generate and write search index to file + data = self.search_index.generate_search_index(self.search_index_prev) + utils.write_file(data.encode("utf-8"), path) + + # Persist search index for repeated invocation + if self.is_dirty: + self.search_index_prev = self.search_index + + # Determine whether we're running under dirty reload + def on_serve(self, server, *, config, builder): + self.is_dirtyreload = self.is_dirty + + # ------------------------------------------------------------------------- + + # Translate the given placeholder value + def _translate(self, config, value): + env = config.theme.get_env() + + # Load language template and return translation for placeholder + language = "partials/language.html" + template = env.get_template(language, None, { "config": config }) + return template.module.t(value) + +# ----------------------------------------------------------------------------- + +# Search index with support for additional fields +class SearchIndex: + + # Initialize search index + def __init__(self, **config): + self.config = config + self.entries = [] + + # Add page to search index + def add_entry_from_context(self, page): + search = page.meta.get("search", {}) + if search.get("exclude"): + return + + # Divide page content into sections + parser = Parser() + parser.feed(page.content) + parser.close() + + # Add sections to index + for section in parser.data: + if not section.is_excluded(): + self.create_entry_for_section(section, page.toc, page.url, page) + + # Override: graceful indexing and additional fields + def create_entry_for_section(self, section, toc, url, page): + item = self._find_toc_by_id(toc, section.id) + if item: + url = url + item.url + elif section.id: + url = url + "#" + section.id + + # Set page title as section title if none was given, which happens when + # the first headline in a Markdown document is not a h1 headline. Also, + # if a page title was set via front matter, use that even though a h1 + # might be given or the page name was specified in nav in mkdocs.yml + if not section.title: + section.title = [str(page.meta.get("title", page.title))] + + # Compute title and text + title = "".join(section.title).strip() + text = "".join(section.text).strip() + + # Segment Chinese characters if jieba is available + if jieba: + title = self._segment_chinese(title) + text = self._segment_chinese(text) + + # Create entry for section + entry = { + "location": url, + "title": title, + "text": text + } + + # Set document tags + tags = page.meta.get("tags") + if isinstance(tags, list): + entry["tags"] = [] + for name in tags: + if name and isinstance(name, (str, int, float, bool)): + entry["tags"].append(name) + + # Set document boost + search = page.meta.get("search", {}) + if "boost" in search: + entry["boost"] = search["boost"] + + # Add entry to index + self.entries.append(entry) + + # Generate search index + def generate_search_index(self, prev): + config = { + key: self.config[key] + for key in ["lang", "separator", "pipeline"] + } + + # Hack: if we're running under dirty reload, the search index will only + # include the entries for the current page. However, MkDocs > 1.4 allows + # us to persist plugin state across rebuilds, which is exactly what we + # do by passing the previously built index to this method. Thus, we just + # remove the previous entries for the current page, and append the new + # entries to the end of the index, as order doesn't matter. + if prev and self.entries: + path = self.entries[0]["location"] + + # Since we're sure that we're running under dirty reload, the list + # of entries will only contain sections for a single page. Thus, we + # use the first entry to remove all entries from the previous run + # that belong to the current page. The rationale behind this is that + # authors might add or remove section headers, so we need to make + # sure that sections are synchronized correctly. + entries = [ + entry for entry in prev.entries + if not entry["location"].startswith(path) + ] + + # Merge previous with current entries + self.entries = entries + self.entries + + # Otherwise just set previous entries + if prev and not self.entries: + self.entries = prev.entries + + # Return search index as JSON + data = { "config": config, "docs": self.entries } + return json.dumps( + data, + separators = (",", ":"), + default = str + ) + + # ------------------------------------------------------------------------- + + # Retrieve item for anchor + def _find_toc_by_id(self, toc, id): + for toc_item in toc: + if toc_item.id == id: + return toc_item + + # Recurse into children of item + toc_item = self._find_toc_by_id(toc_item.children, id) + if toc_item is not None: + return toc_item + + # No item found + return None + + # Find and segment Chinese characters in string + def _segment_chinese(self, data): + expr = re.compile(r"(\p{IsHan}+)", re.UNICODE) + + # Replace callback + def replace(match): + value = match.group(0) + + # Replace occurrence in original string with segmented version and + # surround with zero-width whitespace for efficient indexing + return "".join([ + "\u200b", + "\u200b".join(jieba.cut(value.encode("utf-8"))), + "\u200b", + ]) + + # Return string with segmented occurrences + return expr.sub(replace, data).strip("\u200b") + +# ----------------------------------------------------------------------------- + +# HTML element +class Element: + """ + An element with attributes, essentially a small wrapper object for the + parser to access attributes in other callbacks than handle_starttag. + """ + + # Initialize HTML element + def __init__(self, tag, attrs = {}): + self.tag = tag + self.attrs = attrs + + # String representation + def __repr__(self): + return self.tag + + # Support comparison (compare by tag only) + def __eq__(self, other): + if other is Element: + return self.tag == other.tag + else: + return self.tag == other + + # Support set operations + def __hash__(self): + return hash(self.tag) + + # Check whether the element should be excluded + def is_excluded(self): + return "data-search-exclude" in self.attrs + +# ----------------------------------------------------------------------------- + +# HTML section +class Section: + """ + A block of text with markup, preceded by a title (with markup), i.e., a + headline with a certain level (h1-h6). Internally used by the parser. + """ + + # Initialize HTML section + def __init__(self, el, depth = 0): + self.el = el + self.depth = depth + + # Initialize section data + self.text = [] + self.title = [] + self.id = None + + # String representation + def __repr__(self): + if self.id: + return "#".join([self.el.tag, self.id]) + else: + return self.el.tag + + # Check whether the section should be excluded + def is_excluded(self): + return self.el.is_excluded() + +# ----------------------------------------------------------------------------- + +# HTML parser +class Parser(HTMLParser): + """ + This parser divides the given string of HTML into a list of sections, each + of which are preceded by a h1-h6 level heading. A white- and blacklist of + tags dictates which tags should be preserved as part of the index, and + which should be ignored in their entirety. + """ + + # Initialize HTML parser + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Tags to skip + self.skip = set([ + "object", # Objects + "script", # Scripts + "style" # Styles + ]) + + # Tags to keep + self.keep = set([ + "p", # Paragraphs + "code", "pre", # Code blocks + "li", "ol", "ul", # Lists + "sub", "sup" # Sub- and superscripts + ]) + + # Current context and section + self.context = [] + self.section = None + + # All parsed sections + self.data = [] + + # Called at the start of every HTML tag + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + + # Ignore self-closing tags + el = Element(tag, attrs) + if not tag in void: + self.context.append(el) + else: + return + + # Handle heading + if tag in ([f"h{x}" for x in range(1, 7)]): + depth = len(self.context) + if "id" in attrs: + + # Ensure top-level section + if tag != "h1" and not self.data: + self.section = Section(Element("hx"), depth) + self.data.append(self.section) + + # Set identifier, if not first section + self.section = Section(el, depth) + if self.data: + self.section.id = attrs["id"] + + # Append section to list + self.data.append(self.section) + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle special cases to skip + for key, value in attrs.items(): + + # Skip block if explicitly excluded from search + if key == "data-search-exclude": + self.skip.add(el) + return + + # Skip line numbers - see https://bit.ly/3GvubZx + if key == "class" and value == "linenodiv": + self.skip.add(el) + return + + # Render opening tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + + # Check whether we're inside the section title + data = self.section.text + if self.section.el in self.context: + data = self.section.title + + # Append to section title or text + data.append(f"<{tag}>") + + # Called at the end of every HTML tag + def handle_endtag(self, tag): + if not self.context or self.context[-1] != tag: + return + + # Check whether we're exiting the current context, which happens when + # a headline is nested in another element. In that case, we close the + # current section, continuing to append data to the previous section, + # which could also be a nested section – see https://bit.ly/3IxxIJZ + if self.section.depth > len(self.context): + for section in reversed(self.data): + if section.depth <= len(self.context): + + # Set depth to infinity in order to denote that the current + # section is exited and must never be considered again. + self.section.depth = float("inf") + self.section = section + break + + # Remove element from skip list + el = self.context.pop() + if el in self.skip: + if el.tag not in ["script", "style", "object"]: + self.skip.remove(el) + return + + # Render closing tag if kept + if not self.skip.intersection(self.context): + if tag in self.keep: + + # Check whether we're inside the section title + data = self.section.text + if self.section.el in self.context: + data = self.section.title + + # Search for corresponding opening tag + index = data.index(f"<{tag}>") + for i in range(index + 1, len(data)): + if not data[i].isspace(): + index = len(data) + break + + # Remove element if empty (or only whitespace) + if len(data) > index: + while len(data) > index: + data.pop() + + # Append to section title or text + else: + data.append(f"") + + # Called for the text contents of each tag + def handle_data(self, data): + if self.skip.intersection(self.context): + return + + # Collapse whitespace in non-pre contexts + if not "pre" in self.context: + if not data.isspace(): + data = data.replace("\n", " ") + else: + data = " " + + # Handle preface - ensure top-level section + if not self.section: + self.section = Section(Element("hx")) + self.data.append(self.section) + + # Handle section headline + if self.section.el in self.context: + permalink = False + for el in self.context: + if el.tag == "a" and el.attrs.get("class") == "headerlink": + permalink = True + + # Ignore permalinks + if not permalink: + self.section.title.append( + escape(data, quote = False) + ) + + # Collapse adjacent whitespace + elif data.isspace(): + if not self.section.text or not self.section.text[-1].isspace(): + self.section.text.append(data) + elif "pre" in self.context: + self.section.text.append(data) + + # Handle everything else + else: + self.section.text.append( + escape(data, quote = False) + ) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.search") + +# Tags that are self-closing +void = set([ + "area", # Image map areas + "base", # Document base + "br", # Line breaks + "col", # Table columns + "embed", # External content + "hr", # Horizontal rules + "img", # Images + "input", # Input fields + "link", # Links + "meta", # Metadata + "param", # External parameters + "source", # Image source sets + "track", # Text track + "wbr" # Line break opportunities +]) diff --git a/docs/src/plugins/social/__init__.py b/docs/src/plugins/social/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/plugins/social/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/plugins/social/config.py b/docs/src/plugins/social/config.py new file mode 100644 index 00000000..2d87c25e --- /dev/null +++ b/docs/src/plugins/social/config.py @@ -0,0 +1,48 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from mkdocs.config.base import Config +from mkdocs.config.config_options import Deprecated, Type + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Social plugin configuration +class SocialConfig(Config): + enabled = Type(bool, default = True) + cache_dir = Type(str, default = ".cache/plugin/social") + + # Settings for social cards + cards = Type(bool, default = True) + cards_dir = Type(str, default = "assets/images/social") + cards_layout_options = Type(dict, default = {}) + + # Deprecated settings + cards_color = Deprecated( + option_type = Type(dict, default = {}), + message = + "Deprecated, use 'cards_layout_options.background_color' " + "and 'cards_layout_options.color' with 'default' layout" + ) + cards_font = Deprecated( + option_type = Type(str), + message = "Deprecated, use 'cards_layout_options.font_family'" + ) diff --git a/docs/src/plugins/social/plugin.py b/docs/src/plugins/social/plugin.py new file mode 100644 index 00000000..3cdfa3ce --- /dev/null +++ b/docs/src/plugins/social/plugin.py @@ -0,0 +1,516 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# ----------------------------------------------------------------------------- +# Disclaimer +# ----------------------------------------------------------------------------- +# Please note: this version of the social plugin is not actively development +# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of +# the plugin which is much more powerful and addresses all shortcomings of +# this implementation. Additionally, the new social plugin allows to create +# entirely custom social cards. You can probably imagine, that this was a lot +# of work to pull off. If you run into problems, or want to have additional +# functionality, please consider sponsoring the project. You can then use the +# new version of the plugin immediately. +# ----------------------------------------------------------------------------- + +import concurrent.futures +import functools +import logging +import os +import posixpath +import re +import requests +import sys + +from collections import defaultdict +from hashlib import md5 +from io import BytesIO +from mkdocs.commands.build import DuplicateFilter +from mkdocs.exceptions import PluginError +from mkdocs.plugins import BasePlugin +from shutil import copyfile +from tempfile import TemporaryFile +from zipfile import ZipFile +try: + from cairosvg import svg2png + from PIL import Image, ImageDraw, ImageFont +except ImportError: + pass + +from .config import SocialConfig + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Social plugin +class SocialPlugin(BasePlugin[SocialConfig]): + + def __init__(self): + self._executor = concurrent.futures.ThreadPoolExecutor(4) + + # Retrieve configuration + def on_config(self, config): + self.color = colors.get("indigo") + self.config.cards = self.config.enabled + if not self.config.cards: + return + + # Check dependencies + if "Image" not in globals(): + raise PluginError( + "Required dependencies of \"social\" plugin not found. " + "Install with: pip install \"mkdocs-material[imaging]\"" + ) + + # Move color options + if self.config.cards_color: + + # Move background color to new option + value = self.config.cards_color.get("fill") + if value: + self.config.cards_layout_options["background_color"] = value + + # Move color to new option + value = self.config.cards_color.get("text") + if value: + self.config.cards_layout_options["color"] = value + + # Move font family to new option + if self.config.cards_font: + value = self.config.cards_font + self.config.cards_layout_options["font_family"] = value + + # Check if site URL is defined + if not config.site_url: + log.warning( + "The \"site_url\" option is not set. The cards are generated, " + "but not linked, so they won't be visible on social media." + ) + + # Ensure presence of cache directory + self.cache = self.config.cache_dir + if not os.path.isdir(self.cache): + os.makedirs(self.cache) + + # Retrieve palette from theme configuration + theme = config.theme + if "palette" in theme: + palette = theme["palette"] + + # Use first palette, if multiple are defined + if isinstance(palette, list): + palette = palette[0] + + # Set colors according to palette + if "primary" in palette and palette["primary"]: + primary = palette["primary"].replace(" ", "-") + self.color = colors.get(primary, self.color) + + # Retrieve color overrides + options = self.config.cards_layout_options + self.color = { + "fill": options.get("background_color", self.color["fill"]), + "text": options.get("color", self.color["text"]) + } + + # Retrieve logo and font + self._resized_logo_promise = self._executor.submit(self._load_resized_logo, config) + self.font = self._load_font(config) + + self._image_promises = [] + + # Create social cards + def on_page_markdown(self, markdown, page, config, files): + if not self.config.cards: + return + + # Resolve image directory + directory = self.config.cards_dir + file, _ = os.path.splitext(page.file.src_path) + + # Resolve path of image + path = "{}.png".format(os.path.join( + config.site_dir, + directory, + file + )) + + # Resolve path of image directory + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) + + # Compute site name + site_name = config.site_name + + # Compute page title and description + title = page.meta.get("title", page.title) + description = config.site_description or "" + if "description" in page.meta: + description = page.meta["description"] + + # Check type of meta title - see https://t.ly/m1Us + if not isinstance(title, str): + log.error( + f"Page meta title of page '{page.file.src_uri}' must be a " + f"string, but is of type \"{type(title)}\"." + ) + sys.exit(1) + + # Check type of meta description - see https://t.ly/m1Us + if not isinstance(description, str): + log.error( + f"Page meta description of '{page.file.src_uri}' must be a " + f"string, but is of type \"{type(description)}\"." + ) + sys.exit(1) + + # Generate social card if not in cache + hash = md5("".join([ + site_name, + str(title), + description + ]).encode("utf-8")) + file = os.path.join(self.cache, f"{hash.hexdigest()}.png") + self._image_promises.append(self._executor.submit( + self._cache_image, + cache_path = file, dest_path = path, + render_function = lambda: self._render_card(site_name, title, description) + )) + + # Inject meta tags into page + meta = page.meta.get("meta", []) + page.meta["meta"] = meta + self._generate_meta(page, config) + + def on_post_build(self, config): + if not self.config.cards: + return + + # Check for exceptions + for promise in self._image_promises: + promise.result() + + # ------------------------------------------------------------------------- + + # Render image to cache (if not present), then copy from cache to site + def _cache_image(self, cache_path, dest_path, render_function): + if not os.path.isfile(cache_path): + image = render_function() + image.save(cache_path) + + # Copy file from cache + copyfile(cache_path, dest_path) + + @functools.lru_cache(maxsize=None) + def _get_font(self, kind, size): + return ImageFont.truetype(self.font[kind], size) + + # Render social card + def _render_card(self, site_name, title, description): + # Render background and logo + image = self._render_card_background((1200, 630), self.color["fill"]) + image.alpha_composite( + self._resized_logo_promise.result(), + (1200 - 228, 64 - 4) + ) + + # Render site name + font = self._get_font("Bold", 36) + image.alpha_composite( + self._render_text((826, 48), font, site_name, 1, 20), + (64 + 4, 64) + ) + + # Render page title + font = self._get_font("Bold", 92) + image.alpha_composite( + self._render_text((826, 328), font, title, 3, 30), + (64, 160) + ) + + # Render page description + font = self._get_font("Regular", 28) + image.alpha_composite( + self._render_text((826, 80), font, description, 2, 14), + (64 + 4, 512) + ) + + # Return social card image + return image + + # Render social card background + def _render_card_background(self, size, fill): + return Image.new(mode = "RGBA", size = size, color = fill) + + @functools.lru_cache(maxsize=None) + def _tmp_context(self): + image = Image.new(mode = "RGBA", size = (50, 50)) + return ImageDraw.Draw(image) + + @functools.lru_cache(maxsize=None) + def _text_bounding_box(self, text, font): + return self._tmp_context().textbbox((0, 0), text, font = font) + + # Render social card text + def _render_text(self, size, font, text, lmax, spacing = 0): + width = size[0] + lines, words = [], [] + + # Remove remnant HTML tags + text = re.sub(r"(<[^>]+>)", "", text) + + # Retrieve y-offset of textbox to correct for spacing + yoffset = 0 + + # Create drawing context and split text into lines + for word in text.split(" "): + combine = " ".join(words + [word]) + textbox = self._text_bounding_box(combine, font = font) + yoffset = textbox[1] + if not words or textbox[2] <= width: + words.append(word) + else: + lines.append(words) + words = [word] + + # Join words for each line and create image + lines.append(words) + lines = [" ".join(line) for line in lines] + image = Image.new(mode = "RGBA", size = size) + + # Create drawing context and split text into lines + context = ImageDraw.Draw(image) + context.text( + (0, spacing / 2 - yoffset), "\n".join(lines[:lmax]), + font = font, fill = self.color["text"], spacing = spacing - yoffset + ) + + # Return text image + return image + + # ------------------------------------------------------------------------- + + # Generate meta tags + def _generate_meta(self, page, config): + directory = self.config.cards_dir + file, _ = os.path.splitext(page.file.src_uri) + + # Compute page title + title = page.meta.get("title", page.title) + if not page.is_homepage: + title = f"{title} - {config.site_name}" + + # Compute page description + description = config.site_description + if "description" in page.meta: + description = page.meta["description"] + + # Resolve image URL + url = "{}.png".format(posixpath.join( + config.site_url or ".", + directory, + file + )) + + # Ensure forward slashes + url = url.replace(os.path.sep, "/") + + # Return meta tags + return [ + + # Meta tags for Open Graph + { "property": "og:type", "content": "website" }, + { "property": "og:title", "content": title }, + { "property": "og:description", "content": description }, + { "property": "og:image", "content": url }, + { "property": "og:image:type", "content": "image/png" }, + { "property": "og:image:width", "content": "1200" }, + { "property": "og:image:height", "content": "630" }, + { "property": "og:url", "content": page.canonical_url }, + + # Meta tags for Twitter + { "name": "twitter:card", "content": "summary_large_image" }, + # { "name": "twitter:site", "content": user }, + # { "name": "twitter:creator", "content": user }, + { "name": "twitter:title", "content": title }, + { "name": "twitter:description", "content": description }, + { "name": "twitter:image", "content": url } + ] + + def _load_resized_logo(self, config, width = 144): + logo = self._load_logo(config) + height = int(width * logo.height / logo.width) + return logo.resize((width, height)) + + # Retrieve logo image or icon + def _load_logo(self, config): + theme = config.theme + + # Handle images (precedence over icons) + if "logo" in theme: + _, extension = os.path.splitext(theme["logo"]) + + path = os.path.join(config.docs_dir, theme["logo"]) + + # Allow users to put the logo inside their custom_dir (theme["logo"] case) + if theme.custom_dir: + custom_dir_logo = os.path.join(theme.custom_dir, theme["logo"]) + if os.path.exists(custom_dir_logo): + path = custom_dir_logo + + # Load SVG and convert to PNG + if extension == ".svg": + return self._load_logo_svg(path) + + # Load PNG, JPEG, etc. + return Image.open(path).convert("RGBA") + + # Handle icons + icon = theme["icon"] or {} + if "logo" in icon and icon["logo"]: + logo = icon["logo"] + else: + logo = "material/library" + + # Resolve path of package + base = os.path.abspath(os.path.join( + os.path.dirname(__file__), + "../.." + )) + + path = f"{base}/templates/.icons/{logo}.svg" + + # Allow users to put the logo inside their custom_dir (theme["icon"]["logo"] case) + if theme.custom_dir: + custom_dir_logo = os.path.join(theme.custom_dir, ".icons", f"{logo}.svg") + if os.path.exists(custom_dir_logo): + path = custom_dir_logo + + # Load icon data and fill with color + return self._load_logo_svg(path, self.color["text"]) + + # Load SVG file and convert to PNG + def _load_logo_svg(self, path, fill = None): + file = BytesIO() + data = open(path).read() + + # Fill with color, if given + if fill: + data = data.replace(" + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + +# Casefold a string for comparison when sorting +def casefold(tag: str): + return tag.casefold() diff --git a/docs/src/plugins/tags/config.py b/docs/src/plugins/tags/config.py new file mode 100644 index 00000000..f2d95084 --- /dev/null +++ b/docs/src/plugins/tags/config.py @@ -0,0 +1,38 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +from functools import partial +from markdown.extensions.toc import slugify +from mkdocs.config.config_options import Optional, Type +from mkdocs.config.base import Config + +from . import casefold + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Tags plugin configuration +class TagsConfig(Config): + enabled = Type(bool, default = True) + + # Settings for tags + tags = Type(bool, default = True) + tags_file = Optional(Type(str)) diff --git a/docs/src/plugins/tags/plugin.py b/docs/src/plugins/tags/plugin.py new file mode 100644 index 00000000..e5ce6bde --- /dev/null +++ b/docs/src/plugins/tags/plugin.py @@ -0,0 +1,182 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import logging +import sys + +from collections import defaultdict +from markdown.extensions.toc import slugify +from mkdocs import utils +from mkdocs.plugins import BasePlugin + +# deprecated, but kept for downward compatibility. Use 'material.plugins.tags' +# as an import source instead. This import is removed in the next major version. +from . import casefold +from .config import TagsConfig + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# Tags plugin +class TagsPlugin(BasePlugin[TagsConfig]): + supports_multiple_instances = True + + # Initialize plugin + def on_config(self, config): + if not self.config.enabled: + return + + # Skip if tags should not be built + if not self.config.tags: + return + + # Initialize tags + self.tags = defaultdict(list) + self.tags_file = None + + # Retrieve tags mapping from configuration + self.tags_map = config.extra.get("tags") + + # Use override of slugify function + toc = { "slugify": slugify, "separator": "-" } + if "toc" in config.mdx_configs: + toc = { **toc, **config.mdx_configs["toc"] } + + # Partially apply slugify function + self.slugify = lambda value: ( + toc["slugify"](str(value), toc["separator"]) + ) + + # Hack: 2nd pass for tags index page(s) + def on_nav(self, nav, config, files): + if not self.config.enabled: + return + + # Skip if tags should not be built + if not self.config.tags: + return + + # Resolve tags index page + file = self.config.tags_file + if file: + self.tags_file = self._get_tags_file(files, file) + + # Build and render tags index page + def on_page_markdown(self, markdown, page, config, files): + if not self.config.enabled: + return + + # Skip if tags should not be built + if not self.config.tags: + return + + # Skip, if page is excluded + if page.file.inclusion.is_excluded(): + return + + # Render tags index page + if page.file == self.tags_file: + return self._render_tag_index(markdown) + + # Add page to tags index + for tag in page.meta.get("tags", []): + self.tags[tag].append(page) + + # Inject tags into page (after search and before minification) + def on_page_context(self, context, page, config, nav): + if not self.config.enabled: + return + + # Skip if tags should not be built + if not self.config.tags: + return + + # Provide tags for page + if "tags" in page.meta: + context["tags"] = [ + self._render_tag(tag) + for tag in page.meta["tags"] + ] + + # ------------------------------------------------------------------------- + + # Obtain tags file + def _get_tags_file(self, files, path): + file = files.get_file_from_path(path) + if not file: + log.error(f"Tags file '{path}' does not exist.") + sys.exit(1) + + # Add tags file to files + files.append(file) + return file + + # Render tags index + def _render_tag_index(self, markdown): + if not "[TAGS]" in markdown: + markdown += "\n[TAGS]" + + # Replace placeholder in Markdown with rendered tags index + return markdown.replace("[TAGS]", "\n".join([ + self._render_tag_links(*args) + for args in sorted(self.tags.items()) + ])) + + # Render the given tag and links to all pages with occurrences + def _render_tag_links(self, tag, pages): + classes = ["md-tag"] + if isinstance(self.tags_map, dict): + classes.append("md-tag-icon") + type = self.tags_map.get(tag) + if type: + classes.append(f"md-tag--{type}") + + # Render section for tag and a link to each page + classes = " ".join(classes) + content = [f"## {tag}", ""] + for page in pages: + url = utils.get_relative_url( + page.file.src_uri, + self.tags_file.src_uri + ) + + # Render link to page + title = page.meta.get("title", page.title) + content.append(f"- [{title}]({url})") + + # Return rendered tag links + return "\n".join(content) + + # Render the given tag, linking to the tags index (if enabled) + def _render_tag(self, tag): + type = self.tags_map.get(tag) if self.tags_map else None + if not self.tags_file or not self.slugify: + return dict(name = tag, type = type) + else: + url = f"{self.tags_file.url}#{self.slugify(tag)}" + return dict(name = tag, type = type, url = url) + +# ----------------------------------------------------------------------------- +# Data +# ----------------------------------------------------------------------------- + +# Set up logging +log = logging.getLogger("mkdocs.material.tags") diff --git a/docs/src/templates/.icons/logo.afdesign b/docs/src/templates/.icons/logo.afdesign new file mode 100644 index 00000000..07f57d0a Binary files /dev/null and b/docs/src/templates/.icons/logo.afdesign differ diff --git a/docs/src/templates/.icons/logo.svg b/docs/src/templates/.icons/logo.svg new file mode 100644 index 00000000..763eb2c2 --- /dev/null +++ b/docs/src/templates/.icons/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/src/templates/404.html b/docs/src/templates/404.html new file mode 100644 index 00000000..e87e7783 --- /dev/null +++ b/docs/src/templates/404.html @@ -0,0 +1,28 @@ + + +{% extends "main.html" %} + + +{% block content %} +

    404 - Not found

    +{% endblock %} diff --git a/docs/src/templates/__init__.py b/docs/src/templates/__init__.py new file mode 100644 index 00000000..d1899378 --- /dev/null +++ b/docs/src/templates/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. diff --git a/docs/src/templates/assets/images/favicon.png b/docs/src/templates/assets/images/favicon.png new file mode 100644 index 00000000..1cf13b9f Binary files /dev/null and b/docs/src/templates/assets/images/favicon.png differ diff --git a/docs/src/templates/assets/javascripts/_/index.ts b/docs/src/templates/assets/javascripts/_/index.ts new file mode 100644 index 00000000..be0f4a42 --- /dev/null +++ b/docs/src/templates/assets/javascripts/_/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { getElement, getLocation } from "~/browser" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Feature flag + */ +export type Flag = + | "announce.dismiss" /* Dismissable announcement bar */ + | "content.code.annotate" /* Code annotations */ + | "content.code.copy" /* Code copy button */ + | "content.lazy" /* Lazy content elements */ + | "content.tabs.link" /* Link content tabs */ + | "header.autohide" /* Hide header */ + | "navigation.expand" /* Automatic expansion */ + | "navigation.indexes" /* Section pages */ + | "navigation.instant" /* Instant navigation */ + | "navigation.instant.progress" /* Instant navigation progress */ + | "navigation.sections" /* Section navigation */ + | "navigation.tabs" /* Tabs navigation */ + | "navigation.tabs.sticky" /* Tabs navigation (sticky) */ + | "navigation.top" /* Back-to-top button */ + | "navigation.tracking" /* Anchor tracking */ + | "search.highlight" /* Search highlighting */ + | "search.share" /* Search sharing */ + | "search.suggest" /* Search suggestions */ + | "toc.follow" /* Following table of contents */ + | "toc.integrate" /* Integrated table of contents */ + +/* ------------------------------------------------------------------------- */ + +/** + * Translation + */ +export type Translation = + | "clipboard.copy" /* Copy to clipboard */ + | "clipboard.copied" /* Copied to clipboard */ + | "search.result.placeholder" /* Type to start searching */ + | "search.result.none" /* No matching documents */ + | "search.result.one" /* 1 matching document */ + | "search.result.other" /* # matching documents */ + | "search.result.more.one" /* 1 more on this page */ + | "search.result.more.other" /* # more on this page */ + | "search.result.term.missing" /* Missing */ + | "select.version" /* Version selector */ + +/** + * Translations + */ +export type Translations = + Record + +/* ------------------------------------------------------------------------- */ + +/** + * Versioning + */ +export interface Versioning { + provider: "mike" /* Version provider */ + default?: string | string[] /* Default version */ +} + +/** + * Configuration + */ +export interface Config { + base: string /* Base URL */ + features: Flag[] /* Feature flags */ + translations: Translations /* Translations */ + search: string /* Search worker URL */ + tags?: Record /* Tags mapping */ + version?: Versioning /* Versioning */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Retrieve global configuration and make base URL absolute + */ +const script = getElement("#__config") +const config: Config = JSON.parse(script.textContent!) +config.base = `${new URL(config.base, getLocation())}` + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve global configuration + * + * @returns Global configuration + */ +export function configuration(): Config { + return config +} + +/** + * Check whether a feature flag is enabled + * + * @param flag - Feature flag + * + * @returns Test result + */ +export function feature(flag: Flag): boolean { + return config.features.includes(flag) +} + +/** + * Retrieve the translation for the given key + * + * @param key - Key to be translated + * @param value - Positional value, if any + * + * @returns Translation + */ +export function translation( + key: Translation, value?: string | number +): string { + return typeof value !== "undefined" + ? config.translations[key].replace("#", value.toString()) + : config.translations[key] +} diff --git a/docs/src/templates/assets/javascripts/browser/document/index.ts b/docs/src/templates/assets/javascripts/browser/document/index.ts new file mode 100644 index 00000000..354c9b5c --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/document/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + const document$ = new ReplaySubject(1) + fromEvent(document, "DOMContentLoaded", { once: true }) + .subscribe(() => document$.next(document)) + + /* Return document */ + return document$ +} diff --git a/docs/src/templates/assets/javascripts/browser/element/_/.eslintrc b/docs/src/templates/assets/javascripts/browser/element/_/.eslintrc new file mode 100644 index 00000000..16973760 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/_/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "jsdoc/require-jsdoc": "off", + "jsdoc/require-returns-check": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/browser/element/_/index.ts b/docs/src/templates/assets/javascripts/browser/element/_/index.ts new file mode 100644 index 00000000..b7beb462 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/_/index.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T][] + +export function getElements( + selector: string, node?: ParentNode +): T[] + +export function getElements( + selector: string, node: ParentNode = document +): T[] { + return Array.from(node.querySelectorAll(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( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T] + +export function getElement( + selector: string, node?: ParentNode +): T + +export function getElement( + selector: string, node: ParentNode = document +): T { + const el = getOptionalElement(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( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T] | undefined + +export function getOptionalElement( + selector: string, node?: ParentNode +): T | undefined + +export function getOptionalElement( + selector: string, node: ParentNode = document +): T | undefined { + return node.querySelector(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/docs/src/templates/assets/javascripts/browser/element/focus/index.ts b/docs/src/templates/assets/javascripts/browser/element/focus/index.ts new file mode 100644 index 00000000..f31fe276 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/focus/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + debounceTime, + distinctUntilChanged, + fromEvent, + map, + merge, + shareReplay, + startWith +} from "rxjs" + +import { getActiveElement } from "../_" + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Focus observable + * + * Previously, this observer used `focus` and `blur` events to determine whether + * an element is focused, but this doesn't work if there are focusable elements + * within the elements itself. A better solutions are `focusin` and `focusout` + * events, which bubble up the tree and allow for more fine-grained control. + * + * `debounceTime` is necessary, because when a focus change happens inside an + * element, the observable would first emit `false` and then `true` again. + */ +const observer$ = merge( + fromEvent(document.body, "focusin"), + fromEvent(document.body, "focusout") +) + .pipe( + debounceTime(1), + startWith(undefined), + map(() => getActiveElement() || document.body), + shareReplay(1) + ) + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch element focus + * + * @param el - Element + * + * @returns Element focus observable + */ +export function watchElementFocus( + el: HTMLElement +): Observable { + return observer$ + .pipe( + map(active => el.contains(active)), + distinctUntilChanged() + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/element/index.ts b/docs/src/templates/assets/javascripts/browser/element/index.ts new file mode 100644 index 00000000..50ce84b2 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/element/offset/_/index.ts b/docs/src/templates/assets/javascripts/browser/element/offset/_/index.ts new file mode 100644 index 00000000..6dd229d5 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/offset/_/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + return merge( + fromEvent(window, "load"), + fromEvent(window, "resize") + ) + .pipe( + auditTime(0, animationFrameScheduler), + map(() => getElementOffset(el)), + startWith(getElementOffset(el)) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/element/offset/content/index.ts b/docs/src/templates/assets/javascripts/browser/element/offset/content/index.ts new file mode 100644 index 00000000..557301a6 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/offset/content/index.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + return merge( + fromEvent(el, "scroll"), + fromEvent(window, "resize") + ) + .pipe( + auditTime(0, animationFrameScheduler), + map(() => getElementContentOffset(el)), + startWith(getElementContentOffset(el)) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/element/offset/index.ts b/docs/src/templates/assets/javascripts/browser/element/offset/index.ts new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/offset/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/element/size/_/index.ts b/docs/src/templates/assets/javascripts/browser/element/size/_/index.ts new file mode 100644 index 00000000..35a5e68b --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/size/_/index.ts @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + NEVER, + Observable, + Subject, + defer, + filter, + finalize, + map, + merge, + of, + shareReplay, + startWith, + switchMap, + tap +} from "rxjs" + +import { watchScript } from "../../../script" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Element offset + */ +export interface ElementSize { + width: number /* Element width */ + height: number /* Element height */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Resize observer entry subject + */ +const entry$ = new Subject() + +/** + * Resize observer observable + * + * This observable will create a `ResizeObserver` on the first subscription + * and will automatically terminate it when there are no more subscribers. + * It's quite important to centralize observation in a single `ResizeObserver`, + * as the performance difference can be quite dramatic, as the link shows. + * + * If the browser doesn't have a `ResizeObserver` implementation available, a + * polyfill is automatically downloaded from unpkg.com. This is also compatible + * with the built-in privacy plugin, which will download the polyfill and put + * it alongside the built site for self-hosting. + * + * @see https://bit.ly/3iIYfEm - Google Groups on performance + */ +const observer$ = defer(() => ( + typeof ResizeObserver === "undefined" + ? watchScript("https://unpkg.com/resize-observer-polyfill") + : of(undefined) +)) + .pipe( + map(() => new ResizeObserver(entries => { + for (const entry of entries) + entry$.next(entry) + })), + switchMap(observer => merge(NEVER, of(observer)) + .pipe( + finalize(() => observer.disconnect()) + ) + ), + shareReplay(1) + ) + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element size + * + * @param el - Element + * + * @returns Element size + */ +export function getElementSize( + el: HTMLElement +): ElementSize { + return { + width: el.offsetWidth, + height: el.offsetHeight + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch element size + * + * This function returns an observable that subscribes to a single internal + * instance of `ResizeObserver` upon subscription, and emit resize events until + * termination. Note that this function should not be called with the same + * element twice, as the first unsubscription will terminate observation. + * + * Sadly, we can't use the `DOMRect` objects returned by the observer, because + * we need the emitted values to be consistent with `getElementSize`, which will + * return the used values (rounded) and not actual values (unrounded). Thus, we + * use the `offset*` properties. See the linked GitHub issue. + * + * @see https://bit.ly/3m0k3he - GitHub issue + * + * @param el - Element + * + * @returns Element size observable + */ +export function watchElementSize( + el: HTMLElement +): Observable { + 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/docs/src/templates/assets/javascripts/browser/element/size/content/index.ts b/docs/src/templates/assets/javascripts/browser/element/size/content/index.ts new file mode 100644 index 00000000..5ed388cf --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/size/content/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { ElementSize } from "../_" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element content size (= scroll width and height) + * + * @param el - Element + * + * @returns Element content size + */ +export function getElementContentSize( + el: HTMLElement +): ElementSize { + return { + width: el.scrollWidth, + height: el.scrollHeight + } +} + +/** + * Retrieve the overflowing container of an element, if any + * + * @param el - Element + * + * @returns Overflowing container or nothing + */ +export function getElementContainer( + el: HTMLElement +): HTMLElement | undefined { + let parent = el.parentElement + while (parent) + if ( + el.scrollWidth <= parent.scrollWidth && + el.scrollHeight <= parent.scrollHeight + ) + parent = (el = parent).parentElement + else + break + + /* Return overflowing container */ + return parent ? el : undefined +} diff --git a/docs/src/templates/assets/javascripts/browser/element/size/index.ts b/docs/src/templates/assets/javascripts/browser/element/size/index.ts new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/size/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/element/visibility/index.ts b/docs/src/templates/assets/javascripts/browser/element/visibility/index.ts new file mode 100644 index 00000000..1ffe0b8d --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/element/visibility/index.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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() + +/** + * 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 { + 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 { + 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/docs/src/templates/assets/javascripts/browser/index.ts b/docs/src/templates/assets/javascripts/browser/index.ts new file mode 100644 index 00000000..f1ee2bae --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/keyboard/index.ts b/docs/src/templates/assets/javascripts/browser/keyboard/index.ts new file mode 100644 index 00000000..783f2cda --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/keyboard/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + filter, + fromEvent, + map, + merge, + share, + startWith, + switchMap +} from "rxjs" + +import { getActiveElement } from "../element" +import { getToggle } from "../toggle" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Keyboard mode + */ +export type KeyboardMode = + | "global" /* Global */ + | "search" /* Search is open */ + +/* ------------------------------------------------------------------------- */ + +/** + * Keyboard + */ +export interface Keyboard { + mode: KeyboardMode /* Keyboard mode */ + type: string /* Key type */ + claim(): void /* Key claim */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Check whether an element may receive keyboard input + * + * @param el - Element + * @param type - Key type + * + * @returns Test result + */ +function isSusceptibleToKeyboard( + el: HTMLElement, type: string +): boolean { + switch (el.constructor) { + + /* Input elements */ + case HTMLInputElement: + /* @ts-expect-error - omit unnecessary type cast */ + if (el.type === "radio") + return /^Arrow/.test(type) + else + return true + + /* Select element and textarea */ + case HTMLSelectElement: + case HTMLTextAreaElement: + return true + + /* Everything else */ + default: + return el.isContentEditable + } +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch composition events + * + * @returns Composition observable + */ +export function watchComposition(): Observable { + return merge( + fromEvent(window, "compositionstart").pipe(map(() => true)), + fromEvent(window, "compositionend").pipe(map(() => false)) + ) + .pipe( + startWith(false) + ) +} + +/** + * Watch keyboard + * + * @returns Keyboard observable + */ +export function watchKeyboard(): Observable { + const keyboard$ = fromEvent(window, "keydown") + .pipe( + filter(ev => !(ev.metaKey || ev.ctrlKey)), + map(ev => ({ + mode: getToggle("search") ? "search" : "global", + type: ev.key, + claim() { + ev.preventDefault() + ev.stopPropagation() + } + } as Keyboard)), + filter(({ mode, type }) => { + if (mode === "global") { + const active = getActiveElement() + if (typeof active !== "undefined") + return !isSusceptibleToKeyboard(active, type) + } + return true + }), + share() + ) + + /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */ + return watchComposition() + .pipe( + switchMap(active => !active ? keyboard$ : EMPTY) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/location/_/index.ts b/docs/src/templates/assets/javascripts/browser/location/_/index.ts new file mode 100644 index 00000000..2672fa74 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/location/_/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { Subject } from "rxjs" + +import { feature } from "~/_" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve location + * + * This function returns a `URL` object (and not `Location`) to normalize the + * typings across the application. Furthermore, locations need to be tracked + * without setting them and `Location` is a singleton which represents the + * current location. + * + * @returns URL + */ +export function getLocation(): URL { + return new URL(location.href) +} + +/** + * Set location + * + * If instant navigation is enabled, this function creates a temporary anchor + * element, sets the `href` attribute, appends it to the body, clicks it, and + * then removes it again. The event will bubble up the DOM and trigger be + * intercepted by the instant loading business logic. + * + * Note that we must append and remove the anchor element, or the event will + * not bubble up the DOM, making it impossible to intercept it. + * + * @param url - URL to navigate to + * @param navigate - Force navigation + */ +export function setLocation( + url: URL | HTMLLinkElement, navigate = false +): void { + if (feature("navigation.instant") && !navigate) { + const el = h("a", { href: url.href }) + document.body.appendChild(el) + el.click() + el.remove() + + // If we're not using instant navigation, and the page should not be reloaded + // just instruct the browser to navigate to the given URL + } else { + location.href = url.href + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch location + * + * @returns Location subject + */ +export function watchLocation(): Subject { + return new Subject() +} diff --git a/docs/src/templates/assets/javascripts/browser/location/hash/index.ts b/docs/src/templates/assets/javascripts/browser/location/hash/index.ts new file mode 100644 index 00000000..5d3a134a --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/location/hash/index.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + filter, + fromEvent, + map, + merge, + shareReplay, + startWith +} from "rxjs" + +import { getOptionalElement } from "~/browser" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve location hash + * + * @returns Location hash + */ +export function getLocationHash(): string { + return location.hash.slice(1) +} + +/** + * Set location hash + * + * Setting a new fragment identifier via `location.hash` will have no effect + * if the value doesn't change. When a new fragment identifier is set, we want + * the browser to target the respective element at all times, which is why we + * use this dirty little trick. + * + * @param hash - Location hash + */ +export function setLocationHash(hash: string): void { + const el = h("a", { href: hash }) + el.addEventListener("click", ev => ev.stopPropagation()) + el.click() +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch location hash + * + * @param location$ - Location observable + * + * @returns Location hash observable + */ +export function watchLocationHash( + location$: Observable +): Observable { + return merge( + fromEvent(window, "hashchange"), + location$ + ) + .pipe( + map(getLocationHash), + startWith(getLocationHash()), + filter(hash => hash.length > 0), + shareReplay(1) + ) +} + +/** + * Watch location target + * + * @param location$ - Location observable + * + * @returns Location target observable + */ +export function watchLocationTarget( + location$: Observable +): Observable { + return watchLocationHash(location$) + .pipe( + map(id => getOptionalElement(`[id="${id}"]`)!), + filter(el => typeof el !== "undefined") + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/location/index.ts b/docs/src/templates/assets/javascripts/browser/location/index.ts new file mode 100644 index 00000000..d77a5444 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/location/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/media/index.ts b/docs/src/templates/assets/javascripts/browser/media/index.ts new file mode 100644 index 00000000..dd7400d4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/media/index.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + const media = matchMedia(query) + return fromEventPattern(next => ( + media.addListener(() => next(media.matches)) + )) + .pipe( + startWith(media.matches) + ) +} + +/** + * Watch print mode + * + * @returns Print observable + */ +export function watchPrint(): Observable { + 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( + query$: Observable, factory: () => Observable +): Observable { + return query$ + .pipe( + switchMap(active => active ? factory() : EMPTY) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/request/index.ts b/docs/src/templates/assets/javascripts/browser/request/index.ts new file mode 100644 index 00000000..74a56a64 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/request/index.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + map, + shareReplay, + switchMap +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Options + */ +interface Options { + progress$?: Subject // Progress subject +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Fetch the given URL + * + * If the request fails (e.g. when dispatched from `file://` locations), the + * observable will complete without emitting a value. + * + * @param url - Request URL + * @param options - Options + * + * @returns Response observable + */ +export function request( + url: URL | string, options?: Options +): Observable { + return new Observable(observer => { + const req = new XMLHttpRequest() + req.open("GET", `${url}`) + req.responseType = "blob" + + // Handle response + req.addEventListener("load", () => { + if (req.status >= 200 && req.status < 300) { + observer.next(req.response) + observer.complete() + } else { + observer.error(new Error(req.statusText)) + } + }) + + // Handle network errors + req.addEventListener("error", () => { + observer.error(new Error("Network Error")) + }) + + // Handle aborted requests + req.addEventListener("abort", () => { + observer.error(new Error("Request aborted")) + }) + + // Handle download progress + if (typeof options?.progress$ !== "undefined") { + req.addEventListener("progress", event => { + options.progress$!.next((event.loaded / event.total) * 100) + }) + + // Immediately set progress to 5% to indicate that we're loading + options.progress$.next(5) + } + + // Send request + req.send() + }) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Fetch JSON from the given URL + * + * @template T - Data type + * + * @param url - Request URL + * @param options - Options + * + * @returns Data observable + */ +export function requestJSON( + url: URL | string, options?: Options +): Observable { + return request(url, options) + .pipe( + switchMap(res => res.text()), + map(body => JSON.parse(body) as T), + shareReplay(1) + ) +} + +/** + * Fetch XML from the given URL + * + * @param url - Request URL + * @param options - Options + * + * @returns Data observable + */ +export function requestXML( + url: URL | string, options?: Options +): Observable { + 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/docs/src/templates/assets/javascripts/browser/script/index.ts b/docs/src/templates/assets/javascripts/browser/script/index.ts new file mode 100644 index 00000000..ef5c89e6 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/script/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + defer, + finalize, + fromEvent, + map, + merge, + switchMap, + take, + throwError +} from "rxjs" + +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create and load a `script` element + * + * This function returns an observable that will emit when the script was + * successfully loaded, or throw an error if it wasn't. + * + * @param src - Script URL + * + * @returns Script observable + */ +export function watchScript(src: string): Observable { + 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/docs/src/templates/assets/javascripts/browser/toggle/index.ts b/docs/src/templates/assets/javascripts/browser/toggle/index.ts new file mode 100644 index 00000000..0be4b29d --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/toggle/index.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 = { + 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 { + const el = toggles[name] + return fromEvent(el, "change") + .pipe( + map(() => el.checked), + startWith(el.checked) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/viewport/_/index.ts b/docs/src/templates/assets/javascripts/browser/viewport/_/index.ts new file mode 100644 index 00000000..09c45f32 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/viewport/_/index.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + return combineLatest([ + watchViewportOffset(), + watchViewportSize() + ]) + .pipe( + map(([offset, size]) => ({ offset, size })), + shareReplay(1) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/viewport/at/index.ts b/docs/src/templates/assets/javascripts/browser/viewport/at/index.ts new file mode 100644 index 00000000..8769cf3b --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/viewport/at/index.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + header$: Observable
    /* 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 { + 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/docs/src/templates/assets/javascripts/browser/viewport/index.ts b/docs/src/templates/assets/javascripts/browser/viewport/index.ts new file mode 100644 index 00000000..b3d135e9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/viewport/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/browser/viewport/offset/index.ts b/docs/src/templates/assets/javascripts/browser/viewport/offset/index.ts new file mode 100644 index 00000000..63d37dd2 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/viewport/offset/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + return merge( + fromEvent(window, "scroll", { passive: true }), + fromEvent(window, "resize", { passive: true }) + ) + .pipe( + map(getViewportOffset), + startWith(getViewportOffset()) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/viewport/size/index.ts b/docs/src/templates/assets/javascripts/browser/viewport/size/index.ts new file mode 100644 index 00000000..06694888 --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/viewport/size/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + return fromEvent(window, "resize", { passive: true }) + .pipe( + map(getViewportSize), + startWith(getViewportSize()) + ) +} diff --git a/docs/src/templates/assets/javascripts/browser/worker/index.ts b/docs/src/templates/assets/javascripts/browser/worker/index.ts new file mode 100644 index 00000000..12e4e63b --- /dev/null +++ b/docs/src/templates/assets/javascripts/browser/worker/index.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + endWith, + fromEvent, + ignoreElements, + mergeWith, + share, + takeUntil +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Worker message + */ +export interface WorkerMessage { + type: unknown /* Message type */ + data?: unknown /* Message data */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Create an observable for receiving from a web worker + * + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message observable + */ +function recv(worker: Worker): Observable { + return fromEvent, T>(worker, "message", ev => ev.data) +} + +/** + * Create a subject for sending to a web worker + * + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message subject + */ +function send(worker: Worker): Subject { + const send$ = new Subject() + send$.subscribe(data => worker.postMessage(data)) + + /* Return message subject */ + return send$ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a bidirectional communication channel to a web worker + * + * @template T - Data type + * + * @param url - Worker URL + * @param worker - Worker + * + * @returns Worker subject + */ +export function watchWorker( + url: string, worker = new Worker(url) +): Subject { + const recv$ = recv(worker) + const send$ = send(worker) + + /* Create worker subject and forward messages */ + const worker$ = new Subject() + worker$.subscribe(send$) + + /* Return worker subject */ + const done$ = send$.pipe(ignoreElements(), endWith(true)) + return worker$ + .pipe( + ignoreElements(), + mergeWith(recv$.pipe(takeUntil(done$))), + share() + ) as Subject +} diff --git a/docs/src/templates/assets/javascripts/bundle.ts b/docs/src/templates/assets/javascripts/bundle.ts new file mode 100644 index 00000000..141789c9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/bundle.ts @@ -0,0 +1,316 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import "focus-visible" + +import { + EMPTY, + NEVER, + Observable, + Subject, + defer, + delay, + filter, + map, + merge, + mergeWith, + shareReplay, + switchMap +} from "rxjs" + +import { configuration, feature } from "./_" +import { + at, + getActiveElement, + getOptionalElement, + requestJSON, + setLocation, + setToggle, + watchDocument, + watchKeyboard, + watchLocation, + watchLocationTarget, + watchMedia, + watchPrint, + watchScript, + watchViewport +} from "./browser" +import { + getComponentElement, + getComponentElements, + mountAnnounce, + mountBackToTop, + mountConsent, + mountContent, + mountDialog, + mountHeader, + mountHeaderTitle, + mountPalette, + mountProgress, + mountSearch, + mountSearchHiglight, + mountSidebar, + mountSource, + mountTableOfContents, + mountTabs, + watchHeader, + watchMain +} from "./components" +import { + SearchIndex, + setupClipboardJS, + setupInstantNavigation, + setupVersionSelector +} from "./integrations" +import { + patchIndeterminate, + patchScrollfix, + patchScrolllock +} from "./patches" +import "./polyfills" + +/* ---------------------------------------------------------------------------- + * Functions - @todo refactor + * ------------------------------------------------------------------------- */ + +/** + * Fetch search index + * + * @returns Search index observable + */ +function fetchSearchIndex(): Observable { + if (location.protocol === "file:") { + return watchScript( + `${new URL("search/search_index.js", config.base)}` + ) + .pipe( + // @ts-ignore - @todo fix typings + map(() => __index), + shareReplay(1) + ) + } else { + return requestJSON( + new URL("search/search_index.json", config.base) + ) + } +} + +/* ---------------------------------------------------------------------------- + * Application + * ------------------------------------------------------------------------- */ + +/* Yay, JavaScript is available */ +document.documentElement.classList.remove("no-js") +document.documentElement.classList.add("js") + +/* Set up navigation observables and subjects */ +const document$ = watchDocument() +const location$ = watchLocation() +const target$ = watchLocationTarget(location$) +const keyboard$ = watchKeyboard() + +/* Set up media observables */ +const viewport$ = watchViewport() +const tablet$ = watchMedia("(min-width: 960px)") +const screen$ = watchMedia("(min-width: 1220px)") +const print$ = watchPrint() + +/* Retrieve search index, if search is enabled */ +const config = configuration() +const index$ = document.forms.namedItem("search") + ? fetchSearchIndex() + : NEVER + +/* Set up Clipboard.js integration */ +const alert$ = new Subject() +setupClipboardJS({ alert$ }) + +/* Set up progress indicator */ +const progress$ = new Subject() + +/* Set up instant navigation, if enabled */ +if (feature("navigation.instant")) + setupInstantNavigation({ location$, viewport$, progress$ }) + .subscribe(document$) + +/* Set up version selector */ +if (config.version?.provider === "mike") + setupVersionSelector({ document$ }) + +/* Always close drawer and search on navigation */ +merge(location$, target$) + .pipe( + delay(125) + ) + .subscribe(() => { + setToggle("drawer", false) + setToggle("search", false) + }) + +/* Set up global keyboard handlers */ +keyboard$ + .pipe( + filter(({ mode }) => mode === "global") + ) + .subscribe(key => { + switch (key.type) { + + /* Go to previous page */ + case "p": + case ",": + const prev = getOptionalElement("link[rel=prev]") + if (typeof prev !== "undefined") + setLocation(prev) + break + + /* Go to next page */ + case "n": + case ".": + const next = getOptionalElement("link[rel=next]") + if (typeof next !== "undefined") + setLocation(next) + break + + /* Expand navigation, see https://bit.ly/3ZjG5io */ + case "Enter": + const active = getActiveElement() + if (active instanceof HTMLLabelElement) + active.click() + } + }) + +/* Set up patches */ +patchIndeterminate({ document$, tablet$ }) +patchScrollfix({ document$ }) +patchScrolllock({ viewport$, tablet$ }) + +/* Set up header and main area observable */ +const header$ = watchHeader(getComponentElement("header"), { viewport$ }) +const main$ = document$ + .pipe( + map(() => getComponentElement("main")), + switchMap(el => watchMain(el, { viewport$, header$ })), + shareReplay(1) + ) + +/* Set up control component observables */ +const control$ = merge( + + /* Consent */ + ...getComponentElements("consent") + .map(el => mountConsent(el, { target$ })), + + /* Dialog */ + ...getComponentElements("dialog") + .map(el => mountDialog(el, { alert$ })), + + /* Header */ + ...getComponentElements("header") + .map(el => mountHeader(el, { viewport$, header$, main$ })), + + /* Color palette */ + ...getComponentElements("palette") + .map(el => mountPalette(el)), + + /* Progress bar */ + ...getComponentElements("progress") + .map(el => mountProgress(el, { progress$ })), + + /* Search */ + ...getComponentElements("search") + .map(el => mountSearch(el, { index$, keyboard$ })), + + /* Repository information */ + ...getComponentElements("source") + .map(el => mountSource(el)) +) + +/* Set up content component observables */ +const content$ = defer(() => merge( + + /* Announcement bar */ + ...getComponentElements("announce") + .map(el => mountAnnounce(el)), + + /* Content */ + ...getComponentElements("content") + .map(el => mountContent(el, { viewport$, target$, print$ })), + + /* Search highlighting */ + ...getComponentElements("content") + .map(el => feature("search.highlight") + ? mountSearchHiglight(el, { index$, location$ }) + : EMPTY + ), + + /* Header title */ + ...getComponentElements("header-title") + .map(el => mountHeaderTitle(el, { viewport$, header$ })), + + /* Sidebar */ + ...getComponentElements("sidebar") + .map(el => el.getAttribute("data-md-type") === "navigation" + ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ })) + : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ })) + ), + + /* Navigation tabs */ + ...getComponentElements("tabs") + .map(el => mountTabs(el, { viewport$, header$ })), + + /* Table of contents */ + ...getComponentElements("toc") + .map(el => mountTableOfContents(el, { + viewport$, header$, main$, target$ + })), + + /* Back-to-top button */ + ...getComponentElements("top") + .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ })) +)) + +/* Set up component observables */ +const component$ = document$ + .pipe( + switchMap(() => content$), + mergeWith(control$), + shareReplay(1) + ) + +/* Subscribe to all components */ +component$.subscribe() + +/* ---------------------------------------------------------------------------- + * Exports + * ------------------------------------------------------------------------- */ + +window.document$ = document$ /* Document observable */ +window.location$ = location$ /* Location subject */ +window.target$ = target$ /* Location target observable */ +window.keyboard$ = keyboard$ /* Keyboard observable */ +window.viewport$ = viewport$ /* Viewport observable */ +window.tablet$ = tablet$ /* Media tablet observable */ +window.screen$ = screen$ /* Media screen observable */ +window.print$ = print$ /* Media print observable */ +window.alert$ = alert$ /* Alert subject */ +window.progress$ = progress$ /* Progress indicator subject */ +window.component$ = component$ /* Component observable */ diff --git a/docs/src/templates/assets/javascripts/components/_/index.ts b/docs/src/templates/assets/javascripts/components/_/index.ts new file mode 100644 index 00000000..61c471d9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/_/index.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { getElement, getElements } from "~/browser" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Component type + */ +export type ComponentType = + | "announce" /* Announcement bar */ + | "container" /* Container */ + | "consent" /* Consent */ + | "content" /* Content */ + | "dialog" /* Dialog */ + | "header" /* Header */ + | "header-title" /* Header title */ + | "header-topic" /* Header topic */ + | "main" /* Main area */ + | "outdated" /* Version warning */ + | "palette" /* Color palette */ + | "progress" /* Progress indicator */ + | "search" /* Search */ + | "search-query" /* Search input */ + | "search-result" /* Search results */ + | "search-share" /* Search sharing */ + | "search-suggest" /* Search suggestions */ + | "sidebar" /* Sidebar */ + | "skip" /* Skip link */ + | "source" /* Repository information */ + | "tabs" /* Navigation tabs */ + | "toc" /* Table of contents */ + | "top" /* Back-to-top button */ + +/** + * Component + * + * @template T - Component type + * @template U - Reference type + */ +export type Component< + T extends {} = {}, + U extends HTMLElement = HTMLElement +> = + T & { + ref: U /* Component reference */ + } + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Component type map + */ +interface ComponentTypeMap { + "announce": HTMLElement /* Announcement bar */ + "container": HTMLElement /* Container */ + "consent": HTMLElement /* Consent */ + "content": HTMLElement /* Content */ + "dialog": HTMLElement /* Dialog */ + "header": HTMLElement /* Header */ + "header-title": HTMLElement /* Header title */ + "header-topic": HTMLElement /* Header topic */ + "main": HTMLElement /* Main area */ + "outdated": HTMLElement /* Version warning */ + "palette": HTMLElement /* Color palette */ + "progress": HTMLElement /* Progress indicator */ + "search": HTMLElement /* Search */ + "search-query": HTMLInputElement /* Search input */ + "search-result": HTMLElement /* Search results */ + "search-share": HTMLAnchorElement /* Search sharing */ + "search-suggest": HTMLElement /* Search suggestions */ + "sidebar": HTMLElement /* Sidebar */ + "skip": HTMLAnchorElement /* Skip link */ + "source": HTMLAnchorElement /* Repository information */ + "tabs": HTMLElement /* Navigation tabs */ + "toc": HTMLElement /* Table of contents */ + "top": HTMLAnchorElement /* Back-to-top button */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve the element for a given component or throw a reference error + * + * @template T - Component type + * + * @param type - Component type + * @param node - Node of reference + * + * @returns Element + */ +export function getComponentElement( + 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( + type: T, node: ParentNode = document +): ComponentTypeMap[T][] { + return getElements(`[data-md-component=${type}]`, node) +} diff --git a/docs/src/templates/assets/javascripts/components/announce/index.ts b/docs/src/templates/assets/javascripts/components/announce/index.ts new file mode 100644 index 00000000..dd04b4ff --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/announce/index.ts @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + Subject, + defer, + finalize, + fromEvent, + map, + tap +} from "rxjs" + +import { feature } from "~/_" +import { getElement } from "~/browser" + +import { Component } from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Announcement bar + */ +export interface Announce { + hash: number /* Content hash */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch announcement bar + * + * @param el - Announcement bar element + * + * @returns Announcement bar observable + */ +export function watchAnnounce( + el: HTMLElement +): Observable { + 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> { + if (!feature("announce.dismiss") || !el.childElementCount) + return EMPTY + + /* Support instant navigation - see https://t.ly/3FTme */ + if (!el.hidden) { + const content = getElement(".md-typeset", el) + if (__md_hash(content.innerHTML) === __md_get("__announce")) + el.hidden = true + } + + /* Mount component on subscription */ + return defer(() => { + const push$ = new Subject() + push$.subscribe(({ hash }) => { + el.hidden = true + + /* Persist preference in local storage */ + __md_set("__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/docs/src/templates/assets/javascripts/components/consent/index.ts b/docs/src/templates/assets/javascripts/components/consent/index.ts new file mode 100644 index 00000000..bc99db58 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/consent/index.ts @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + finalize, + map, + tap +} from "rxjs" + +import { Component } from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Consent + */ +export interface Consent { + hidden: boolean /* Consent is hidden */ +} + +/** + * Consent defaults + */ +export interface ConsentDefaults { + analytics?: boolean /* Consent for Analytics */ + github?: boolean /* Consent for GitHub */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + target$: Observable /* Target observable */ +} + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Target observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch consent + * + * @param el - Consent element + * @param options - Options + * + * @returns Consent observable + */ +export function watchConsent( + el: HTMLElement, { target$ }: WatchOptions +): Observable { + 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> { + const internal$ = new Subject() + 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/docs/src/templates/assets/javascripts/components/content/_/index.ts b/docs/src/templates/assets/javascripts/components/content/_/index.ts new file mode 100644 index 00000000..899a695c --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/_/index.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { Observable, merge } from "rxjs" + +import { Viewport, getElements } from "~/browser" + +import { Component } from "../../_" +import { + Annotation, + mountAnnotationBlock +} from "../annotation" +import { + CodeBlock, + mountCodeBlock +} from "../code" +import { + Details, + mountDetails +} from "../details" +import { + Mermaid, + mountMermaid +} from "../mermaid" +import { + DataTable, + mountDataTable +} from "../table" +import { + ContentTabs, + mountContentTabs +} from "../tabs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Content + */ +export type Content = + | Annotation + | CodeBlock + | ContentTabs + | DataTable + | Details + | Mermaid + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + target$: Observable /* Location target observable */ + print$: Observable /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount content + * + * This function mounts all components that are found in the content of the + * actual article, including code blocks, data tables and details. + * + * @param el - Content element + * @param options - Options + * + * @returns Content component observable + */ +export function mountContent( + el: HTMLElement, { viewport$, target$, print$ }: MountOptions +): Observable> { + return merge( + + /* Annotations */ + ...getElements(".annotate:not(.highlight)", el) + .map(child => mountAnnotationBlock(child, { target$, print$ })), + + /* Code blocks */ + ...getElements("pre:not(.mermaid) > code", el) + .map(child => mountCodeBlock(child, { target$, print$ })), + + /* Mermaid diagrams */ + ...getElements("pre.mermaid", el) + .map(child => mountMermaid(child)), + + /* Data tables */ + ...getElements("table:not([class])", el) + .map(child => mountDataTable(child)), + + /* Details */ + ...getElements("details", el) + .map(child => mountDetails(child, { target$, print$ })), + + /* Content tabs */ + ...getElements("[data-tabs]", el) + .map(child => mountContentTabs(child, { viewport$ })) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/content/annotation/_/index.ts b/docs/src/templates/assets/javascripts/components/content/annotation/_/index.ts new file mode 100644 index 00000000..c5138fa4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/annotation/_/index.ts @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + animationFrameScheduler, + auditTime, + combineLatest, + debounceTime, + defer, + delay, + endWith, + filter, + finalize, + fromEvent, + ignoreElements, + map, + merge, + switchMap, + take, + takeUntil, + tap, + throttleTime, + withLatestFrom +} from "rxjs" + +import { + ElementOffset, + getActiveElement, + getElementSize, + watchElementContentOffset, + watchElementFocus, + watchElementOffset, + watchElementVisibility +} from "~/browser" + +import { Component } from "../../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Annotation + */ +export interface Annotation { + active: boolean /* Annotation is active */ + offset: ElementOffset /* Annotation offset */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Location target observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch annotation + * + * @param el - Annotation element + * @param container - Containing element + * + * @returns Annotation observable + */ +export function watchAnnotation( + el: HTMLElement, container: HTMLElement +): Observable { + const offset$ = defer(() => combineLatest([ + watchElementOffset(el), + watchElementContentOffset(container) + ])) + .pipe( + map(([{ x, y }, scroll]): ElementOffset => { + const { width, height } = getElementSize(el) + return ({ + x: x - scroll.x + width / 2, + y: y - scroll.y + height / 2 + }) + }) + ) + + /* Actively watch annotation on focus */ + return watchElementFocus(el) + .pipe( + switchMap(active => offset$ + .pipe( + map(offset => ({ active, offset })), + take(+!active || Infinity) + ) + ) + ) +} + +/** + * Mount annotation + * + * @param el - Annotation element + * @param container - Containing element + * @param options - Options + * + * @returns Annotation component observable + */ +export function mountAnnotation( + el: HTMLElement, container: HTMLElement, { target$ }: MountOptions +): Observable> { + const [tooltip, index] = Array.from(el.children) + + /* Mount component on subscription */ + return defer(() => { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + push$.subscribe({ + + /* Handle emission */ + next({ offset }) { + el.style.setProperty("--md-tooltip-x", `${offset.x}px`) + el.style.setProperty("--md-tooltip-y", `${offset.y}px`) + }, + + /* Handle complete */ + complete() { + el.style.removeProperty("--md-tooltip-x") + el.style.removeProperty("--md-tooltip-y") + } + }) + + /* Start animation only when annotation is visible */ + watchElementVisibility(el) + .pipe( + takeUntil(done$) + ) + .subscribe(visible => { + el.toggleAttribute("data-md-visible", visible) + }) + + /* Toggle tooltip presence to mitigate empty lines when copying */ + merge( + push$.pipe(filter(({ active }) => active)), + push$.pipe(debounceTime(250), filter(({ active }) => !active)) + ) + .subscribe({ + + /* Handle emission */ + next({ active }) { + if (active) + el.prepend(tooltip) + else + tooltip.remove() + }, + + /* Handle complete */ + complete() { + el.prepend(tooltip) + } + }) + + /* Toggle tooltip visibility */ + push$ + .pipe( + auditTime(16, animationFrameScheduler) + ) + .subscribe(({ active }) => { + tooltip.classList.toggle("md-tooltip--active", active) + }) + + /* Track relative origin of tooltip */ + push$ + .pipe( + throttleTime(125, animationFrameScheduler), + filter(() => !!el.offsetParent), + map(() => el.offsetParent!.getBoundingClientRect()), + map(({ x }) => x) + ) + .subscribe({ + + /* Handle emission */ + next(origin) { + if (origin) + el.style.setProperty("--md-tooltip-0", `${-origin}px`) + else + el.style.removeProperty("--md-tooltip-0") + }, + + /* Handle complete */ + complete() { + el.style.removeProperty("--md-tooltip-0") + } + }) + + /* Allow to copy link without scrolling to anchor */ + fromEvent(index, "click") + .pipe( + takeUntil(done$), + filter(ev => !(ev.metaKey || ev.ctrlKey)) + ) + .subscribe(ev => { + ev.stopPropagation() + ev.preventDefault() + }) + + /* Allow to open link in new tab or blur on close */ + fromEvent(index, "mousedown") + .pipe( + takeUntil(done$), + withLatestFrom(push$) + ) + .subscribe(([ev, { active }]) => { + + /* Open in new tab */ + if (ev.button !== 0 || ev.metaKey || ev.ctrlKey) { + ev.preventDefault() + + /* Close annotation */ + } else if (active) { + ev.preventDefault() + + /* Focus parent annotation, if any */ + const parent = el.parentElement!.closest(".md-annotation") + if (parent instanceof HTMLElement) + parent.focus() + else + getActiveElement()?.blur() + } + }) + + /* Open and focus annotation on location target */ + target$ + .pipe( + takeUntil(done$), + filter(target => target === tooltip), + delay(125) + ) + .subscribe(() => el.focus()) + + /* Create and return component */ + return watchAnnotation(el, container) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/content/annotation/block/index.ts b/docs/src/templates/assets/javascripts/components/content/annotation/block/index.ts new file mode 100644 index 00000000..c73b01fa --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/annotation/block/index.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { EMPTY, Observable, defer } from "rxjs" + +import { Component } from "../../../_" +import { Annotation } from "../_" +import { mountAnnotationList } from "../list" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Location target observable */ + print$: Observable /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Find list element directly following a block + * + * @param el - Annotation block element + * + * @returns List element or nothing + */ +function findList(el: HTMLElement): HTMLElement | undefined { + if (el.nextElementSibling) { + const sibling = el.nextElementSibling as HTMLElement + if (sibling.tagName === "OL") + return sibling + + /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ + else if (sibling.tagName === "P" && !sibling.children.length) + return findList(sibling) + } + + /* Everything else */ + return undefined +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount annotation block + * + * @param el - Annotation block element + * @param options - Options + * + * @returns Annotation component observable + */ +export function mountAnnotationBlock( + el: HTMLElement, options: MountOptions +): Observable> { + return defer(() => { + const list = findList(el) + return typeof list !== "undefined" + ? mountAnnotationList(list, el, options) + : EMPTY + }) +} diff --git a/docs/src/templates/assets/javascripts/components/content/annotation/index.ts b/docs/src/templates/assets/javascripts/components/content/annotation/index.ts new file mode 100644 index 00000000..c593b723 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/annotation/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" +export * from "./block" +export * from "./list" diff --git a/docs/src/templates/assets/javascripts/components/content/annotation/list/index.ts b/docs/src/templates/assets/javascripts/components/content/annotation/list/index.ts new file mode 100644 index 00000000..725dd583 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/annotation/list/index.ts @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + Subject, + defer, + endWith, + finalize, + ignoreElements, + merge, + share, + takeUntil +} from "rxjs" + +import { + getElement, + getElements, + getOptionalElement +} from "~/browser" +import { renderAnnotation } from "~/templates" + +import { Component } from "../../../_" +import { + Annotation, + mountAnnotation +} from "../_" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Location target observable */ + print$: Observable /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Find all annotation hosts in the containing element + * + * @param container - Containing element + * + * @returns Annotation hosts + */ +function findHosts(container: HTMLElement): HTMLElement[] { + return container.tagName === "CODE" + ? getElements(".c, .c1, .cm", container) + : [container] +} + +/** + * Find all annotation markers in the containing element + * + * @param container - Containing element + * + * @returns Annotation markers + */ +function findMarkers(container: HTMLElement): Text[] { + const markers: Text[] = [] + for (const el of findHosts(container)) { + const nodes: Text[] = [] + + /* Find all text nodes in current element */ + const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) + for (let node = it.nextNode(); node; node = it.nextNode()) + nodes.push(node as Text) + + /* Find all markers in each text node */ + for (let text of nodes) { + let match: RegExpExecArray | null + + /* Split text at marker and add to list */ + while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) { + const [, id, force] = match + if (typeof force === "undefined") { + const marker = text.splitText(match.index) + text = marker.splitText(id.length) + markers.push(marker) + + /* Replace entire text with marker */ + } else { + text.textContent = id + markers.push(text) + break + } + } + } + } + return markers +} + +/** + * Swap the child nodes of two elements + * + * @param source - Source element + * @param target - Target element + */ +function swap(source: HTMLElement, target: HTMLElement): void { + target.append(...Array.from(source.childNodes)) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount annotation list + * + * This function analyzes the containing code block and checks for markers + * referring to elements in the given annotation list. If no markers are found, + * the list is left untouched. Otherwise, list elements are rendered as + * annotations inside the code block. + * + * @param el - Annotation list element + * @param container - Containing element + * @param options - Options + * + * @returns Annotation component observable + */ +export function mountAnnotationList( + el: HTMLElement, container: HTMLElement, { target$, print$ }: MountOptions +): Observable> { + + /* Compute prefix for tooltip anchors */ + const parent = container.closest("[id]") + const prefix = parent?.id + + /* Find and replace all markers with empty annotations */ + const annotations = new Map() + for (const marker of findMarkers(container)) { + const [, id] = marker.textContent!.match(/\((\d+)\)/)! + if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) { + annotations.set(id, renderAnnotation(id, prefix)) + marker.replaceWith(annotations.get(id)!) + } + } + + /* Keep list if there are no annotations to render */ + if (annotations.size === 0) + return EMPTY + + /* Mount component on subscription */ + return defer(() => { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + + /* Retrieve container pairs for swapping */ + const pairs: [HTMLElement, HTMLElement][] = [] + for (const [id, annotation] of annotations) + pairs.push([ + getElement(".md-typeset", annotation), + getElement(`:scope > li:nth-child(${id})`, el) + ]) + + /* Handle print mode - see https://bit.ly/3rgPdpt */ + print$.pipe(takeUntil(done$)) + .subscribe(active => { + el.hidden = !active + + /* Add class to discern list element */ + el.classList.toggle("md-annotation-list", active) + + /* Show annotations in code block or list (print) */ + for (const [inner, child] of pairs) + if (!active) + swap(child, inner) + else + swap(inner, child) + }) + + /* Create and return component */ + return merge(...[...annotations] + .map(([, annotation]) => ( + mountAnnotation(annotation, container, { target$ }) + )) + ) + .pipe( + finalize(() => push$.complete()), + share() + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/content/code/_/index.ts b/docs/src/templates/assets/javascripts/components/content/code/_/index.ts new file mode 100644 index 00000000..ccc09339 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/code/_/index.ts @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import ClipboardJS from "clipboard" +import { + EMPTY, + Observable, + Subject, + defer, + distinctUntilChanged, + distinctUntilKeyChanged, + filter, + finalize, + map, + mergeWith, + switchMap, + take, + tap +} from "rxjs" + +import { feature } from "~/_" +import { + getElementContentSize, + watchElementSize, + watchElementVisibility +} from "~/browser" +import { renderClipboardButton } from "~/templates" + +import { Component } from "../../../_" +import { + Annotation, + mountAnnotationList +} from "../../annotation" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Code block + */ +export interface CodeBlock { + scrollable: boolean /* Code block overflows */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Location target observable */ + print$: Observable /* Media print observable */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Global sequence number for code blocks + */ +let sequence = 0 + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Find candidate list element directly following a code block + * + * @param el - Code block element + * + * @returns List element or nothing + */ +function findCandidateList(el: HTMLElement): HTMLElement | undefined { + if (el.nextElementSibling) { + const sibling = el.nextElementSibling as HTMLElement + if (sibling.tagName === "OL") + return sibling + + /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ + else if (sibling.tagName === "P" && !sibling.children.length) + return findCandidateList(sibling) + } + + /* Everything else */ + return undefined +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch code block + * + * This function monitors size changes of the viewport, as well as switches of + * content tabs with embedded code blocks, as both may trigger overflow. + * + * @param el - Code block element + * + * @returns Code block observable + */ +export function watchCodeBlock( + el: HTMLElement +): Observable { + return watchElementSize(el) + .pipe( + map(({ width }) => { + const content = getElementContentSize(el) + return { + scrollable: content.width > width + } + }), + distinctUntilKeyChanged("scrollable") + ) +} + +/** + * Mount code block + * + * This function ensures that an overflowing code block is focusable through + * keyboard, so it can be scrolled without a mouse to improve on accessibility. + * Furthermore, if code annotations are enabled, they are mounted if and only + * if the code block is currently visible, e.g., not in a hidden content tab. + * + * Note that code blocks may be mounted eagerly or lazily. If they're mounted + * lazily (on first visibility), code annotation anchor links will not work, + * as they are evaluated on initial page load, and code annotations in general + * might feel a little bumpier. + * + * @param el - Code block element + * @param options - Options + * + * @returns Code block and annotation component observable + */ +export function mountCodeBlock( + el: HTMLElement, options: MountOptions +): Observable> { + const { matches: hover } = matchMedia("(hover)") + + /* Defer mounting of code block - see https://bit.ly/3vHVoVD */ + const factory$ = defer(() => { + const push$ = new Subject() + push$.subscribe(({ scrollable }) => { + if (scrollable && hover) + el.setAttribute("tabindex", "0") + else + el.removeAttribute("tabindex") + }) + + /* Render button for Clipboard.js integration */ + if (ClipboardJS.isSupported()) { + if (el.closest(".copy") || ( + feature("content.code.copy") && !el.closest(".no-copy") + )) { + const parent = el.closest("pre")! + parent.id = `__code_${sequence++}` + parent.insertBefore( + renderClipboardButton(parent.id), + el + ) + } + } + + /* Handle code annotations */ + const container = el.closest(".highlight") + if (container instanceof HTMLElement) { + const list = findCandidateList(container) + + /* Mount code annotations, if enabled */ + if (typeof list !== "undefined" && ( + container.classList.contains("annotate") || + feature("content.code.annotate") + )) { + const annotations$ = mountAnnotationList(list, el, options) + + /* Create and return component */ + return watchCodeBlock(el) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })), + mergeWith( + watchElementSize(container) + .pipe( + map(({ width, height }) => width && height), + distinctUntilChanged(), + switchMap(active => active ? annotations$ : EMPTY) + ) + ) + ) + } + } + + /* Create and return component */ + return watchCodeBlock(el) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) + + /* Mount code block lazily */ + if (feature("content.lazy")) + return watchElementVisibility(el) + .pipe( + filter(visible => visible), + take(1), + switchMap(() => factory$) + ) + + /* Mount code block */ + return factory$ +} diff --git a/docs/src/templates/assets/javascripts/components/content/code/index.ts b/docs/src/templates/assets/javascripts/components/content/code/index.ts new file mode 100644 index 00000000..3f86e2b4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/code/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" diff --git a/docs/src/templates/assets/javascripts/components/content/details/index.ts b/docs/src/templates/assets/javascripts/components/content/details/index.ts new file mode 100644 index 00000000..17bfae45 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/details/index.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* Location target observable */ + print$: Observable /* Media print observable */ +} + +/** + * Mount options + */ +interface MountOptions { + target$: Observable /* Location target observable */ + print$: Observable /* 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
    { + 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> { + return defer(() => { + const push$ = new Subject
    () + push$.subscribe(({ action, reveal }) => { + el.toggleAttribute("open", action === "open") + if (reveal) + el.scrollIntoView() + }) + + /* Create and return component */ + return watchDetails(el, options) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/content/index.ts b/docs/src/templates/assets/javascripts/components/content/index.ts new file mode 100644 index 00000000..a29d8b41 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/components/content/mermaid/index.css b/docs/src/templates/assets/javascripts/components/content/mermaid/index.css new file mode 100644 index 00000000..3092b8ec --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/mermaid/index.css @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Rules: general + * ------------------------------------------------------------------------- */ + +/* General node */ +.node circle, +.node ellipse, +.node path, +.node polygon, +.node rect { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* General marker */ +marker { + fill: var(--md-mermaid-edge-color) !important; +} + +/* General edge label */ +.edgeLabel .label rect { + fill: transparent; +} + +/* ---------------------------------------------------------------------------- + * Rules: flowcharts + * ------------------------------------------------------------------------- */ + +/* Flowchart node label */ +.label { + color: var(--md-mermaid-label-fg-color); + font-family: var(--md-mermaid-font-family); +} + +/* Flowchart node label container */ +.label foreignObject { + overflow: visible; + line-height: initial; +} + +/* Flowchart edge label in node label */ +.label div .edgeLabel { + color: var(--md-mermaid-label-fg-color); + background-color: var(--md-mermaid-label-bg-color); +} + +/* Flowchart edge label */ +.edgeLabel, +.edgeLabel rect { + color: var(--md-mermaid-edge-color); + background-color: var(--md-mermaid-label-bg-color); + fill: var(--md-mermaid-label-bg-color); +} + +/* Flowchart edge path */ +.edgePath .path, +.flowchart-link { + stroke: var(--md-mermaid-edge-color); + stroke-width: .05rem; +} + +/* Flowchart arrow head */ +.edgePath .arrowheadPath { + fill: var(--md-mermaid-edge-color); + stroke: none; +} + +/* Flowchart subgraph */ +.cluster rect { + fill: var(--md-default-fg-color--lightest); + stroke: var(--md-default-fg-color--lighter); +} + +/* Flowchart subgraph labels */ +.cluster span { + color: var(--md-mermaid-label-fg-color); + font-family: var(--md-mermaid-font-family); +} + +/* Flowchart markers */ +g #flowchart-circleStart, +g #flowchart-circleEnd, +g #flowchart-crossStart, +g #flowchart-crossEnd, +g #flowchart-pointStart, +g #flowchart-pointEnd { + stroke: none; +} + +/* ---------------------------------------------------------------------------- + * Rules: class diagrams + * ------------------------------------------------------------------------- */ + +/* Class group node */ +g.classGroup line, +g.classGroup rect { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* Class group node text */ +g.classGroup text { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color); +} + +/* Class label box */ +.classLabel .box { + background-color: var(--md-mermaid-label-bg-color); + opacity: 1; + fill: var(--md-mermaid-label-bg-color); +} + +/* Class label text */ +.classLabel .label { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color); +} + +/* Class group divider */ +.node .divider { + stroke: var(--md-mermaid-node-fg-color); +} + +/* Class relation */ +.relation { + stroke: var(--md-mermaid-edge-color); +} + +/* Class relation cardinality */ +.cardinality { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color); +} + +/* Class relation cardinality text */ +.cardinality text { + fill: inherit !important; +} + +/* Class extension, composition and dependency marker */ +defs #classDiagram-extensionStart, +defs #classDiagram-extensionEnd, +defs #classDiagram-compositionStart, +defs #classDiagram-compositionEnd, +defs #classDiagram-dependencyStart, +defs #classDiagram-dependencyEnd { + fill: var(--md-mermaid-edge-color) !important; + stroke: var(--md-mermaid-edge-color) !important; +} + +/* Class aggregation marker */ +defs #classDiagram-aggregationStart, +defs #classDiagram-aggregationEnd { + fill: var(--md-mermaid-label-bg-color) !important; + stroke: var(--md-mermaid-edge-color) !important; +} + +/* ---------------------------------------------------------------------------- + * Rules: state diagrams + * ------------------------------------------------------------------------- */ + +/* State group node */ +g.stateGroup rect { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* State group title */ +g.stateGroup .state-title { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color) !important; +} + +/* State group background */ +g.stateGroup .composit { + fill: var(--md-mermaid-label-bg-color); +} + +/* State node label */ +.nodeLabel { + color: var(--md-mermaid-label-fg-color); + font-family: var(--md-mermaid-font-family); +} + +/* State start and end marker */ +.start-state, +.node circle.state-start, +.node circle.state-end { + fill: var(--md-mermaid-edge-color); + stroke: none; +} + +/* State end marker */ +.end-state-outer, +.end-state-inner { + fill: var(--md-mermaid-edge-color); +} + +/* State end marker */ +.end-state-inner, +.node circle.state-end { + stroke: var(--md-mermaid-label-bg-color); +} + +/* State transition */ +.transition { + stroke: var(--md-mermaid-edge-color); +} + +/* State fork and join */ +[id^=state-fork] rect, +[id^=state-join] rect { + fill: var(--md-mermaid-edge-color) !important; + stroke: none !important; +} + +/* State cluster (yes, 2x... Mermaid WTF) */ +.statediagram-cluster.statediagram-cluster .inner { + fill: var(--md-default-bg-color); +} + +/* State cluster node */ +.statediagram-cluster rect { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* State cluster divider */ +.statediagram-state rect.divider { + fill: var(--md-default-fg-color--lightest); + stroke: var(--md-default-fg-color--lighter); +} + +/* State diagram markers */ +defs #statediagram-barbEnd { + stroke: var(--md-mermaid-edge-color); +} + +/* ---------------------------------------------------------------------------- + * Rules: entity-relationship diagrams + * ------------------------------------------------------------------------- */ + +/* Attribute box */ +.attributeBoxEven, +.attributeBoxOdd { + fill: var(--md-mermaid-node-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* Entity node */ +.entityBox { + fill: var(--md-mermaid-label-bg-color); + stroke: var(--md-mermaid-node-fg-color); +} + +/* Entity node label */ +.entityLabel { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-label-fg-color); +} + +/* Entity relationship label container */ +.relationshipLabelBox { + background-color: var(--md-mermaid-label-bg-color); + opacity: 1; + fill: var(--md-mermaid-label-bg-color); + fill-opacity: 1; +} + +/* Entity relationship label */ +.relationshipLabel { + fill: var(--md-mermaid-label-fg-color); +} + +/* Entity relationship line { */ +.relationshipLine { + stroke: var(--md-mermaid-edge-color); +} + +/* Entity relationship line markers */ +defs #ZERO_OR_ONE_START *, +defs #ZERO_OR_ONE_END *, +defs #ZERO_OR_MORE_START *, +defs #ZERO_OR_MORE_END *, +defs #ONLY_ONE_START *, +defs #ONLY_ONE_END *, +defs #ONE_OR_MORE_START *, +defs #ONE_OR_MORE_END * { + stroke: var(--md-mermaid-edge-color) !important; +} + +/* Entity relationship line markers */ +defs #ZERO_OR_MORE_START circle, +defs #ZERO_OR_MORE_END circle { + fill: var(--md-mermaid-label-bg-color); +} + +/* ---------------------------------------------------------------------------- + * Rules: sequence diagrams + * ------------------------------------------------------------------------- */ + +/* Sequence actor */ +.actor { + fill: var(--md-mermaid-sequence-actor-bg-color); + stroke: var(--md-mermaid-sequence-actor-border-color); +} + +/* Sequence actor text */ +text.actor > tspan { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-sequence-actor-fg-color); +} + +/* Sequence actor line */ +line { + stroke: var(--md-mermaid-sequence-actor-line-color); +} + +/* Sequence actor */ +.actor-man circle, +.actor-man line { + fill: var(--md-mermaid-sequence-actorman-bg-color); + stroke: var(--md-mermaid-sequence-actorman-line-color); +} + +/* Sequence message line */ +.messageLine0, +.messageLine1 { + stroke: var(--md-mermaid-sequence-message-line-color); +} + +/* Sequence note */ +.note { + fill: var(--md-mermaid-sequence-note-bg-color); + stroke: var(--md-mermaid-sequence-note-border-color); +} + +/* Sequence message, loop and note text */ +.messageText, +.loopText, +.loopText > tspan, +.noteText > tspan { + font-family: var(--md-mermaid-font-family) !important; + stroke: none; +} + +/* Sequence message text */ +.messageText { + fill: var(--md-mermaid-sequence-message-fg-color); +} + +/* Sequence loop text */ +.loopText, +.loopText > tspan { + fill: var(--md-mermaid-sequence-loop-fg-color); +} + +/* Sequence note text */ +.noteText > tspan { + fill: var(--md-mermaid-sequence-note-fg-color); +} + +/* Sequence arrow head */ +#arrowhead path { + fill: var(--md-mermaid-sequence-message-line-color); + stroke: none; +} + +/* Sequence loop line */ +.loopLine { + fill: var(--md-mermaid-sequence-loop-bg-color); + stroke: var(--md-mermaid-sequence-loop-border-color); +} + +/* Sequence label box */ +.labelBox { + fill: var(--md-mermaid-sequence-label-bg-color); + stroke: none; +} + +/* Sequence label text */ +.labelText, +.labelText > span { + font-family: var(--md-mermaid-font-family); + fill: var(--md-mermaid-sequence-label-fg-color); +} + +/* Sequence number */ +.sequenceNumber { + fill: var(--md-mermaid-sequence-number-fg-color); +} + +/* Sequence rectangle */ +rect.rect { + fill: var(--md-mermaid-sequence-box-bg-color); + stroke: none; +} + +/* Sequence rectangle text */ +rect.rect + text.text { + fill: var(--md-mermaid-sequence-box-fg-color); +} + +/* Sequence diagram markers */ +defs #sequencenumber { + fill: var(--md-mermaid-sequence-number-bg-color) !important; +} diff --git a/docs/src/templates/assets/javascripts/components/content/mermaid/index.ts b/docs/src/templates/assets/javascripts/components/content/mermaid/index.ts new file mode 100644 index 00000000..3f6480fd --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/mermaid/index.ts @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 + +/** + * Global sequence number for diagrams + */ +let sequence = 0 + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Fetch Mermaid script + * + * @returns Mermaid scripts observable + */ +function fetchScripts(): Observable { + return typeof mermaid === "undefined" || mermaid instanceof Element + ? watchScript("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js") + : of(undefined) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount Mermaid diagram + * + * @param el - Code block element + * + * @returns Mermaid diagram component observable + */ +export function mountMermaid( + el: HTMLElement +): Observable> { + el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du + mermaid$ ||= fetchScripts() + .pipe( + tap(() => mermaid.initialize({ + startOnLoad: false, + themeCSS, + sequence: { + actorFontSize: "16px", // Hack: mitigate https://bit.ly/3y0NEi3 + messageFontSize: "16px", + noteFontSize: "16px" + } + })), + map(() => undefined), + shareReplay(1) + ) + + /* Render diagram */ + mermaid$.subscribe(() => { + el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du + const id = `__mermaid_${sequence++}` + + /* Create host element to replace code block */ + const host = h("div", { class: "mermaid" }) + const text = el.textContent + + /* Render and inject diagram */ + mermaid.mermaidAPI.render(id, text, (svg: string, fn: Function) => { + + /* Create a shadow root and inject diagram */ + const shadow = host.attachShadow({ mode: "closed" }) + shadow.innerHTML = svg + + /* Replace code block with diagram and bind functions */ + el.replaceWith(host) + fn?.(shadow) + }) + }) + + /* Create and return component */ + return mermaid$ + .pipe( + map(() => ({ ref: el })) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/content/table/index.ts b/docs/src/templates/assets/javascripts/components/content/table/index.ts new file mode 100644 index 00000000..c318e7a6 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/table/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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> { + el.replaceWith(sentinel) + sentinel.replaceWith(renderTable(el)) + + /* Create and return component */ + return of({ ref: el }) +} diff --git a/docs/src/templates/assets/javascripts/components/content/tabs/index.ts b/docs/src/templates/assets/javascripts/components/content/tabs/index.ts new file mode 100644 index 00000000..f57447e2 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/content/tabs/index.ts @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + animationFrameScheduler, + asyncScheduler, + auditTime, + combineLatest, + defer, + endWith, + finalize, + fromEvent, + ignoreElements, + map, + merge, + skip, + startWith, + subscribeOn, + takeUntil, + tap, + withLatestFrom +} from "rxjs" + +import { feature } from "~/_" +import { + Viewport, + getElement, + getElementContentOffset, + getElementContentSize, + getElementOffset, + getElementSize, + getElements, + watchElementContentOffset, + watchElementSize +} from "~/browser" +import { renderTabbedControl } from "~/templates" + +import { Component } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Content tabs + */ +export interface ContentTabs { + active: HTMLLabelElement /* Active tab label */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch content tabs + * + * @param el - Content tabs element + * + * @returns Content tabs observable + */ +export function watchContentTabs( + el: HTMLElement +): Observable { + const inputs = getElements(":scope > input", el) + const initial = inputs.find(input => input.checked) || inputs[0] + return merge(...inputs.map(input => fromEvent(input, "change") + .pipe( + map(() => getElement(`label[for="${input.id}"]`)) + ) + )) + .pipe( + startWith(getElement(`label[for="${initial.id}"]`)), + map(active => ({ active })) + ) +} + +/** + * Mount content tabs + * + * This function scrolls the active tab into view. While this functionality is + * provided by browsers as part of `scrollInfoView`, browsers will always also + * scroll the vertical axis, which we do not want. Thus, we decided to provide + * this functionality ourselves. + * + * @param el - Content tabs element + * @param options - Options + * + * @returns Content tabs component observable + */ +export function mountContentTabs( + el: HTMLElement, { viewport$ }: MountOptions +): Observable> { + + /* 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() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + combineLatest([push$, watchElementSize(el)]) + .pipe( + auditTime(1, animationFrameScheduler), + takeUntil(done$) + ) + .subscribe({ + + /* Handle emission */ + next([{ active }, size]) { + const offset = getElementOffset(active) + const { width } = getElementSize(active) + + /* Set tab indicator offset and width */ + el.style.setProperty("--md-indicator-x", `${offset.x}px`) + el.style.setProperty("--md-indicator-width", `${width}px`) + + /* Scroll container to active content tab */ + const content = getElementContentOffset(container) + if ( + offset.x < content.x || + offset.x + width > content.x + size.width + ) + container.scrollTo({ + left: Math.max(0, offset.x - 16), + behavior: "smooth" + }) + }, + + /* Handle complete */ + complete() { + el.style.removeProperty("--md-indicator-x") + el.style.removeProperty("--md-indicator-width") + } + }) + + /* Hide content tab buttons on borders */ + combineLatest([ + watchElementContentOffset(container), + watchElementSize(container) + ]) + .pipe( + takeUntil(done$) + ) + .subscribe(([offset, size]) => { + const content = getElementContentSize(container) + prev.hidden = offset.x < 16 + next.hidden = offset.x > content.width - size.width - 16 + }) + + /* Paginate content tab container on click */ + merge( + fromEvent(prev, "click").pipe(map(() => -1)), + fromEvent(next, "click").pipe(map(() => +1)) + ) + .pipe( + takeUntil(done$) + ) + .subscribe(direction => { + const { width } = getElementSize(container) + container.scrollBy({ + left: width * direction, + behavior: "smooth" + }) + }) + + /* Set up linking of content tabs, if enabled */ + if (feature("content.tabs.link")) + push$.pipe( + skip(1), + withLatestFrom(viewport$) + ) + .subscribe(([{ active }, { offset }]) => { + const tab = active.innerText.trim() + if (active.hasAttribute("data-md-switching")) { + active.removeAttribute("data-md-switching") + + /* Determine viewport offset of active tab */ + } else { + const y = el.offsetTop - offset.y + + /* Passively activate other tabs */ + for (const set of getElements("[data-tabs]")) + for (const input of getElements( + ":scope > input", set + )) { + const label = getElement(`label[for="${input.id}"]`) + if ( + label !== active && + label.innerText.trim() === tab + ) { + label.setAttribute("data-md-switching", "") + input.click() + break + } + } + + /* Bring active tab into view */ + window.scrollTo({ + top: el.offsetTop - y + }) + + /* Persist active tabs in local storage */ + const tabs = __md_get("__tabs") || [] + __md_set("__tabs", [...new Set([tab, ...tabs])]) + } + }) + + /* Pause media (audio, video) on switch - see https://bit.ly/3Bk6cel */ + push$.pipe(takeUntil(done$)) + .subscribe(() => { + for (const media of getElements("audio, video", el)) + media.pause() + }) + + /* Create and return component */ + return watchContentTabs(el) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) + .pipe( + subscribeOn(asyncScheduler) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/dialog/index.ts b/docs/src/templates/assets/javascripts/components/dialog/index.ts new file mode 100644 index 00000000..6ff1bd44 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/dialog/index.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* Alert subject */ +} + +/** + * Mount options + */ +interface MountOptions { + alert$: Subject /* Alert subject */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch dialog + * + * @param _el - Dialog element + * @param options - Options + * + * @returns Dialog observable + */ +export function watchDialog( + _el: HTMLElement, { alert$ }: WatchOptions +): Observable { + 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> { + const inner = getElement(".md-typeset", el) + return defer(() => { + const push$ = new Subject() + 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/docs/src/templates/assets/javascripts/components/header/_/index.ts b/docs/src/templates/assets/javascripts/components/header/_/index.ts new file mode 100644 index 00000000..0f33eb48 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/header/_/index.ts @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + bufferCount, + combineLatest, + combineLatestWith, + defer, + distinctUntilChanged, + distinctUntilKeyChanged, + endWith, + filter, + ignoreElements, + map, + of, + shareReplay, + startWith, + switchMap, + takeUntil +} from "rxjs" + +import { feature } from "~/_" +import { + Viewport, + watchElementSize, + watchToggle +} from "~/browser" + +import { Component } from "../../_" +import { Main } from "../../main" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Header + */ +export interface Header { + height: number /* Header visible height */ + hidden: boolean /* Header is hidden */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + viewport$: Observable /* Viewport observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* Header observable */ + main$: Observable
    /* 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 { + 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
    { + 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> { + return defer(() => { + const push$ = new Subject
    () + const done$ = push$.pipe(ignoreElements(), endWith(true)) + push$ + .pipe( + distinctUntilKeyChanged("active"), + combineLatestWith(header$) + ) + .subscribe(([{ active }, { hidden }]) => { + el.classList.toggle("md-header--shadow", active && !hidden) + el.hidden = hidden + }) + + /* Link to main area */ + main$.subscribe(push$) + + /* Create and return component */ + return header$ + .pipe( + takeUntil(done$), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/header/index.ts b/docs/src/templates/assets/javascripts/components/header/index.ts new file mode 100644 index 00000000..cf23ec1a --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/header/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/components/header/title/index.ts b/docs/src/templates/assets/javascripts/components/header/title/index.ts new file mode 100644 index 00000000..f3bc0d08 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/header/title/index.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + header$: Observable
    /* Header observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* 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 { + 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> { + return defer(() => { + const push$ = new Subject() + push$.subscribe({ + + /* Handle emission */ + next({ active }) { + el.classList.toggle("md-header__title--active", active) + }, + + /* Handle complete */ + complete() { + el.classList.remove("md-header__title--active") + } + }) + + /* Obtain headline, if any */ + const heading = getOptionalElement(".md-content h1") + if (typeof heading === "undefined") + return EMPTY + + /* Create and return component */ + return watchHeaderTitle(heading, options) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/index.ts b/docs/src/templates/assets/javascripts/components/index.ts new file mode 100644 index 00000000..3d4391d1 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" +export * from "./announce" +export * from "./consent" +export * from "./content" +export * from "./dialog" +export * from "./header" +export * from "./main" +export * from "./palette" +export * from "./progress" +export * from "./search" +export * from "./sidebar" +export * from "./source" +export * from "./tabs" +export * from "./toc" +export * from "./top" diff --git a/docs/src/templates/assets/javascripts/components/main/index.ts b/docs/src/templates/assets/javascripts/components/main/index.ts new file mode 100644 index 00000000..2509f9b9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/main/index.ts @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + header$: Observable
    /* 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
    { + + /* 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/docs/src/templates/assets/javascripts/components/palette/index.ts b/docs/src/templates/assets/javascripts/components/palette/index.ts new file mode 100644 index 00000000..cf578f60 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/palette/index.ts @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + asyncScheduler, + defer, + finalize, + fromEvent, + map, + mergeMap, + observeOn, + of, + shareReplay, + startWith, + tap +} from "rxjs" + +import { getElements } from "~/browser" +import { h } from "~/utilities" + +import { + Component, + getComponentElement +} from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Palette colors + */ +export interface PaletteColor { + scheme?: string /* Color scheme */ + primary?: string /* Primary color */ + accent?: string /* Accent color */ +} + +/** + * Palette + */ +export interface Palette { + index: number /* Palette index */ + color: PaletteColor /* Palette colors */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch color palette + * + * @param inputs - Color palette element + * + * @returns Color palette observable + */ +export function watchPalette( + inputs: HTMLInputElement[] +): Observable { + const current = __md_get("__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> { + const meta = h("meta", { name: "theme-color" }) + document.head.appendChild(meta) + + // Add color scheme meta tag + const scheme = h("meta", { name: "color-scheme" }) + document.head.appendChild(scheme) + + /* Mount component on subscription */ + return defer(() => { + const push$ = new Subject() + push$.subscribe(palette => { + document.body.setAttribute("data-md-color-switching", "") + + /* Set color palette */ + for (const [key, value] of Object.entries(palette.color)) + document.body.setAttribute(`data-md-color-${key}`, value) + + /* Toggle visibility */ + for (let index = 0; index < inputs.length; index++) { + const label = inputs[index].nextElementSibling + if (label instanceof HTMLElement) + label.hidden = palette.index !== index + } + + /* Persist preference in local storage */ + __md_set("__palette", palette) + }) + + /* Update theme-color meta tag */ + push$ + .pipe( + map(() => { + const header = getComponentElement("header") + const style = window.getComputedStyle(header) + + // Set color scheme + scheme.content = style.colorScheme + + /* Return color in hexadecimal format */ + return style.backgroundColor.match(/\d+/g)! + .map(value => (+value).toString(16).padStart(2, "0")) + .join("") + }) + ) + .subscribe(color => meta.content = `#${color}`) + + /* Revert transition durations after color switch */ + push$.pipe(observeOn(asyncScheduler)) + .subscribe(() => { + document.body.removeAttribute("data-md-color-switching") + }) + + /* Create and return component */ + const inputs = getElements("input", el) + return watchPalette(inputs) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/progress/index.ts b/docs/src/templates/assets/javascripts/components/progress/index.ts new file mode 100644 index 00000000..30c722b8 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/progress/index.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + defer, + finalize, + map, + tap +} from "rxjs" + +import { Component } from "../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Progress indicator + */ +export interface Progress { + value: number // Progress value +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + progress$: Subject // Progress subject +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount progress indicator + * + * @param el - Progress indicator element + * @param options - Options + * + * @returns Progress indicator component observable + */ +export function mountProgress( + el: HTMLElement, { progress$ }: MountOptions +): Observable> { + + // Mount component on subscription + return defer(() => { + const push$ = new Subject() + push$.subscribe(({ value }) => { + el.style.setProperty("--md-progress-value", `${value}`) + }) + + // Create and return component + return progress$ + .pipe( + tap(value => push$.next({ value })), + finalize(() => push$.complete()), + map(value => ({ ref: el, value })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/search/_/index.ts b/docs/src/templates/assets/javascripts/components/search/_/index.ts new file mode 100644 index 00000000..aa963b47 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/_/index.ts @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + NEVER, + Observable, + ObservableInput, + filter, + fromEvent, + merge, + mergeWith +} from "rxjs" + +import { configuration } from "~/_" +import { + Keyboard, + getActiveElement, + getElements, + setToggle +} from "~/browser" +import { + SearchIndex, + SearchResult, + setupSearchWorker +} from "~/integrations" + +import { + Component, + getComponentElement, + getComponentElements +} from "../../_" +import { + SearchQuery, + mountSearchQuery +} from "../query" +import { mountSearchResult } from "../result" +import { + SearchShare, + mountSearchShare +} from "../share" +import { + SearchSuggest, + mountSearchSuggest +} from "../suggest" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search + */ +export type Search = + | SearchQuery + | SearchResult + | SearchShare + | SearchSuggest + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + index$: ObservableInput /* Search index observable */ + keyboard$: Observable /* 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> { + const config = configuration() + try { + const worker$ = setupSearchWorker(config.search, index$) + + /* Retrieve query and result components */ + const query = getComponentElement("search-query", el) + const result = getComponentElement("search-result", el) + + /* Always close search on result selection */ + fromEvent(el, "click") + .pipe( + filter(({ target }) => ( + target instanceof Element && !!target.closest("a") + )) + ) + .subscribe(() => setToggle("search", false)) + + /* Set up search keyboard handlers */ + keyboard$ + .pipe( + filter(({ mode }) => mode === "search") + ) + .subscribe(key => { + const active = getActiveElement() + switch (key.type) { + + /* Enter: go to first (best) result */ + case "Enter": + if (active === query) { + const anchors = new Map() + for (const anchor of getElements( + ":first-child [href]", result + )) { + const article = anchor.firstElementChild! + anchors.set(anchor, parseFloat( + article.getAttribute("data-md-score")! + )) + } + + /* Go to result with highest score, if any */ + if (anchors.size) { + const [[best]] = [...anchors].sort(([, a], [, b]) => b - a) + best.click() + } + + /* Otherwise omit form submission */ + key.claim() + } + break + + /* Escape or Tab: close search */ + case "Escape": + case "Tab": + setToggle("search", false) + query.blur() + break + + /* Vertical arrows: select previous or next search result */ + case "ArrowUp": + case "ArrowDown": + if (typeof active === "undefined") { + query.focus() + } else { + const els = [query, ...getElements( + ":not(details) > [href], summary, details[open] [href]", + result + )] + const i = Math.max(0, ( + Math.max(0, els.indexOf(active)) + els.length + ( + key.type === "ArrowUp" ? -1 : +1 + ) + ) % els.length) + els[i].focus() + } + + /* Prevent scrolling of page */ + key.claim() + break + + /* All other keys: hand to search query */ + default: + if (query !== getActiveElement()) + query.focus() + } + }) + + /* Set up global keyboard handlers */ + keyboard$ + .pipe( + filter(({ mode }) => mode === "global") + ) + .subscribe(key => { + switch (key.type) { + + /* Open search and select query */ + case "f": + case "s": + case "/": + query.focus() + query.select() + + /* Prevent scrolling of page */ + key.claim() + break + } + }) + + /* Create and return component */ + const query$ = mountSearchQuery(query, { worker$ }) + return merge( + query$, + mountSearchResult(result, { worker$, query$ }) + ) + .pipe( + mergeWith( + + /* Search sharing */ + ...getComponentElements("search-share", el) + .map(child => mountSearchShare(child, { query$ })), + + /* Search suggestions */ + ...getComponentElements("search-suggest", el) + .map(child => mountSearchSuggest(child, { worker$, keyboard$ })) + ) + ) + + /* Gracefully handle broken search */ + } catch (err) { + el.hidden = true + return NEVER + } +} diff --git a/docs/src/templates/assets/javascripts/components/search/highlight/.eslintrc b/docs/src/templates/assets/javascripts/components/search/highlight/.eslintrc new file mode 100644 index 00000000..38a5714d --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/highlight/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "no-null/no-null": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/components/search/highlight/index.ts b/docs/src/templates/assets/javascripts/components/search/highlight/index.ts new file mode 100644 index 00000000..bc3f94c9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/highlight/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* Map of replacements */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + index$: ObservableInput /* Search index observable */ + location$: Observable /* 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> { + return combineLatest([ + index$, + location$ + .pipe( + startWith(getLocation()), + filter(url => !!url.searchParams.get("h")) + ) + ]) + .pipe( + map(([index, url]) => setupSearchHighlighter(index.config)( + url.searchParams.get("h")! + )), + map(fn => { + const nodes = new Map() + + /* 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/docs/src/templates/assets/javascripts/components/search/index.ts b/docs/src/templates/assets/javascripts/components/search/index.ts new file mode 100644 index 00000000..846d8685 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/components/search/query/index.ts b/docs/src/templates/assets/javascripts/components/search/query/index.ts new file mode 100644 index 00000000..4ce21279 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/query/index.ts @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + combineLatest, + distinctUntilChanged, + distinctUntilKeyChanged, + endWith, + finalize, + first, + fromEvent, + ignoreElements, + map, + merge, + shareReplay, + takeUntil, + tap +} from "rxjs" + +import { + getElement, + getLocation, + setToggle, + watchElementFocus, + watchToggle +} from "~/browser" +import { + SearchMessage, + SearchMessageType, + isSearchReadyMessage +} from "~/integrations" + +import { Component } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search query + */ +export interface SearchQuery { + value: string /* Query value */ + focus: boolean /* Query focus */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + worker$: Subject /* Search worker */ +} + +/** + * Mount options + */ +interface MountOptions { + worker$: Subject /* Search worker */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch search query + * + * Note that the focus event which triggers re-reading the current query value + * is delayed by `1ms` so the input's empty state is allowed to propagate. + * + * @param el - Search query element + * @param options - Options + * + * @returns Search query observable + */ +export function watchSearchQuery( + el: HTMLInputElement, { worker$ }: WatchOptions +): Observable { + + /* Support search deep linking */ + const { searchParams } = getLocation() + if (searchParams.has("q")) { + setToggle("search", true) + + /* Set query from parameter */ + el.value = searchParams.get("q")! + el.focus() + + /* Remove query parameter on close */ + watchToggle("search") + .pipe( + first(active => !active) + ) + .subscribe(() => { + const url = getLocation() + url.searchParams.delete("q") + history.replaceState({}, "", `${url}`) + }) + } + + /* Intercept focus and input events */ + const focus$ = watchElementFocus(el) + const value$ = merge( + worker$.pipe(first(isSearchReadyMessage)), + fromEvent(el, "keyup"), + focus$ + ) + .pipe( + map(() => el.value), + distinctUntilChanged() + ) + + /* Combine into single observable */ + return combineLatest([value$, focus$]) + .pipe( + map(([value, focus]) => ({ value, focus })), + shareReplay(1) + ) +} + +/** + * Mount search query + * + * @param el - Search query element + * @param options - Options + * + * @returns Search query component observable + */ +export function mountSearchQuery( + el: HTMLInputElement, { worker$ }: MountOptions +): Observable> { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + + /* Handle value change */ + combineLatest([ + worker$.pipe(first(isSearchReadyMessage)), + push$ + ], (_, query) => query) + .pipe( + distinctUntilKeyChanged("value") + ) + .subscribe(({ value }) => worker$.next({ + type: SearchMessageType.QUERY, + data: value + })) + + /* Handle focus change */ + push$ + .pipe( + distinctUntilKeyChanged("focus") + ) + .subscribe(({ focus }) => { + if (focus) + setToggle("search", focus) + }) + + /* Handle reset */ + fromEvent(el.form!, "reset") + .pipe( + takeUntil(done$) + ) + .subscribe(() => el.focus()) + + // Focus search query on label click - note that this is necessary to bring + // up the keyboard on iOS and other mobile platforms, as the search dialog is + // not visible at first, and programatically focusing an input element must + // be triggered by a user interaction - see https://t.ly/Cb30n + const label = getElement("header [for=__search]") + fromEvent(label, "click") + .subscribe(() => el.focus()) + + /* Create and return component */ + return watchSearchQuery(el, { worker$ }) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })), + shareReplay(1) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/search/result/index.ts b/docs/src/templates/assets/javascripts/components/search/result/index.ts new file mode 100644 index 00000000..c3c9ef20 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/result/index.ts @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + Subject, + bufferCount, + filter, + finalize, + first, + fromEvent, + map, + merge, + mergeMap, + of, + share, + skipUntil, + switchMap, + takeUntil, + tap, + withLatestFrom, + zipWith +} from "rxjs" + +import { translation } from "~/_" +import { + getElement, + getOptionalElement, + watchElementBoundary, + watchToggle +} from "~/browser" +import { + SearchMessage, + SearchResult, + isSearchReadyMessage, + isSearchResultMessage +} from "~/integrations" +import { renderSearchResultItem } from "~/templates" +import { round } from "~/utilities" + +import { Component } from "../../_" +import { SearchQuery } from "../query" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + query$: Observable /* Search query observable */ + worker$: Subject /* Search worker */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount search result list + * + * This function performs a lazy rendering of the search results, depending on + * the vertical offset of the search result container. + * + * @param el - Search result list element + * @param options - Options + * + * @returns Search result list component observable + */ +export function mountSearchResult( + el: HTMLElement, { worker$, query$ }: MountOptions +): Observable> { + const push$ = new Subject() + const boundary$ = watchElementBoundary(el.parentElement!) + .pipe( + filter(Boolean) + ) + + /* Retrieve container */ + const container = el.parentElement! + + /* Retrieve nested components */ + const meta = getElement(":scope > :first-child", el) + const list = getElement(":scope > :last-child", el) + + /* Reveal to accessibility tree – see https://bit.ly/3iAA7t8 */ + watchToggle("search") + .subscribe(active => list.setAttribute( + "role", active ? "list" : "presentation" + )) + + /* Update search result metadata */ + push$ + .pipe( + withLatestFrom(query$), + skipUntil(worker$.pipe(first(isSearchReadyMessage))) + ) + .subscribe(([{ items }, { value }]) => { + switch (items.length) { + + /* No results */ + case 0: + meta.textContent = value.length + ? translation("search.result.none") + : translation("search.result.placeholder") + break + + /* One result */ + case 1: + meta.textContent = translation("search.result.one") + break + + /* Multiple result */ + default: + const count = round(items.length) + meta.textContent = translation("search.result.other", count) + } + }) + + /* Render search result item */ + const render$ = push$ + .pipe( + tap(() => list.innerHTML = ""), + switchMap(({ items }) => merge( + of(...items.slice(0, 10)), + of(...items.slice(10)) + .pipe( + bufferCount(4), + zipWith(boundary$), + switchMap(([chunk]) => chunk) + ) + )), + map(renderSearchResultItem), + share() + ) + + /* Update search result list */ + render$.subscribe(item => list.appendChild(item)) + render$ + .pipe( + mergeMap(item => { + const details = getOptionalElement("details", item) + if (typeof details === "undefined") + return EMPTY + + /* Keep position of details element stable */ + return fromEvent(details, "toggle") + .pipe( + takeUntil(push$), + map(() => details) + ) + }) + ) + .subscribe(details => { + if ( + details.open === false && + details.offsetTop <= container.scrollTop + ) + container.scrollTo({ top: details.offsetTop }) + }) + + /* Filter search result message */ + const result$ = worker$ + .pipe( + filter(isSearchResultMessage), + map(({ data }) => data) + ) + + /* Create and return component */ + return result$ + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/search/share/index.ts b/docs/src/templates/assets/javascripts/components/search/share/index.ts new file mode 100644 index 00000000..3db382c8 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/share/index.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + endWith, + finalize, + fromEvent, + ignoreElements, + map, + takeUntil, + tap +} from "rxjs" + +import { getLocation } from "~/browser" + +import { Component } from "../../_" +import { SearchQuery } from "../query" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search sharing + */ +export interface SearchShare { + url: URL /* Deep link for sharing */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + query$: Observable /* Search query observable */ +} + +/** + * Mount options + */ +interface MountOptions { + query$: Observable /* 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 { + return query$ + .pipe( + map(({ value }) => { + const url = getLocation() + url.hash = "" + + /* Compute readable query strings */ + value = value + .replace(/\s+/g, "+") /* Collapse whitespace */ + .replace(/&/g, "%26") /* Escape '&' character */ + .replace(/=/g, "%3D") /* Escape '=' character */ + + /* Replace query string */ + url.search = `q=${value}` + return { url } + }) + ) +} + +/** + * Mount search sharing + * + * @param el - Search sharing element + * @param options - Options + * + * @returns Search sharing component observable + */ +export function mountSearchShare( + el: HTMLAnchorElement, options: MountOptions +): Observable> { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + push$.subscribe(({ url }) => { + el.setAttribute("data-clipboard-text", el.href) + el.href = `${url}` + }) + + /* Prevent following of link */ + fromEvent(el, "click") + .pipe( + takeUntil(done$) + ) + .subscribe(ev => ev.preventDefault()) + + /* Create and return component */ + return watchSearchShare(el, options) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/search/suggest/index.ts b/docs/src/templates/assets/javascripts/components/search/suggest/index.ts new file mode 100644 index 00000000..e7881475 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/search/suggest/index.ts @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + asyncScheduler, + combineLatestWith, + distinctUntilChanged, + filter, + finalize, + fromEvent, + map, + merge, + observeOn, + tap +} from "rxjs" + +import { Keyboard } from "~/browser" +import { + SearchMessage, + SearchResult, + isSearchResultMessage +} from "~/integrations" + +import { Component, getComponentElement } from "../../_" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search suggestions + */ +export interface SearchSuggest {} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Mount options + */ +interface MountOptions { + keyboard$: Observable /* Keyboard observable */ + worker$: Subject /* Search worker */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount search suggestions + * + * This function will perform a lazy rendering of the search results, depending + * on the vertical offset of the search result container. + * + * @param el - Search result list element + * @param options - Options + * + * @returns Search result list component observable + */ +export function mountSearchSuggest( + el: HTMLElement, { worker$, keyboard$ }: MountOptions +): Observable> { + const push$ = new Subject() + + /* Retrieve query component and track all changes */ + const query = getComponentElement("search-query") + const query$ = merge( + fromEvent(query, "keydown"), + fromEvent(query, "focus") + ) + .pipe( + observeOn(asyncScheduler), + map(() => query.value), + distinctUntilChanged(), + ) + + /* Update search suggestions */ + push$ + .pipe( + combineLatestWith(query$), + map(([{ suggest }, value]) => { + const words = value.split(/([\s-]+)/) + if (suggest?.length && words[words.length - 1]) { + const last = suggest[suggest.length - 1] + if (last.startsWith(words[words.length - 1])) + words[words.length - 1] = last + } else { + words.length = 0 + } + return words + }) + ) + .subscribe(words => el.innerHTML = words + .join("") + .replace(/\s/g, " ") + ) + + /* Set up search keyboard handlers */ + keyboard$ + .pipe( + filter(({ mode }) => mode === "search") + ) + .subscribe(key => { + switch (key.type) { + + /* Right arrow: accept current suggestion */ + case "ArrowRight": + if ( + el.innerText.length && + query.selectionStart === query.value.length + ) + query.value = el.innerText + break + } + }) + + /* Filter search result message */ + const result$ = worker$ + .pipe( + filter(isSearchResultMessage), + map(({ data }) => data) + ) + + /* Create and return component */ + return result$ + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(() => ({ ref: el })) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/sidebar/index.ts b/docs/src/templates/assets/javascripts/components/sidebar/index.ts new file mode 100644 index 00000000..82f3d03e --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/sidebar/index.ts @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + animationFrameScheduler, + asyncScheduler, + auditTime, + combineLatest, + defer, + distinctUntilChanged, + endWith, + finalize, + first, + from, + fromEvent, + ignoreElements, + map, + mergeMap, + observeOn, + takeUntil, + tap, + withLatestFrom +} from "rxjs" + +import { + Viewport, + getElement, + getElementContainer, + getElementOffset, + getElementSize, + getElements +} from "~/browser" + +import { Component } from "../_" +import { Header } from "../header" +import { Main } from "../main" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Sidebar + */ +export interface Sidebar { + height: number /* Sidebar height */ + locked: boolean /* Sidebar is locked */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + viewport$: Observable /* Viewport observable */ + main$: Observable
    /* Main area observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* Header observable */ + main$: Observable
    /* 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 { + const parent = el.closest(".md-grid")! + const adjust = + parent.offsetTop - + parent.parentElement!.offsetTop + + /* Compute the sidebar's available height and if it should be locked */ + return combineLatest([main$, viewport$]) + .pipe( + map(([{ offset, height }, { offset: { y } }]) => { + height = height + + Math.min(adjust, Math.max(0, y - offset)) + - adjust + return { + height, + locked: y >= offset + adjust + } + }), + distinctUntilChanged((a, b) => ( + a.height === b.height && + a.locked === b.locked + )) + ) +} + +/** + * Mount sidebar + * + * This function doesn't set the height of the actual sidebar, but of its first + * child – the `.md-sidebar__scrollwrap` element in order to mitigiate jittery + * sidebars when the footer is scrolled into view. At some point we switched + * from `absolute` / `fixed` positioning to `sticky` positioning, significantly + * reducing jitter in some browsers (respectively Firefox and Safari) when + * scrolling from the top. However, top-aligned sticky positioning means that + * the sidebar snaps to the bottom when the end of the container is reached. + * This is what leads to the mentioned jitter, as the sidebar's height may be + * updated too slowly. + * + * This behaviour can be mitigiated by setting the height of the sidebar to `0` + * while preserving the padding, and the height on its first element. + * + * @param el - Sidebar element + * @param options - Options + * + * @returns Sidebar component observable + */ +export function mountSidebar( + el: HTMLElement, { header$, ...options }: MountOptions +): Observable> { + const inner = getElement(".md-sidebar__scrollwrap", el) + const { y } = getElementOffset(inner) + return defer(() => { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + const next$ = push$ + .pipe( + auditTime(0, animationFrameScheduler) + ) + + /* Update sidebar height and offset */ + next$.pipe(withLatestFrom(header$)) + .subscribe({ + + /* Handle emission */ + next([{ height }, { height: offset }]) { + inner.style.height = `${height - 2 * y}px` + el.style.top = `${offset}px` + }, + + /* Handle complete */ + complete() { + inner.style.height = "" + el.style.top = "" + } + }) + + /* Bring active item into view on initial load */ + next$.pipe(first()) + .subscribe(() => { + for (const item of getElements(".md-nav__link--active[href]", el)) { + const container = getElementContainer(item) + if (typeof container !== "undefined") { + const offset = item.offsetTop - container.offsetTop + const { height } = getElementSize(container) + container.scrollTo({ + top: offset - height / 2 + }) + } + } + }) + + /* Handle accessibility for expandable items, see https://bit.ly/3jaod9p */ + from(getElements("label[tabindex]", el)) + .pipe( + mergeMap(label => fromEvent(label, "click") + .pipe( + observeOn(asyncScheduler), + map(() => label), + takeUntil(done$) + ) + ) + ) + .subscribe(label => { + const input = getElement(`[id="${label.htmlFor}"]`) + const nav = getElement(`[aria-labelledby="${label.id}"]`) + nav.setAttribute("aria-expanded", `${input.checked}`) + }) + + /* Create and return component */ + return watchSidebar(el, options) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/source/_/index.ts b/docs/src/templates/assets/javascripts/components/source/_/index.ts new file mode 100644 index 00000000..5f6c4d11 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/_/index.ts @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + Subject, + catchError, + defer, + filter, + finalize, + map, + of, + shareReplay, + tap +} from "rxjs" + +import { getElement } from "~/browser" +import { ConsentDefaults } from "~/components/consent" +import { renderSourceFacts } from "~/templates" + +import { + Component, + getComponentElements +} from "../../_" +import { + SourceFacts, + fetchSourceFacts +} from "../facts" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Repository information + */ +export interface Source { + facts: SourceFacts /* Repository facts */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Repository information observable + */ +let fetch$: Observable + +/* ---------------------------------------------------------------------------- + * 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 { + return fetch$ ||= defer(() => { + const cached = __md_get("__source", sessionStorage) + if (cached) { + return of(cached) + } else { + + /* Check if consent is configured and was given */ + const els = getComponentElements("consent") + if (els.length) { + const consent = __md_get("__consent") + if (!(consent && consent.github)) + return EMPTY + } + + /* Fetch repository facts */ + return fetchSourceFacts(el.href) + .pipe( + tap(facts => __md_set("__source", facts, sessionStorage)) + ) + } + }) + .pipe( + catchError(() => EMPTY), + filter(facts => Object.keys(facts).length > 0), + map(facts => ({ facts })), + shareReplay(1) + ) +} + +/** + * Mount repository information + * + * @param el - Repository information element + * + * @returns Repository information component observable + */ +export function mountSource( + el: HTMLAnchorElement +): Observable> { + const inner = getElement(":scope > :last-child", el) + return defer(() => { + const push$ = new Subject() + 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/docs/src/templates/assets/javascripts/components/source/facts/_/index.ts b/docs/src/templates/assets/javascripts/components/source/facts/_/index.ts new file mode 100644 index 00000000..154f229f --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/facts/_/index.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + + /* Try to match GitHub repository */ + let match = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i) + if (match) { + const [, user, repo] = match + return fetchSourceFactsFromGitHub(user, repo) + } + + /* Try to match GitLab repository */ + match = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i) + if (match) { + const [, base, slug] = match + return fetchSourceFactsFromGitLab(base, slug) + } + + /* Fallback */ + return EMPTY +} diff --git a/docs/src/templates/assets/javascripts/components/source/facts/github/index.ts b/docs/src/templates/assets/javascripts/components/source/facts/github/index.ts new file mode 100644 index 00000000..12cc55e0 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/facts/github/index.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + if (typeof repo !== "undefined") { + const url = `https://api.github.com/repos/${user}/${repo}` + return zip( + + /* Fetch version */ + requestJSON(`${url}/releases/latest`) + .pipe( + catchError(() => EMPTY), // @todo refactor instant loading + map(release => ({ + version: release.tag_name + })), + defaultIfEmpty({}) + ), + + /* Fetch stars and forks */ + requestJSON(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(url) + .pipe( + map(info => ({ + repositories: info.public_repos + })), + defaultIfEmpty({}) + ) + } +} diff --git a/docs/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts b/docs/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts new file mode 100644 index 00000000..d85d4afd --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 { + const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}` + return requestJSON(url) + .pipe( + catchError(() => EMPTY), // @todo refactor instant loading + map(({ star_count, forks_count }) => ({ + stars: star_count, + forks: forks_count + })), + defaultIfEmpty({}) + ) +} diff --git a/docs/src/templates/assets/javascripts/components/source/facts/index.ts b/docs/src/templates/assets/javascripts/components/source/facts/index.ts new file mode 100644 index 00000000..f9bda64d --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/facts/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/components/source/index.ts b/docs/src/templates/assets/javascripts/components/source/index.ts new file mode 100644 index 00000000..7fac4813 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/source/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/components/tabs/index.ts b/docs/src/templates/assets/javascripts/components/tabs/index.ts new file mode 100644 index 00000000..1e69df28 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/tabs/index.ts @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + header$: Observable
    /* Header observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* 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 { + 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> { + return defer(() => { + const push$ = new Subject() + 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/docs/src/templates/assets/javascripts/components/toc/index.ts b/docs/src/templates/assets/javascripts/components/toc/index.ts new file mode 100644 index 00000000..04b8d85f --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/toc/index.ts @@ -0,0 +1,379 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + asyncScheduler, + bufferCount, + combineLatestWith, + debounceTime, + defer, + distinctUntilChanged, + distinctUntilKeyChanged, + endWith, + filter, + finalize, + ignoreElements, + map, + merge, + observeOn, + of, + repeat, + scan, + share, + skip, + startWith, + switchMap, + takeUntil, + tap, + withLatestFrom +} from "rxjs" + +import { feature } from "~/_" +import { + Viewport, + getElement, + getElementContainer, + getElementSize, + getElements, + getLocation, + getOptionalElement, + watchElementSize +} from "~/browser" + +import { + Component, + getComponentElement +} from "../_" +import { Header } from "../header" +import { Main } from "../main" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Table of contents + */ +export interface TableOfContents { + prev: HTMLAnchorElement[][] /* Anchors (previous) */ + next: HTMLAnchorElement[][] /* Anchors (next) */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* Header observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* Header observable */ + main$: Observable
    /* Main area observable */ + target$: Observable /* 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 { + const table = new Map() + + /* Compute anchor-to-target mapping */ + const anchors = getElements("[href^=\\#]", el) + for (const anchor of anchors) { + const id = decodeURIComponent(anchor.hash.substring(1)) + const target = getOptionalElement(`[id="${id}"]`) + if (typeof target !== "undefined") + table.set(anchor, target) + } + + /* Compute necessary adjustment for header */ + const adjust$ = header$ + .pipe( + distinctUntilKeyChanged("height"), + map(({ height }) => { + const main = getComponentElement("main") + const grid = getElement(":scope > :first-child", main) + return height + 0.8 * ( + grid.offsetTop - + main.offsetTop + ) + }), + share() + ) + + /* Compute partition of previous and next anchors */ + const partition$ = watchElementSize(document.body) + .pipe( + distinctUntilKeyChanged("height"), + + /* Build index to map anchor paths to vertical offsets */ + switchMap(body => defer(() => { + let path: HTMLAnchorElement[] = [] + return of([...table].reduce((index, [anchor, target]) => { + while (path.length) { + const last = table.get(path[path.length - 1])! + if (last.tagName >= target.tagName) { + path.pop() + } else { + break + } + } + + /* If the current anchor is hidden, continue with its parent */ + let offset = target.offsetTop + while (!offset && target.parentElement) { + target = target.parentElement + offset = target.offsetTop + } + + /* Fix anchor offsets in tables - see https://bit.ly/3CUFOcn */ + let parent = target.offsetParent as HTMLElement + for (; parent; parent = parent.offsetParent as HTMLElement) + offset += parent.offsetTop + + /* Map reversed anchor path to vertical offset */ + return index.set( + [...path = [...path, anchor]].reverse(), + offset + ) + }, new Map())) + }) + .pipe( + + /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */ + map(index => new Map([...index].sort(([, a], [, b]) => a - b))), + combineLatestWith(adjust$), + + /* Re-compute partition when viewport offset changes */ + switchMap(([index, adjust]) => viewport$ + .pipe( + scan(([prev, next], { offset: { y }, size }) => { + const last = y + size.height >= Math.floor(body.height) + + /* Look forward */ + while (next.length) { + const [, offset] = next[0] + if (offset - adjust < y || last) { + prev = [...prev, next.shift()!] + } else { + break + } + } + + /* Look backward */ + while (prev.length) { + const [, offset] = prev[prev.length - 1] + if (offset - adjust >= y && !last) { + next = [prev.pop()!, ...next] + } else { + break + } + } + + /* Return partition */ + return [prev, next] + }, [[], [...index]]), + distinctUntilChanged((a, b) => ( + a[0] === b[0] && + a[1] === b[1] + )) + ) + ) + ) + ) + ) + + /* Compute and return anchor list migrations */ + return partition$ + .pipe( + map(([prev, next]) => ({ + prev: prev.map(([path]) => path), + next: next.map(([path]) => path) + })), + + /* Extract anchor list migrations */ + startWith({ prev: [], next: [] }), + bufferCount(2, 1), + map(([a, b]) => { + + /* Moving down */ + if (a.prev.length < b.prev.length) { + return { + prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length), + next: [] + } + + /* Moving up */ + } else { + return { + prev: b.prev.slice(-1), + next: b.next.slice(0, b.next.length - a.next.length) + } + } + }) + ) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Mount table of contents + * + * @param el - Table of contents element + * @param options - Options + * + * @returns Table of contents component observable + */ +export function mountTableOfContents( + el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions +): Observable> { + return defer(() => { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + push$.subscribe(({ prev, next }) => { + + /* Look forward */ + for (const [anchor] of next) { + anchor.classList.remove("md-nav__link--passed") + anchor.classList.remove("md-nav__link--active") + } + + /* Look backward */ + for (const [index, [anchor]] of prev.entries()) { + anchor.classList.add("md-nav__link--passed") + anchor.classList.toggle( + "md-nav__link--active", + index === prev.length - 1 + ) + } + }) + + /* Set up following, if enabled */ + if (feature("toc.follow")) { + + /* Toggle smooth scrolling only for anchor clicks */ + const smooth$ = merge( + viewport$.pipe(debounceTime(1), map(() => undefined)), + viewport$.pipe(debounceTime(250), map(() => "smooth" as const)) + ) + + /* Bring active anchor into view */ // @todo: refactor + push$ + .pipe( + filter(({ prev }) => prev.length > 0), + combineLatestWith(main$.pipe(observeOn(asyncScheduler))), + withLatestFrom(smooth$) + ) + .subscribe(([[{ prev }], behavior]) => { + const [anchor] = prev[prev.length - 1] + if (anchor.offsetHeight) { + + /* Retrieve overflowing container and scroll */ + const container = getElementContainer(anchor) + if (typeof container !== "undefined") { + const offset = anchor.offsetTop - container.offsetTop + const { height } = getElementSize(container) + container.scrollTo({ + top: offset - height / 2, + behavior + }) + } + } + }) + } + + /* Set up anchor tracking, if enabled */ + if (feature("navigation.tracking")) + viewport$ + .pipe( + takeUntil(done$), + distinctUntilKeyChanged("offset"), + debounceTime(250), + skip(1), + takeUntil(target$.pipe(skip(1))), + repeat({ delay: 250 }), + withLatestFrom(push$) + ) + .subscribe(([, { prev }]) => { + const url = getLocation() + + /* Set hash fragment to active anchor */ + const anchor = prev[prev.length - 1] + if (anchor && anchor.length) { + const [active] = anchor + const { hash } = new URL(active.href) + if (url.hash !== hash) { + url.hash = hash + history.replaceState({}, "", `${url}`) + } + + /* Reset anchor when at the top */ + } else { + url.hash = "" + history.replaceState({}, "", `${url}`) + } + }) + + /* Create and return component */ + return watchTableOfContents(el, { viewport$, header$ }) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) + }) +} diff --git a/docs/src/templates/assets/javascripts/components/top/index.ts b/docs/src/templates/assets/javascripts/components/top/index.ts new file mode 100644 index 00000000..82e88b61 --- /dev/null +++ b/docs/src/templates/assets/javascripts/components/top/index.ts @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + Observable, + Subject, + bufferCount, + combineLatest, + distinctUntilChanged, + distinctUntilKeyChanged, + endWith, + finalize, + fromEvent, + ignoreElements, + map, + repeat, + skip, + takeUntil, + tap +} from "rxjs" + +import { Viewport } from "~/browser" + +import { Component } from "../_" +import { Header } from "../header" +import { Main } from "../main" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Back-to-top button + */ +export interface BackToTop { + hidden: boolean /* Back-to-top button is hidden */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + viewport$: Observable /* Viewport observable */ + main$: Observable
    /* Main area observable */ + target$: Observable /* Location target observable */ +} + +/** + * Mount options + */ +interface MountOptions { + viewport$: Observable /* Viewport observable */ + header$: Observable
    /* Header observable */ + main$: Observable
    /* Main area observable */ + target$: Observable /* 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 { + + /* 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> { + const push$ = new Subject() + const done$ = push$.pipe(ignoreElements(), endWith(true)) + push$.subscribe({ + + /* Handle emission */ + next({ hidden }) { + el.hidden = hidden + if (hidden) { + el.setAttribute("tabindex", "-1") + el.blur() + } else { + el.removeAttribute("tabindex") + } + }, + + /* Handle complete */ + complete() { + el.style.top = "" + el.hidden = true + el.removeAttribute("tabindex") + } + }) + + /* Watch header height */ + header$ + .pipe( + takeUntil(done$), + distinctUntilKeyChanged("height") + ) + .subscribe(({ height }) => { + el.style.top = `${height + 16}px` + }) + + /* Go back to top */ + fromEvent(el, "click") + .subscribe(ev => { + ev.preventDefault() + window.scrollTo({ top: 0 }) + }) + + /* Create and return component */ + return watchBackToTop(el, { viewport$, main$, target$ }) + .pipe( + tap(state => push$.next(state)), + finalize(() => push$.complete()), + map(state => ({ ref: el, ...state })) + ) +} diff --git a/docs/src/templates/assets/javascripts/integrations/clipboard/index.ts b/docs/src/templates/assets/javascripts/integrations/clipboard/index.ts new file mode 100644 index 00000000..cf46f601 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/clipboard/index.ts @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 /* Alert subject */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Extract text to copy + * + * @param el - HTML element + * + * @returns Extracted text + */ +function extract(el: HTMLElement): string { + el.setAttribute("data-md-copying", "") + const copy = el.closest("[data-copy]") + const text = copy + ? copy.getAttribute("data-copy")! + : el.innerText + el.removeAttribute("data-md-copying") + return text +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up Clipboard.js integration + * + * @param options - Options + */ +export function setupClipboardJS( + { alert$ }: SetupOptions +): void { + if (ClipboardJS.isSupported()) { + new Observable(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/docs/src/templates/assets/javascripts/integrations/index.ts b/docs/src/templates/assets/javascripts/integrations/index.ts new file mode 100644 index 00000000..5d91a9d5 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/integrations/instant/.eslintrc b/docs/src/templates/assets/javascripts/integrations/instant/.eslintrc new file mode 100644 index 00000000..5adf108a --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/instant/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-self-assign": "off", + "no-null/no-null": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/instant/index.ts b/docs/src/templates/assets/javascripts/integrations/instant/index.ts new file mode 100644 index 00000000..d321b751 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/instant/index.ts @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + Subject, + bufferCount, + catchError, + concat, + debounceTime, + distinctUntilKeyChanged, + endWith, + filter, + fromEvent, + ignoreElements, + map, + of, + sample, + share, + skip, + startWith, + switchMap, + take, + withLatestFrom +} from "rxjs" + +import { configuration, feature } from "~/_" +import { + Viewport, + getElement, + getElements, + getLocation, + getOptionalElement, + request, + setLocation, + setLocationHash +} from "~/browser" +import { getComponentElement } from "~/components" + +import { fetchSitemap } from "../sitemap" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Setup options + */ +interface SetupOptions { + location$: Subject // Location subject + viewport$: Observable // Viewport observable + progress$: Subject // Progress suject +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Create a map of head elements for lookup and replacement + * + * @param head - Document head + * + * @returns Element map + */ +function lookup(head: HTMLHeadElement): Map { + + // @todo When resolving URLs, we must make sure to use the correct base for + // resolution. The next time we refactor instant loading, we should use the + // location subject as a source, which is also used for anchor links tracking, + // but for now we just rely on canonical. + const canonical = getElement("[rel=canonical]", head) + canonical.href = canonical.href.replace("//localhost:", "//127.0.0.1") + + // Create tag map and index elements in head + const tags = new Map() + for (const el of getElements(":scope > *", head)) { + let html = el.outerHTML + + // If the current element is a style sheet or script, we must resolve the + // URL relative to the current location and make it absolute, so it's easy + // to deduplicate it later on by comparing the outer HTML of tags. We must + // keep identical style sheets and scripts without replacing them. + for (const key of ["href", "src"]) { + const value = el.getAttribute(key)! + if (value === null) + continue + + // Resolve URL relative to current location + const url = new URL(value, canonical.href) + const ref = el.cloneNode() as HTMLElement + + // Set resolved URL and retrieve HTML for deduplication + ref.setAttribute(key, `${url}`) + html = ref.outerHTML + break + } + + // Index element in tag map + tags.set(html, el) + } + + // Return tag map + return tags +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up instant navigation + * + * This is a heavily orchestrated operation - see inline comments to learn how + * this works with Material for MkDocs, and how you can hook into it. + * + * @param options - Options + * + * @returns Document observable + */ +export function setupInstantNavigation( + { location$, viewport$, progress$ }: SetupOptions +): Observable { + const config = configuration() + if (location.protocol === "file:") + return EMPTY + + // Load sitemap immediately, so we have it available when the user initiates + // the first instant navigation request, and canonicalize URLs to the current + // base URL. The base URL will remain stable in between loads, as it's only + // read at the first initialization of the application. + const sitemap$ = fetchSitemap() + .pipe( + map(paths => paths.map(path => `${new URL(path, config.base)}`)) + ) + + // Intercept inter-site navigation - to keep the number of event listeners + // low we use the fact that uncaptured events bubble up to the body. This also + // has the nice property that we don't need to detach and then again attach + // event listeners when instant navigation occurs. + const instant$ = fromEvent(document.body, "click") + .pipe( + withLatestFrom(sitemap$), + switchMap(([ev, sitemap]) => { + if (!(ev.target instanceof Element)) + return EMPTY + + // Skip, as target is not within a link - clicks on non-link elements + // are also captured, which we need to exclude from processing + const el = ev.target.closest("a") + if (el === null) + return EMPTY + + // Skip, as link opens in new window - we now know we have captured a + // click on a link, but the link either has a `target` property defined, + // or the user pressed the `meta` or `ctrl` key to open it in a new + // window. Thus, we need to filter those events, too. + if (el.target || ev.metaKey || ev.ctrlKey) + return EMPTY + + // Next, we must check if the URL is relevant for us, i.e., if it's an + // internal link to a page that is managed by MkDocs. Only then we can + // be sure that the structure of the page to be loaded adheres to the + // current document structure and can subsequently be injected into it + // without doing a full reload. For this reason, we must canonicalize + // the URL by removing all search parameters and hash fragments. + const url = new URL(el.href) + url.search = url.hash = "" + + // Skip, if URL is not included in the sitemap - this could be the case + // when linking between versions or languages, or to another page that + // the author included as part of the build, but that is not managed by + // MkDocs. In that case we must not continue with instant navigation. + if (!sitemap.includes(`${url}`)) + return EMPTY + + // We now know that we have a link to an internal page, so we prevent + // the browser from navigation and emit the URL for instant navigation. + // Note that this also includes anchor links, which means we need to + // implement anchor positioning ourselves. The reason for this is that + // if we wouldn't manage anchor links as well, scroll restoration will + // not work correctly (e.g. following an anchor link and scrolling). + ev.preventDefault() + return of(new URL(el.href)) + }), + share() + ) + + // Before fetching for the first time, resolve the absolute favicon position, + // as the browser will try to fetch the icon immediately + instant$.pipe(take(1)) + .subscribe(() => { + const favicon = getOptionalElement("link[rel=icon]") + if (typeof favicon !== "undefined") + favicon.href = favicon.href + }) + + // Enable scroll restoration before window unloads - this is essential to + // ensure that full reloads (F5) restore the viewport offset correctly. If + // only popstate events wouldn't reset the scroll position prior to their + // emission, we could just reset this in popstate. Meh. + fromEvent(window, "beforeunload") + .subscribe(() => { + history.scrollRestoration = "auto" + }) + + // When an instant navigation event occurs, disable scroll restoration, since + // we must normalize and synchronize the behavior across all browsers. For + // instance, when the user clicks the back or forward button, the browser + // would immediately jump to the position of the previous document. + instant$.pipe(withLatestFrom(viewport$)) + .subscribe(([url, { offset }]) => { + history.scrollRestoration = "manual" + + // While it would be better UX to defer the history state change until the + // document was fully fetched and parsed, we must schedule it here, since + // popstate events are emitted when history state changes happen. Moreover + // we need to back up the current viewport offset, so we can restore it + // when popstate events occur, e.g., when the browser's back and forward + // buttons are used for navigation. + history.replaceState(offset, "") + history.pushState(null, "", url) + }) + + // Emit URL that should be fetched via instant navigation on location subject, + // which was passed into this function. Instant navigation can be intercepted + // by other parts of the application, which can synchronously back up or + // restore state before instant navigation happens. + instant$.subscribe(location$) + + // Fetch document - when fetching, we could use `responseType: document`, but + // since all MkDocs links are relative, we need to make sure that the current + // location matches the document we just loaded. Otherwise any relative links + // in the document might use the old location. If the request fails for some + // reason, we fall back to regular navigation and set the location explicitly, + // which will force-load the page. Furthermore, we must pre-warm the buffer + // for the duplicate check, or the first click on an anchor link will also + // trigger an instant navigation event, which doesn't make sense. + const response$ = location$ + .pipe( + startWith(getLocation()), + distinctUntilKeyChanged("pathname"), + skip(1), + switchMap(url => request(url, { progress$ }) + .pipe( + catchError(() => { + setLocation(url, true) + return EMPTY + }) + ) + ) + ) + + // Initialize the DOM parser, parse the returned HTML, and replace selected + // components before handing control down to the application + const dom = new DOMParser() + const document$ = response$ + .pipe( + switchMap(res => res.text()), + switchMap(res => { + const next = dom.parseFromString(res, "text/html") + for (const selector of [ + "[data-md-component=announce]", + "[data-md-component=container]", + "[data-md-component=header-topic]", + "[data-md-component=outdated]", + "[data-md-component=logo]", + "[data-md-component=skip]", + ...feature("navigation.tabs.sticky") + ? ["[data-md-component=tabs]"] + : [] + ]) { + const source = getOptionalElement(selector) + const target = getOptionalElement(selector, next) + if ( + typeof source !== "undefined" && + typeof target !== "undefined" + ) { + source.replaceWith(target) + } + } + + // Update meta tags + const source = lookup(document.head) + const target = lookup(next.head) + for (const [html, el] of target) { + + // Hack: skip stylesheets and scripts until we manage to replace them + // entirely in order to omit flashes of white content @todo refactor + if ( + el.getAttribute("rel") === "stylesheet" || + el.hasAttribute("src") + ) + continue + + if (source.has(html)) { + source.delete(html) + } else { + document.head.appendChild(el) + } + } + + // Remove meta tags that are not present in the new document + for (const el of source.values()) + + // Hack: skip stylesheets and scripts until we manage to replace them + // entirely in order to omit flashes of white content @todo refactor + if ( + el.getAttribute("rel") === "stylesheet" || + el.hasAttribute("src") + ) + continue + else + el.remove() + + // After components and meta tags were replaced, re-evaluate scripts + // that were provided by the author as part of Markdown files + const container = getComponentElement("container") + return concat(getElements("script", container)) + .pipe( + switchMap(el => { + const script = next.createElement("script") + if (el.src) { + for (const name of el.getAttributeNames()) + script.setAttribute(name, el.getAttribute(name)!) + el.replaceWith(script) + + // Complete when script is loaded + return new Observable(observer => { + script.onload = () => observer.complete() + }) + + // Complete immediately + } else { + script.textContent = el.textContent + el.replaceWith(script) + return EMPTY + } + }), + ignoreElements(), + endWith(next) + ) + }), + share() + ) + + // Intercept popstate events, e.g. when using the browser's back and forward + // buttons, and emit new location for fetching and parsing + const popstate$ = fromEvent(window, "popstate") + popstate$.pipe(map(getLocation)) + .subscribe(location$) + + // Intercept clicks on anchor links, and scroll document into position - as + // we disabled scroll restoration, we need to do this manually here + location$ + .pipe( + startWith(getLocation()), + bufferCount(2, 1), + filter(([prev, next]) => ( + prev.pathname === next.pathname && + prev.hash !== next.hash + )), + map(([, next]) => next) + ) + .subscribe(url => { + if (history.state !== null || !url.hash) { + window.scrollTo(0, history.state?.y ?? 0) + } else { + history.scrollRestoration = "auto" + setLocationHash(url.hash) + history.scrollRestoration = "manual" + } + }) + + // Intercept clicks on the same anchor link - we must use a distinct pipeline + // for this, or we'd end up in a loop, setting the hash again and again + location$ + .pipe( + sample(instant$), + startWith(getLocation()), + bufferCount(2, 1), + filter(([prev, next]) => ( + prev.pathname === next.pathname && + prev.hash === next.hash + )), + map(([, next]) => next) + ) + .subscribe(url => { + history.scrollRestoration = "auto" + setLocationHash(url.hash) + history.scrollRestoration = "manual" + + // Hack: we need to make sure that we don't end up with multiple history + // entries for the same anchor link, so we just remove the last entry + history.back() + }) + + // After parsing the document, check if the current history entry has a state. + // This may happen when users press the back or forward button to visit a page + // that was already seen. If there's no state, it means a new page was visited + // and we should scroll to the top, unless an anchor is given. + document$.pipe(withLatestFrom(location$)) + .subscribe(([, url]) => { + if (history.state !== null || !url.hash) { + window.scrollTo(0, history.state?.y ?? 0) + } else { + setLocationHash(url.hash) + } + }) + + // If the current history is not empty, register an event listener updating + // the current history state whenever the scroll position changes. This must + // be debounced and cannot be done in popstate, as popstate has already + // removed the entry from the history. + viewport$ + .pipe( + distinctUntilKeyChanged("offset"), + debounceTime(100) + ) + .subscribe(({ offset }) => { + history.replaceState(offset, "") + }) + + // Return document + return document$ +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/_/index.ts b/docs/src/templates/assets/javascripts/integrations/search/_/index.ts new file mode 100644 index 00000000..0e217fa4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/_/index.ts @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + SearchDocument, + SearchIndex, + SearchOptions, + setupSearchDocumentMap +} from "../config" +import { + Position, + PositionTable, + highlight, + highlightAll, + tokenize +} from "../internal" +import { + SearchQueryTerms, + getSearchQueryTerms, + parseSearchQuery, + segment, + transformSearchQuery +} from "../query" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search item + */ +export interface SearchItem + extends SearchDocument +{ + score: number /* Score (relevance) */ + terms: SearchQueryTerms /* Search query terms */ +} + +/** + * Search result + */ +export interface SearchResult { + items: SearchItem[][] /* Search items */ + suggest?: string[] /* Search suggestions */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create field extractor factory + * + * @param table - Position table map + * + * @returns Extractor factory + */ +function extractor(table: Map) { + return (name: keyof SearchDocument) => { + return (doc: SearchDocument) => { + if (typeof doc[name] === "undefined") + return undefined + + /* Compute identifier and initialize table */ + const id = [doc.location, name].join(":") + table.set(id, lunr.tokenizer.table = []) + + /* Return field value */ + return doc[name] + } + } +} + +/** + * Compute the difference of two lists of strings + * + * @param a - 1st list of strings + * @param b - 2nd list of strings + * + * @returns Difference + */ +function difference(a: string[], b: string[]): string[] { + const [x, y] = [new Set(a), new Set(b)] + return [ + ...new Set([...x].filter(value => !y.has(value))) + ] +} + +/* ---------------------------------------------------------------------------- + * Class + * ------------------------------------------------------------------------- */ + +/** + * Search index + */ +export class Search { + + /** + * Search document map + */ + protected map: Map + + /** + * Search options + */ + protected options: SearchOptions + + /** + * The underlying Lunr.js search index + */ + protected index: lunr.Index + + /** + * Internal position table map + */ + protected table: Map + + /** + * Create the search integration + * + * @param data - Search index + */ + public constructor({ config, docs, options }: SearchIndex) { + const field = extractor(this.table = new Map()) + + /* Set up document map and options */ + this.map = setupSearchDocumentMap(docs) + this.options = options + + /* Set up document index */ + this.index = lunr(function () { + this.metadataWhitelist = ["position"] + this.b(0) + + /* Set up (multi-)language support */ + if (config.lang.length === 1 && config.lang[0] !== "en") { + // @ts-expect-error - namespace indexing not supported + this.use(lunr[config.lang[0]]) + } else if (config.lang.length > 1) { + this.use(lunr.multiLanguage(...config.lang)) + } + + /* Set up custom tokenizer (must be after language setup) */ + this.tokenizer = tokenize as typeof lunr.tokenizer + lunr.tokenizer.separator = new RegExp(config.separator) + + /* Set up custom segmenter, if loaded */ + lunr.segmenter = "TinySegmenter" in lunr + ? new lunr.TinySegmenter() + : undefined + + /* Compute functions to be removed from the pipeline */ + const fns = difference([ + "trimmer", "stopWordFilter", "stemmer" + ], config.pipeline) + + /* Remove functions from the pipeline for registered languages */ + for (const lang of config.lang.map(language => ( + // @ts-expect-error - namespace indexing not supported + language === "en" ? lunr : lunr[language] + ))) + for (const fn of fns) { + this.pipeline.remove(lang[fn]) + this.searchPipeline.remove(lang[fn]) + } + + /* Set up index reference */ + this.ref("location") + + /* Set up index fields */ + this.field("title", { boost: 1e3, extractor: field("title") }) + this.field("text", { boost: 1e0, extractor: field("text") }) + this.field("tags", { boost: 1e6, extractor: field("tags") }) + + /* Add documents to index */ + for (const doc of docs) + this.add(doc, { boost: doc.boost }) + }) + } + + /** + * Search for matching documents + * + * @param query - Search query + * + * @returns Search result + */ + public search(query: string): SearchResult { + + // Experimental Chinese segmentation + query = query.replace(/\p{sc=Han}+/gu, value => { + return [...segment(value, this.index.invertedIndex)] + .join("* ") + }) + + // @todo: move segmenter (above) into transformSearchQuery + query = transformSearchQuery(query) + if (!query) + return { items: [] } + + /* Parse query to extract clauses for analysis */ + const clauses = parseSearchQuery(query) + .filter(clause => ( + clause.presence !== lunr.Query.presence.PROHIBITED + )) + + /* Perform search and post-process results */ + const groups = this.index.search(query) + + /* Apply post-query boosts based on title and search query terms */ + .reduce((item, { ref, score, matchData }) => { + let doc = this.map.get(ref) + if (typeof doc !== "undefined") { + + /* Shallow copy document */ + doc = { ...doc } + if (doc.tags) + doc.tags = [...doc.tags] + + /* Compute and analyze search query terms */ + const terms = getSearchQueryTerms( + clauses, + Object.keys(matchData.metadata) + ) + + /* Highlight matches in fields */ + for (const field of this.index.fields) { + if (typeof doc[field] === "undefined") + continue + + /* Collect positions from matches */ + const positions: Position[] = [] + for (const match of Object.values(matchData.metadata)) + if (typeof match[field] !== "undefined") + positions.push(...match[field].position) + + /* Skip highlighting, if no positions were collected */ + if (!positions.length) + continue + + /* Load table and determine highlighting method */ + const table = this.table.get([doc.location, field].join(":"))! + const fn = Array.isArray(doc[field]) + ? highlightAll + : highlight + + // @ts-expect-error - stop moaning, TypeScript! + doc[field] = fn(doc[field], table, positions, field !== "text") + } + + /* Highlight title and text and apply post-query boosts */ + const boost = +!doc.parent + + Object.values(terms) + .filter(t => t).length / + Object.keys(terms).length + + /* Append item */ + item.push({ + ...doc, + score: score * (1 + boost ** 2), + terms + }) + } + return item + }, []) + + /* Sort search results again after applying boosts */ + .sort((a, b) => b.score - a.score) + + /* Group search results by article */ + .reduce((items, result) => { + const doc = this.map.get(result.location) + if (typeof doc !== "undefined") { + const ref = doc.parent + ? doc.parent.location + : doc.location + items.set(ref, [...items.get(ref) || [], result]) + } + return items + }, new Map()) + + /* Ensure that every item set has an article */ + for (const [ref, items] of groups) + if (!items.find(item => item.location === ref)) { + const doc = this.map.get(ref)! + items.push({ ...doc, score: 0, terms: {} }) + } + + /* Generate search suggestions, if desired */ + let suggest: string[] | undefined + if (this.options.suggest) { + const titles = this.index.query(builder => { + for (const clause of clauses) + builder.term(clause.term, { + fields: ["title"], + presence: lunr.Query.presence.REQUIRED, + wildcard: lunr.Query.wildcard.TRAILING + }) + }) + + /* Retrieve suggestions for best match */ + suggest = titles.length + ? Object.keys(titles[0].matchData.metadata) + : [] + } + + /* Return search result */ + return { + items: [...groups.values()], + ...typeof suggest !== "undefined" && { suggest } + } + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/config/index.ts b/docs/src/templates/assets/javascripts/integrations/search/config/index.ts new file mode 100644 index 00000000..3d88d1c6 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/config/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search configuration + */ +export interface SearchConfig { + lang: string[] /* Search languages */ + separator: string /* Search separator */ + pipeline: SearchPipelineFn[] /* Search pipeline */ +} + +/** + * Search document + */ +export interface SearchDocument { + location: string /* Document location */ + title: string /* Document title */ + text: string /* Document text */ + tags?: string[] /* Document tags */ + boost?: number /* Document boost */ + parent?: SearchDocument /* Document parent */ +} + +/** + * Search options + */ +export interface SearchOptions { + suggest: boolean /* Search suggestions */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search index + */ +export interface SearchIndex { + config: SearchConfig /* Search configuration */ + docs: SearchDocument[] /* Search documents */ + options: SearchOptions /* Search options */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Search pipeline function + */ +type SearchPipelineFn = + | "trimmer" /* Trimmer */ + | "stopWordFilter" /* Stop word filter */ + | "stemmer" /* Stemmer */ + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a search document map + * + * This function creates a mapping of URLs (including anchors) to the actual + * articles and sections. It relies on the invariant that the search index is + * ordered with the main article appearing before all sections with anchors. + * If this is not the case, the logic music be changed. + * + * @param docs - Search documents + * + * @returns Search document map + */ +export function setupSearchDocumentMap( + docs: SearchDocument[] +): Map { + const map = new Map() + for (const doc of docs) { + const [path] = doc.location.split("#") + + /* Add document article */ + const article = map.get(path) + if (typeof article === "undefined") { + map.set(path, doc) + + /* Add document section */ + } else { + map.set(doc.location, doc) + doc.parent = article + } + } + + /* Return search document map */ + return map +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/highlighter/index.ts b/docs/src/templates/assets/javascripts/integrations/search/highlighter/index.ts new file mode 100644 index 00000000..0fcbb19e --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/highlighter/index.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import escapeHTML from "escape-html" + +import { SearchConfig } from "../config" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search highlight function + * + * @param value - Value + * + * @returns Highlighted value + */ +export type SearchHighlightFn = (value: string) => string + +/** + * Search highlight factory function + * + * @param query - Query value + * + * @returns Search highlight function + */ +export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a search highlighter + * + * @param config - Search configuration + * + * @returns Search highlight factory function + */ +export function setupSearchHighlighter( + config: SearchConfig +): SearchHighlightFactoryFn { + // Hack: temporarily remove pure lookaheads and lookbehinds + const regex = config.separator.split("|").map(term => { + const temp = term.replace(/(\(\?[!=<][^)]+\))/g, "") + return temp.length === 0 ? "�" : term + }) + .join("|") + + const separator = new RegExp(regex, "img") + const highlight = (_: unknown, data: string, term: string) => { + return `${data}${term}` + } + + /* Return factory function */ + return (query: string) => { + query = query + .replace(/[\s*+\-:~^]+/g, " ") + .trim() + + /* Create search term match expression */ + const match = new RegExp(`(^|${config.separator}|)(${ + query + .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") + .replace(separator, "|") + })`, "img") + + /* Highlight string value */ + return value => escapeHTML(value) + .replace(match, highlight) + .replace(/<\/mark>(\s+)]*>/img, "$1") + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/index.ts b/docs/src/templates/assets/javascripts/integrations/search/index.ts new file mode 100644 index 00000000..94c010bb --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" +export * from "./config" +export * from "./highlighter" +export * from "./query" +export * from "./worker" diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/.eslintrc b/docs/src/templates/assets/javascripts/integrations/search/internal/.eslintrc new file mode 100644 index 00000000..9368ceb6 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-fallthrough": "off", + "no-underscore-dangle": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/_/index.ts b/docs/src/templates/assets/javascripts/integrations/search/internal/_/index.ts new file mode 100644 index 00000000..ae8f6104 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/_/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Visitor function + * + * @param start - Start offset + * @param end - End offset + */ +type VisitorFn = ( + start: number, end: number +) => void + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Split a string using the given separator + * + * @param input - Input value + * @param separator - Separator + * @param fn - Visitor function + */ +export function split( + input: string, separator: RegExp, fn: VisitorFn +): void { + separator = new RegExp(separator, "g") + + /* Split string using separator */ + let match: RegExpExecArray | null + let index = 0 + do { + match = separator.exec(input) + + /* Emit non-empty range */ + const until = match?.index ?? input.length + if (index < until) + fn(index, until) + + /* Update last index */ + if (match) { + const [term] = match + index = match.index + term.length + + /* Support zero-length lookaheads */ + if (term.length === 0) + separator.lastIndex = match.index + 1 + } + } while (match) +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts b/docs/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts new file mode 100644 index 00000000..2a98b9e1 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Extraction type + * + * This type defines the possible values that are encoded into the first two + * bits of a section that is part of the blocks of a tokenization table. There + * are three types of interest: HTML opening and closing tags, as well as the + * actual text content we need to extract for indexing. + */ +export const enum Extract { + TAG_OPEN = 0, /* HTML opening tag */ + TEXT = 1, /* Text content */ + TAG_CLOSE = 2 /* HTML closing tag */ +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Visitor function + * + * @param block - Block index + * @param type - Extraction type + * @param start - Start offset + * @param end - End offset + */ +type VisitorFn = ( + block: number, type: Extract, start: number, end: number +) => void + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Split a string into markup and text sections + * + * This function scans a string and divides it up into sections of markup and + * text. For each section, it invokes the given visitor function with the block + * index, extraction type, as well as start and end offsets. Using a visitor + * function (= streaming data) is ideal for minimizing pressure on the GC. + * + * @param input - Input value + * @param fn - Visitor function + */ +export function extract( + input: string, fn: VisitorFn +): void { + + let block = 0 /* Current block */ + let start = 0 /* Current start offset */ + let end = 0 /* Current end offset */ + + /* Split string into sections */ + for (let stack = 0; end < input.length; end++) { + + /* Opening tag after non-empty section */ + if (input.charAt(end) === "<" && end > start) { + fn(block, Extract.TEXT, start, start = end) + + /* Closing tag */ + } else if (input.charAt(end) === ">") { + if (input.charAt(start + 1) === "/") { + if (--stack === 0) + fn(block++, Extract.TAG_CLOSE, start, end + 1) + + /* Tag is not self-closing */ + } else if (input.charAt(end - 1) !== "/") { + if (stack++ === 0) + fn(block, Extract.TAG_OPEN, start, end + 1) + } + + /* New section */ + start = end + 1 + } + } + + /* Add trailing section */ + if (end > start) + fn(block, Extract.TEXT, start, end) +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts b/docs/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts new file mode 100644 index 00000000..7cc3bf1a --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Position table + */ +export type PositionTable = number[][] + +/** + * Position + */ +export type Position = number + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Highlight all occurrences in a string + * + * This function receives a field's value (e.g. like `title` or `text`), it's + * position table that was generated during indexing, and the positions found + * when executing the query. It then highlights all occurrences, and returns + * their concatenation. In case of multiple blocks, two are returned. + * + * @param input - Input value + * @param table - Table for indexing + * @param positions - Occurrences + * @param full - Full results + * + * @returns Highlighted string value + */ +export function highlight( + input: string, table: PositionTable, positions: Position[], full = false +): string { + return highlightAll([input], table, positions, full).pop()! +} + +/** + * Highlight all occurrences in a set of strings + * + * @param inputs - Input values + * @param table - Table for indexing + * @param positions - Occurrences + * @param full - Full results + * + * @returns Highlighted string values + */ +export function highlightAll( + inputs: string[], table: PositionTable, positions: Position[], full = false +): string[] { + + /* Map blocks to input values */ + const mapping = [0] + for (let t = 1; t < table.length; t++) { + const prev = table[t - 1] + const next = table[t] + + /* Check if table points to new block */ + const p = prev[prev.length - 1] >>> 2 & 0x3FF + const q = next[0] >>> 12 + + /* Add block to mapping */ + mapping.push(+(p > q) + mapping[mapping.length - 1]) + } + + /* Highlight strings one after another */ + return inputs.map((input, i) => { + let cursor = 0 + + /* Map occurrences to blocks */ + const blocks = new Map() + for (const p of positions.sort((a, b) => a - b)) { + const index = p & 0xFFFFF + const block = p >>> 20 + if (mapping[block] !== i) + continue + + /* Ensure presence of block group */ + let group = blocks.get(block) + if (typeof group === "undefined") + blocks.set(block, group = []) + + /* Add index to group */ + group.push(index) + } + + /* Just return string, if no occurrences */ + if (blocks.size === 0) + return input + + /* Compute slices */ + const slices: string[] = [] + for (const [block, indexes] of blocks) { + const t = table[block] + + /* Extract positions and length */ + const start = t[0] >>> 12 + const end = t[t.length - 1] >>> 12 + const length = t[t.length - 1] >>> 2 & 0x3FF + + /* Add prefix, if full results are desired */ + if (full && start > cursor) + slices.push(input.slice(cursor, start)) + + /* Extract and highlight slice */ + let slice = input.slice(start, end + length) + for (const j of indexes.sort((a, b) => b - a)) { + + /* Retrieve offset and length of match */ + const p = (t[j] >>> 12) - start + const q = (t[j] >>> 2 & 0x3FF) + p + + /* Wrap occurrence */ + slice = [ + slice.slice(0, p), + "", + slice.slice(p, q), + "", + slice.slice(q) + ].join("") + } + + /* Update cursor */ + cursor = end + length + + /* Append slice and abort if we have two */ + if (slices.push(slice) === 2) + break + } + + /* Add suffix, if full results are desired */ + if (full && cursor < input.length) + slices.push(input.slice(cursor)) + + /* Return highlighted slices */ + return slices.join("") + }) +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/index.ts b/docs/src/templates/assets/javascripts/integrations/search/internal/index.ts new file mode 100644 index 00000000..c752329e --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" +export * from "./extract" +export * from "./highlight" +export * from "./tokenize" diff --git a/docs/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts b/docs/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts new file mode 100644 index 00000000..f5089bc9 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { split } from "../_" +import { + Extract, + extract +} from "../extract" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Split a string or set of strings into tokens + * + * This tokenizer supersedes the default tokenizer that is provided by Lunr.js, + * as it is aware of HTML tags and allows for multi-character splitting. + * + * It takes the given inputs, splits each of them into markup and text sections, + * tokenizes and segments (if necessary) each of them, and then indexes them in + * a table by using a compact bit representation. Bitwise techniques are used + * to write and read from the table during indexing and querying. + * + * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller + * + * @param input - Input value(s) + * + * @returns Tokens + */ +export function tokenize( + input?: string | string[] +): lunr.Token[] { + const tokens: lunr.Token[] = [] + if (typeof input === "undefined") + return tokens + + /* Tokenize strings one after another */ + const inputs = Array.isArray(input) ? input : [input] + for (let i = 0; i < inputs.length; i++) { + const table = lunr.tokenizer.table + const total = table.length + + /* Split string into sections and tokenize content blocks */ + extract(inputs[i], (block, type, start, end) => { + table[block += total] ||= [] + switch (type) { + + /* Handle markup */ + case Extract.TAG_OPEN: + case Extract.TAG_CLOSE: + table[block].push( + start << 12 | + end - start << 2 | + type + ) + break + + /* Handle text content */ + case Extract.TEXT: + const section = inputs[i].slice(start, end) + split(section, lunr.tokenizer.separator, (index, until) => { + + /** + * Apply segmenter after tokenization. Note that the segmenter will + * also split words at word boundaries, which is not what we want, + * so we need to check if we can somehow mitigate this behavior. + */ + if (typeof lunr.segmenter !== "undefined") { + const subsection = section.slice(index, until) + if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) { + const segments = lunr.segmenter.segment(subsection) + for (let s = 0, l = 0; s < segments.length; s++) { + + /* Add block to section */ + table[block] ||= [] + table[block].push( + start + index + l << 12 | + segments[s].length << 2 | + type + ) + + /* Add token with position */ + tokens.push(new lunr.Token( + segments[s].toLowerCase(), { + position: block << 20 | table[block].length - 1 + } + )) + + /* Keep track of length */ + l += segments[s].length + } + return + } + } + + /* Add block to section */ + table[block].push( + start + index << 12 | + until - index << 2 | + type + ) + + /* Add token with position */ + tokens.push(new lunr.Token( + section.slice(index, until).toLowerCase(), { + position: block << 20 | table[block].length - 1 + } + )) + }) + } + }) + } + + /* Return tokens */ + return tokens +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/query/.eslintrc b/docs/src/templates/assets/javascripts/integrations/search/query/.eslintrc new file mode 100644 index 00000000..3031c7e3 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/query/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-control-regex": "off", + "@typescript-eslint/no-explicit-any": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/query/_/index.ts b/docs/src/templates/assets/javascripts/integrations/search/query/_/index.ts new file mode 100644 index 00000000..14482e43 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/query/_/index.ts @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { split } from "../../internal" +import { transform } from "../transform" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search query clause + */ +export interface SearchQueryClause { + presence: lunr.Query.presence /* Clause presence */ + term: string /* Clause term */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Search query terms + */ +export type SearchQueryTerms = Record + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Transform search query + * + * This function lexes the given search query and applies the transformation + * function to each term, preserving markup like `+` and `-` modifiers. + * + * @param query - Search query + * + * @returns Search query + */ +export function transformSearchQuery( + query: string +): string { + + /* Split query terms with tokenizer */ + return transform(query, part => { + const terms: string[] = [] + + /* Initialize lexer and analyze part */ + const lexer = new lunr.QueryLexer(part) + lexer.run() + + /* Extract and tokenize term from lexeme */ + for (const { type, str: term, start, end } of lexer.lexemes) + switch (type) { + + /* Hack: remove colon - see https://bit.ly/3wD3T3I */ + case "FIELD": + if (!["title", "text", "tags"].includes(term)) + part = [ + part.slice(0, end), + " ", + part.slice(end + 1) + ].join("") + break + + /* Tokenize term */ + case "TERM": + split(term, lunr.tokenizer.separator, (...range) => { + terms.push([ + part.slice(0, start), + term.slice(...range), + part.slice(end) + ].join("")) + }) + } + + /* Return terms */ + return terms + }) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Parse a search query for analysis + * + * Lunr.js itself has a bug where it doesn't detect or remove wildcards for + * query clauses, so we must do this here. + * + * @see https://bit.ly/3DpTGtz - GitHub issue + * + * @param value - Query value + * + * @returns Search query clauses + */ +export function parseSearchQuery( + value: string +): SearchQueryClause[] { + const query = new lunr.Query(["title", "text", "tags"]) + const parser = new lunr.QueryParser(value, query) + + /* Parse Search query */ + parser.parse() + for (const clause of query.clauses) { + clause.usePipeline = true + + /* Handle leading wildcard */ + if (clause.term.startsWith("*")) { + clause.wildcard = lunr.Query.wildcard.LEADING + clause.term = clause.term.slice(1) + } + + /* Handle trailing wildcard */ + if (clause.term.endsWith("*")) { + clause.wildcard = lunr.Query.wildcard.TRAILING + clause.term = clause.term.slice(0, -1) + } + } + + /* Return query clauses */ + return query.clauses +} + +/** + * Analyze the search query clauses in regard to the search terms found + * + * @param query - Search query clauses + * @param terms - Search terms + * + * @returns Search query terms + */ +export function getSearchQueryTerms( + query: SearchQueryClause[], terms: string[] +): SearchQueryTerms { + const clauses = new Set(query) + + /* Match query clauses against terms */ + const result: SearchQueryTerms = {} + for (let t = 0; t < terms.length; t++) + for (const clause of clauses) + if (terms[t].startsWith(clause.term)) { + result[clause.term] = true + clauses.delete(clause) + } + + /* Annotate unmatched non-stopword query clauses */ + for (const clause of clauses) + if (lunr.stopWordFilter?.(clause.term)) + result[clause.term] = false + + /* Return query terms */ + return result +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/query/index.ts b/docs/src/templates/assets/javascripts/integrations/search/query/index.ts new file mode 100644 index 00000000..763e2fd4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/query/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./_" +export * from "./segment" +export * from "./transform" diff --git a/docs/src/templates/assets/javascripts/integrations/search/query/segment/index.ts b/docs/src/templates/assets/javascripts/integrations/search/query/segment/index.ts new file mode 100644 index 00000000..b96796f4 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/query/segment/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Segment a search query using the inverted index + * + * This function implements a clever approach to text segmentation for Asian + * languages, as it used the information already available in the search index. + * The idea is to greedily segment the search query based on the tokens that are + * already part of the index, as described in the linked issue. + * + * @see https://bit.ly/3lwjrk7 - GitHub issue + * + * @param query - Query value + * @param index - Inverted index + * + * @returns Segmented query value + */ +export function segment( + query: string, index: object +): Iterable { + const segments = new Set() + + /* Segment search query */ + const wordcuts = new Uint16Array(query.length) + for (let i = 0; i < query.length; i++) + for (let j = i + 1; j < query.length; j++) { + const value = query.slice(i, j) + if (value in index) + wordcuts[i] = j - i + } + + /* Compute longest matches with minimum overlap */ + const stack = [0] + for (let s = stack.length; s > 0;) { + const p = stack[--s] + for (let q = 1; q < wordcuts[p]; q++) + if (wordcuts[p + q] > wordcuts[p] - q) { + segments.add(query.slice(p, p + q)) + stack[s++] = p + q + } + + /* Continue at end of query string */ + const q = p + wordcuts[p] + if (wordcuts[q] && q < query.length - 1) + stack[s++] = q + + /* Add current segment */ + segments.add(query.slice(p, q)) + } + + // @todo fix this case in the code block above, this is a hotfix + if (segments.has("")) + return new Set([query]) + + /* Return segmented query value */ + return segments +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/query/transform/index.ts b/docs/src/templates/assets/javascripts/integrations/search/query/transform/index.ts new file mode 100644 index 00000000..41497786 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/query/transform/index.ts @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Visitor function + * + * @param value - String value + * + * @returns String term(s) + */ +type VisitorFn = ( + value: string +) => string | string[] + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Default transformation function + * + * 1. Trim excess whitespace from left and right. + * + * 2. Search for parts in quotation marks and prepend a `+` modifier to denote + * that the resulting document must contain all parts, converting the query + * to an `AND` query (as opposed to the default `OR` behavior). While users + * may expect parts enclosed in quotation marks to map to span queries, i.e. + * for which order is important, Lunr.js doesn't support them, so the best + * we can do is to convert the parts to an `AND` query. + * + * 3. Replace control characters which are not located at the beginning of the + * query or preceded by white space, or are not followed by a non-whitespace + * character or are at the end of the query string. Furthermore, filter + * unmatched quotation marks. + * + * 4. Split the query string at whitespace, then pass each part to the visitor + * function for tokenization, and append a wildcard to every resulting term + * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since + * it ensures consistent and stable ranking when multiple terms are entered. + * Also, if a fuzzy or boost modifier are given, but no numeric value has + * been entered, default to 1 to not induce a query error. + * + * @param query - Query value + * @param fn - Visitor function + * + * @returns Transformed query value + */ +export function transform( + query: string, fn: VisitorFn = term => term +): string { + return query + + /* => 1 */ + .trim() + + /* => 2 */ + .split(/"([^"]+)"/g) + .map((parts, index) => index & 1 + ? parts.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") + : parts + ) + .join("") + + /* => 3 */ + .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") + + /* => 4 */ + .split(/\s+/g) + .reduce((prev, term) => { + const next = fn(term) + return [...prev, ...Array.isArray(next) ? next : [next]] + }, [] as string[]) + .map(term => /([~^]$)/.test(term) ? `${term}1` : term) + .map(term => /(^[+-]|[~^]\d+$)/.test(term) ? term : `${term}*`) + .join(" ") +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/worker/_/index.ts b/docs/src/templates/assets/javascripts/integrations/search/worker/_/index.ts new file mode 100644 index 00000000..26713573 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/worker/_/index.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + ObservableInput, + Subject, + first, + merge, + of, + switchMap +} from "rxjs" + +import { feature } from "~/_" +import { watchToggle, watchWorker } from "~/browser" + +import { SearchIndex } from "../../config" +import { + SearchMessage, + SearchMessageType +} from "../message" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up search worker + * + * This function creates and initializes a web worker that is used for search, + * so that the user interface doesn't freeze. In general, the application does + * not care how search is implemented, as long as the web worker conforms to + * the format expected by the application as defined in `SearchMessage`. This + * allows the author to implement custom search functionality, by providing a + * custom web worker via configuration. + * + * Material for MkDocs' built-in search implementation makes use of Lunr.js, an + * efficient and fast implementation for client-side search. Leveraging a tiny + * iframe-based web worker shim, search is even supported for the `file://` + * protocol, enabling search for local non-hosted builds. + * + * If the protocol is `file://`, search initialization is deferred to mitigate + * freezing, as it's now synchronous by design - see https://bit.ly/3C521EO + * + * @see https://bit.ly/3igvtQv - How to implement custom search + * + * @param url - Worker URL + * @param index$ - Search index observable input + * + * @returns Search worker + */ +export function setupSearchWorker( + url: string, index$: ObservableInput +): Subject { + const worker$ = watchWorker(url) + merge( + of(location.protocol !== "file:"), + watchToggle("search") + ) + .pipe( + first(active => active), + switchMap(() => index$) + ) + .subscribe(({ config, docs }) => worker$.next({ + type: SearchMessageType.SETUP, + data: { + config, + docs, + options: { + suggest: feature("search.suggest") + } + } + })) + + /* Return search worker */ + return worker$ +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/worker/index.ts b/docs/src/templates/assets/javascripts/integrations/search/worker/index.ts new file mode 100644 index 00000000..7120ad6e --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/worker/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc b/docs/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc new file mode 100644 index 00000000..3df9d551 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-console": "off", + "@typescript-eslint/no-misused-promises": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/search/worker/main/index.ts b/docs/src/templates/assets/javascripts/integrations/search/worker/main/index.ts new file mode 100644 index 00000000..2df38080 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/worker/main/index.ts @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import lunr from "lunr" + +import { getElement } from "~/browser/element/_" +import "~/polyfills" + +import { Search } from "../../_" +import { SearchConfig } from "../../config" +import { + SearchMessage, + SearchMessageType +} from "../message" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Add support for `iframe-worker` shim + * + * While `importScripts` is synchronous when executed inside of a web worker, + * it's not possible to provide a synchronous shim implementation. The cool + * thing is that awaiting a non-Promise will convert it into a Promise, so + * extending the type definition to return a `Promise` shouldn't break anything. + * + * @see https://bit.ly/2PjDnXi - GitHub comment + * + * @param urls - Scripts to load + * + * @returns Promise resolving with no result + */ +declare global { + function importScripts(...urls: string[]): Promise | void +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Search index + */ +let index: Search + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Fetch (= import) multi-language support through `lunr-languages` + * + * This function automatically imports the stemmers necessary to process the + * languages which are defined as part of the search configuration. + * + * If the worker runs inside of an `iframe` (when using `iframe-worker` as + * a shim), the base URL for the stemmers to be loaded must be determined by + * searching for the first `script` element with a `src` attribute, which will + * contain the contents of this script. + * + * @param config - Search configuration + * + * @returns Promise resolving with no result + */ +async function setupSearchLanguages( + config: SearchConfig +): Promise { + let base = "../lunr" + + /* Detect `iframe-worker` and fix base URL */ + if (typeof parent !== "undefined" && "IFrameWorker" in parent) { + const worker = getElement("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 { + switch (message.type) { + + /* Search setup message */ + case SearchMessageType.SETUP: + await setupSearchLanguages(message.data.config) + index = new Search(message.data) + return { + type: SearchMessageType.READY + } + + /* Search query message */ + case SearchMessageType.QUERY: + const query = message.data + try { + return { + type: SearchMessageType.RESULT, + data: index.search(query) + } + + /* Return empty result in case of error */ + } catch (err) { + console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) + console.warn(err) + return { + type: SearchMessageType.RESULT, + data: { items: [] } + } + } + + /* All other messages */ + default: + throw new TypeError("Invalid message type") + } +} + +/* ---------------------------------------------------------------------------- + * Worker + * ------------------------------------------------------------------------- */ + +/* Expose Lunr.js in global scope, or stemmers won't work */ +self.lunr = lunr + +/* Handle messages */ +addEventListener("message", async ev => { + postMessage(await handler(ev.data)) +}) diff --git a/docs/src/templates/assets/javascripts/integrations/search/worker/message/index.ts b/docs/src/templates/assets/javascripts/integrations/search/worker/message/index.ts new file mode 100644 index 00000000..54d5001e --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/search/worker/message/index.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { SearchResult } from "../../_" +import { SearchIndex } from "../../config" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Search message type + */ +export const enum SearchMessageType { + SETUP, /* Search index setup */ + READY, /* Search index ready */ + QUERY, /* Search query */ + RESULT /* Search results */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Message containing the data necessary to setup the search index + */ +export interface SearchSetupMessage { + type: SearchMessageType.SETUP /* Message type */ + data: SearchIndex /* Message data */ +} + +/** + * Message indicating the search index is ready + */ +export interface SearchReadyMessage { + type: SearchMessageType.READY /* Message type */ +} + +/** + * Message containing a search query + */ +export interface SearchQueryMessage { + type: SearchMessageType.QUERY /* Message type */ + data: string /* Message data */ +} + +/** + * Message containing results for a search query + */ +export interface SearchResultMessage { + type: SearchMessageType.RESULT /* Message type */ + data: SearchResult /* Message data */ +} + +/* ------------------------------------------------------------------------- */ + +/** + * Message exchanged with the search worker + */ +export type SearchMessage = + | SearchSetupMessage + | SearchReadyMessage + | SearchQueryMessage + | SearchResultMessage + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Type guard for search ready messages + * + * @param message - Search worker message + * + * @returns Test result + */ +export function isSearchReadyMessage( + message: SearchMessage +): message is SearchReadyMessage { + return message.type === SearchMessageType.READY +} + +/** + * Type guard for search result messages + * + * @param message - Search worker message + * + * @returns Test result + */ +export function isSearchResultMessage( + message: SearchMessage +): message is SearchResultMessage { + return message.type === SearchMessageType.RESULT +} diff --git a/docs/src/templates/assets/javascripts/integrations/sitemap/index.ts b/docs/src/templates/assets/javascripts/integrations/sitemap/index.ts new file mode 100644 index 00000000..08695bad --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/sitemap/index.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { + EMPTY, + Observable, + catchError, + defaultIfEmpty, + map, + of, + tap +} from "rxjs" + +import { configuration } from "~/_" +import { getElements, requestXML } from "~/browser" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Sitemap, i.e. a list of URLs + */ +export type Sitemap = string[] + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Preprocess a list of URLs + * + * This function replaces the `site_url` in the sitemap with the actual base + * URL, to allow instant navigation to work in occasions like Netlify previews. + * + * @param urls - URLs + * + * @returns URL path parts + */ +function preprocess(urls: Sitemap): Sitemap { + if (urls.length < 2) + return [""] + + /* Take the first two URLs and remove everything after the last slash */ + const [root, next] = [...urls] + .sort((a, b) => a.length - b.length) + .map(url => url.replace(/[^/]+$/, "")) + + /* Compute common prefix */ + let index = 0 + if (root === next) + index = root.length + else + while (root.charCodeAt(index) === next.charCodeAt(index)) + index++ + + /* Remove common prefix and return in original order */ + return urls.map(url => url.replace(root.slice(0, index), "")) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Fetch the sitemap for the given base URL + * + * @param base - Base URL + * + * @returns Sitemap observable + */ +export function fetchSitemap(base?: URL): Observable { + const cached = __md_get("__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/docs/src/templates/assets/javascripts/integrations/version/.eslintrc b/docs/src/templates/assets/javascripts/integrations/version/.eslintrc new file mode 100644 index 00000000..38a5714d --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/version/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "no-null/no-null": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/integrations/version/index.ts b/docs/src/templates/assets/javascripts/integrations/version/index.ts new file mode 100644 index 00000000..38d78f17 --- /dev/null +++ b/docs/src/templates/assets/javascripts/integrations/version/index.ts @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 subject */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set up version selector + * + * @param options - Options + */ +export function setupVersionSelector( + { document$ }: SetupOptions +): void { + const config = configuration() + const versions$ = requestJSON( + 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(document.body, "click") + .pipe( + filter(ev => !ev.metaKey && !ev.ctrlKey), + withLatestFrom(current$), + switchMap(([ev, current]) => { + if (ev.target instanceof Element) { + const el = ev.target.closest("a") + if (el && !el.target && urls.has(el.href)) { + const url = el.href + // This is a temporary hack to detect if a version inside the + // version selector or on another part of the site was clicked. + // If we're inside the version selector, we definitely want to + // find the same page, as we might have different deployments + // due to aliases. However, if we're outside the version + // selector, we must abort here, because we might otherwise + // interfere with instant navigation. We need to refactor this + // at some point together with instant navigation. + // + // See https://github.com/squidfunk/mkdocs-material/issues/4012 + if (!ev.target.closest(".md-version")) { + const version = urls.get(url)! + if (version === current) + return EMPTY + } + ev.preventDefault() + return of(url) + } + } + return EMPTY + }), + switchMap(url => { + const { version } = urls.get(url)! + return fetchSitemap(new URL(url)) + .pipe( + map(sitemap => { + const location = getLocation() + const path = location.href.replace(config.base, "") + return sitemap.includes(path.split("#")[0]) + ? new URL(`../${version}/${path}`, config.base) + : new URL(url) + }) + ) + }) + ) + ) + ) + .subscribe(url => setLocation(url, true)) + + /* Render version selector and warning */ + combineLatest([versions$, current$]) + .subscribe(([versions, current]) => { + const topic = getElement(".md-header__topic") + topic.appendChild(renderVersionSelector(versions, current)) + }) + + /* Integrate outdated version banner with instant navigation */ + document$.pipe(switchMap(() => current$)) + .subscribe(current => { + + /* Check if version state was already determined */ + let outdated = __md_get("__outdated", sessionStorage) + if (outdated === null) { + outdated = true + + /* Obtain and normalize default versions */ + let ignored = config.version?.default || "latest" + if (!Array.isArray(ignored)) + ignored = [ignored] + + /* Check if version is considered a default */ + main: for (const ignore of ignored) + for (const alias of current.aliases) + if (new RegExp(ignore, "i").test(alias)) { + outdated = false + break main + } + + /* Persist version state in session storage */ + __md_set("__outdated", outdated, sessionStorage) + } + + /* Unhide outdated version banner */ + if (outdated) + for (const warning of getComponentElements("outdated")) + warning.hidden = false + }) +} diff --git a/docs/src/templates/assets/javascripts/patches/indeterminate/index.ts b/docs/src/templates/assets/javascripts/patches/indeterminate/index.ts new file mode 100644 index 00000000..9b7b0d5a --- /dev/null +++ b/docs/src/templates/assets/javascripts/patches/indeterminate/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + tablet$: Observable /* 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( + ".md-toggle--indeterminate" + )), + tap(el => { + el.indeterminate = true + el.checked = false + }), + mergeMap(el => fromEvent(el, "change") + .pipe( + takeWhile(() => el.classList.contains("md-toggle--indeterminate")), + map(() => el) + ) + ), + withLatestFrom(tablet$) + ) + .subscribe(([el, tablet]) => { + el.classList.remove("md-toggle--indeterminate") + if (tablet) + el.checked = false + }) +} diff --git a/docs/src/templates/assets/javascripts/patches/index.ts b/docs/src/templates/assets/javascripts/patches/index.ts new file mode 100644 index 00000000..b6e65fc0 --- /dev/null +++ b/docs/src/templates/assets/javascripts/patches/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/patches/scrollfix/index.ts b/docs/src/templates/assets/javascripts/patches/scrollfix/index.ts new file mode 100644 index 00000000..607c46a0 --- /dev/null +++ b/docs/src/templates/assets/javascripts/patches/scrollfix/index.ts @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 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/docs/src/templates/assets/javascripts/patches/scrolllock/index.ts b/docs/src/templates/assets/javascripts/patches/scrolllock/index.ts new file mode 100644 index 00000000..4ec3e103 --- /dev/null +++ b/docs/src/templates/assets/javascripts/patches/scrolllock/index.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 observable */ + tablet$: Observable /* 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/docs/src/templates/assets/javascripts/polyfills/index.ts b/docs/src/templates/assets/javascripts/polyfills/index.ts new file mode 100644 index 00000000..2aec8290 --- /dev/null +++ b/docs/src/templates/assets/javascripts/polyfills/index.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 + ): void { + const parent = this.parentNode + if (parent) { + if (nodes.length === 0) + parent.removeChild(this) + + /* Replace children and create text nodes */ + for (let i = nodes.length - 1; i >= 0; i--) { + let node = nodes[i] + if (typeof node === "string") + node = document.createTextNode(node) + else if (node.parentNode) + node.parentNode.removeChild(node) + + /* Replace child or insert before previous sibling */ + if (!i) + parent.replaceChild(node, this) + else + parent.insertBefore(this.previousSibling!, node) + } + } + } +} diff --git a/docs/src/templates/assets/javascripts/templates/annotation/index.tsx b/docs/src/templates/assets/javascripts/templates/annotation/index.tsx new file mode 100644 index 00000000..9b8f85f5 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/annotation/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { h } from "~/utilities" + +import { renderTooltip } from "../tooltip" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render an annotation + * + * @param id - Annotation identifier + * @param prefix - Tooltip identifier prefix + * + * @returns Element + */ +export function renderAnnotation( + id: string | number, prefix?: string +): HTMLElement { + prefix = prefix ? `${prefix}_annotation_${id}` : undefined + + /* Render tooltip with anchor, if given */ + if (prefix) { + const anchor = prefix ? `#${prefix}` : undefined + return ( + + ) + } else { + return ( + + ) + } +} diff --git a/docs/src/templates/assets/javascripts/templates/clipboard/index.tsx b/docs/src/templates/assets/javascripts/templates/clipboard/index.tsx new file mode 100644 index 00000000..95dbf12a --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/clipboard/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( + + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/index.ts b/docs/src/templates/assets/javascripts/templates/index.ts new file mode 100644 index 00000000..b50b93b8 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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/docs/src/templates/assets/javascripts/templates/search/index.tsx b/docs/src/templates/assets/javascripts/templates/search/index.tsx new file mode 100644 index 00000000..350c0505 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/search/index.tsx @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { ComponentChild } from "preact" + +import { configuration, feature, translation } from "~/_" +import { SearchItem } from "~/integrations/search" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Render flag + */ +const enum Flag { + TEASER = 1, /* Render teaser */ + PARENT = 2 /* Render as parent */ +} + +/* ---------------------------------------------------------------------------- + * Helper function + * ------------------------------------------------------------------------- */ + +/** + * Render a search document + * + * @param document - Search document + * @param flag - Render flags + * + * @returns Element + */ +function renderSearchDocument( + document: SearchItem, flag: Flag +): HTMLElement { + const parent = flag & Flag.PARENT + const teaser = flag & Flag.TEASER + + /* Render missing query terms */ + const missing = Object.keys(document.terms) + .filter(key => !document.terms[key]) + .reduce((list, key) => [ + ...list, {key}, " " + ], []) + .slice(0, -1) + + /* Assemble query string for highlighting */ + const config = configuration() + const url = new URL(document.location, config.base) + if (feature("search.highlight")) + url.searchParams.set("h", Object.entries(document.terms) + .filter(([, match]) => match) + .reduce((highlight, [value]) => `${highlight} ${value}`.trim(), "") + ) + + /* Render article or section, depending on flags */ + const { tags } = configuration() + return ( + +
    + {parent > 0 &&
    } + {parent > 0 &&

    {document.title}

    } + {parent <= 0 &&

    {document.title}

    } + {teaser > 0 && document.text.length > 0 && + document.text + } + {document.tags && document.tags.map(tag => { + const type = tags + ? tag in tags + ? `md-tag-icon md-tag--${tags[tag]}` + : "md-tag-icon" + : "" + return ( + {tag} + ) + })} + {teaser > 0 && missing.length > 0 && +

    + {translation("search.result.term.missing")}: {...missing} +

    + } +
    +
    + ) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render a search result + * + * @param result - Search result + * + * @returns Element + */ +export function renderSearchResultItem( + result: SearchItem[] +): HTMLElement { + const threshold = result[0].score + const docs = [...result] + + const config = configuration() + + /* Find and extract parent article */ + const parent = docs.findIndex(doc => { + const l = `${new URL(doc.location, config.base)}` // @todo hacky + return !l.includes("#") + }) + const [article] = docs.splice(parent, 1) + + /* Determine last index above threshold */ + let index = docs.findIndex(doc => doc.score < threshold) + if (index === -1) + index = docs.length + + /* Partition sections */ + const best = docs.slice(0, index) + const more = docs.slice(index) + + /* Render children */ + const children = [ + renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)), + ...best.map(section => renderSearchDocument(section, Flag.TEASER)), + ...more.length ? [ +
    + +
    + {more.length > 0 && more.length === 1 + ? translation("search.result.more.one") + : translation("search.result.more.other", more.length) + } +
    +
    + {...more.map(section => renderSearchDocument(section, Flag.TEASER))} +
    + ] : [] + ] + + /* Render search result */ + return ( +
  • + {children} +
  • + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/source/index.tsx b/docs/src/templates/assets/javascripts/templates/source/index.tsx new file mode 100644 index 00000000..b59a8f67 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/source/index.tsx @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( +
      + {Object.entries(facts).map(([key, value]) => ( +
    • + {typeof value === "number" ? round(value) : value} +
    • + ))} +
    + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/tabbed/index.tsx b/docs/src/templates/assets/javascripts/templates/tabbed/index.tsx new file mode 100644 index 00000000..b283ac66 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/tabbed/index.tsx @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( + + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/table/index.tsx b/docs/src/templates/assets/javascripts/templates/table/index.tsx new file mode 100644 index 00000000..1fcba152 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/table/index.tsx @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( +
    +
    + {table} +
    +
    + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/tooltip/index.tsx b/docs/src/templates/assets/javascripts/templates/tooltip/index.tsx new file mode 100644 index 00000000..ec583490 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/tooltip/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render a tooltip + * + * @param id - Tooltip identifier + * + * @returns Element + */ +export function renderTooltip(id?: string): HTMLElement { + return ( +
    +
    +
    + ) +} diff --git a/docs/src/templates/assets/javascripts/templates/version/index.tsx b/docs/src/templates/assets/javascripts/templates/version/index.tsx new file mode 100644 index 00000000..4aff7aa7 --- /dev/null +++ b/docs/src/templates/assets/javascripts/templates/version/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 ( +
  • + + {version.title} + +
  • + ) +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Render a version selector + * + * @param versions - Versions + * @param active - Active version + * + * @returns Element + */ +export function renderVersionSelector( + versions: Version[], active: Version +): HTMLElement { + return ( +
    + +
      + {versions.map(renderVersion)} +
    +
    + ) +} diff --git a/docs/src/templates/assets/javascripts/utilities/h/.eslintrc b/docs/src/templates/assets/javascripts/utilities/h/.eslintrc new file mode 100644 index 00000000..d79b45b0 --- /dev/null +++ b/docs/src/templates/assets/javascripts/utilities/h/.eslintrc @@ -0,0 +1,7 @@ +{ + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-namespace": "off", + "jsdoc/require-jsdoc": "off" + } +} diff --git a/docs/src/templates/assets/javascripts/utilities/h/index.ts b/docs/src/templates/assets/javascripts/utilities/h/index.ts new file mode 100644 index 00000000..08d809f1 --- /dev/null +++ b/docs/src/templates/assets/javascripts/utilities/h/index.ts @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION 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 + +/** + * Child element + */ +type Child = + | ChildNode + | HTMLElement + | Text + | string + | number + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Append a child node to an element + * + * @param el - Element + * @param child - Child node(s) + */ +function appendChild(el: HTMLElement, child: Child | Child[]): void { + + /* Handle primitive types (including raw HTML) */ + if (typeof child === "string" || typeof child === "number") { + el.innerHTML += child.toString() + + /* Handle nodes */ + } else if (child instanceof Node) { + el.appendChild(child) + + /* Handle nested children */ + } else if (Array.isArray(child)) { + for (const node of child) + appendChild(el, node) + } +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * JSX factory + * + * @template T - Element type + * + * @param tag - HTML tag + * @param attributes - HTML attributes + * @param children - Child elements + * + * @returns Element + */ +export function h( + tag: T, attributes?: Attributes | null, ...children: Child[] +): HTMLElementTagNameMap[T] + +export function h( + tag: string, attributes?: Attributes | null, ...children: Child[] +): T + +export function h( + 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/docs/src/templates/assets/javascripts/utilities/index.ts b/docs/src/templates/assets/javascripts/utilities/index.ts new file mode 100644 index 00000000..42886e0b --- /dev/null +++ b/docs/src/templates/assets/javascripts/utilities/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +export * from "./h" +export * from "./round" diff --git a/docs/src/templates/assets/javascripts/utilities/round/index.ts b/docs/src/templates/assets/javascripts/utilities/round/index.ts new file mode 100644 index 00000000..3e6bf91a --- /dev/null +++ b/docs/src/templates/assets/javascripts/utilities/round/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Round a number for display with repository facts + * + * This is a reverse-engineered version of GitHub's weird rounding algorithm + * for stars, forks and all other numbers. While all numbers below `1,000` are + * returned as-is, bigger numbers are converted to fixed numbers: + * + * - `1,049` => `1k` + * - `1,050` => `1.1k` + * - `1,949` => `1.9k` + * - `1,950` => `2k` + * + * @param value - Original value + * + * @returns Rounded value + */ +export function round(value: number): string { + if (value > 999) { + const digits = +((value - 950) % 1000 > 99) + return `${((value + 0.000001) / 1000).toFixed(digits)}k` + } else { + return value.toString() + } +} diff --git a/docs/src/templates/assets/javascripts/workers/search.ts b/docs/src/templates/assets/javascripts/workers/search.ts new file mode 100644 index 00000000..e995b1ff --- /dev/null +++ b/docs/src/templates/assets/javascripts/workers/search.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-2023 Martin Donath + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE 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/docs/src/templates/assets/stylesheets/_config.scss b/docs/src/templates/assets/stylesheets/_config.scss new file mode 100644 index 00000000..e64b8e29 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/_config.scss @@ -0,0 +1,42 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Variables: breakpoints +// ---------------------------------------------------------------------------- + +// Device-specific breakpoints +$break-devices: ( + mobile: ( + portrait: px2em(220px) px2em(479.75px), + landscape: px2em(480px) px2em(719.75px) + ), + tablet: ( + portrait: px2em(720px) px2em(959.75px), + landscape: px2em(960px) px2em(1219.75px) + ), + screen: ( + small: px2em(1220px) px2em(1599.75px), + medium: px2em(1600px) px2em(1999.75px), + large: px2em(2000px) + ) +); diff --git a/docs/src/templates/assets/stylesheets/main.scss b/docs/src/templates/assets/stylesheets/main.scss new file mode 100644 index 00000000..2b203d3d --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main.scss @@ -0,0 +1,86 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Dependencies +// ---------------------------------------------------------------------------- + +@import "material-color"; +@import "material-shadows"; + +// ---------------------------------------------------------------------------- +// Local imports +// ---------------------------------------------------------------------------- + +@import "utilities/break"; +@import "utilities/convert"; + +@import "config"; + +@import "main/resets"; +@import "main/colors"; +@import "main/icons"; +@import "main/typeset"; + +@import "main/components/author"; +@import "main/components/banner"; +@import "main/components/base"; +@import "main/components/clipboard"; +@import "main/components/consent"; +@import "main/components/content"; +@import "main/components/dialog"; +@import "main/components/feedback"; +@import "main/components/footer"; +@import "main/components/form"; +@import "main/components/header"; +@import "main/components/meta"; +@import "main/components/nav"; +@import "main/components/pagination"; +@import "main/components/post"; +@import "main/components/progress"; +@import "main/components/search"; +@import "main/components/select"; +@import "main/components/sidebar"; +@import "main/components/source"; +@import "main/components/status"; +@import "main/components/tabs"; +@import "main/components/tag"; +@import "main/components/tooltip"; +@import "main/components/top"; +@import "main/components/version"; + +@import "main/extensions/markdown/admonition"; +@import "main/extensions/markdown/footnotes"; +@import "main/extensions/markdown/toc"; + +@import "main/extensions/pymdownx/arithmatex"; +@import "main/extensions/pymdownx/critic"; +@import "main/extensions/pymdownx/details"; +@import "main/extensions/pymdownx/emoji"; +@import "main/extensions/pymdownx/highlight"; +@import "main/extensions/pymdownx/keys"; +@import "main/extensions/pymdownx/tabbed"; +@import "main/extensions/pymdownx/tasklist"; + +@import "main/integrations/mermaid"; + +@import "main/modifiers"; diff --git a/docs/src/templates/assets/stylesheets/main/_colors.scss b/docs/src/templates/assets/stylesheets/main/_colors.scss new file mode 100644 index 00000000..68969fe9 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/_colors.scss @@ -0,0 +1,153 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Color variables +:root { + @extend %root; + + // Primary color shades + --md-primary-fg-color: hsla(#{hex2hsl($clr-indigo-500)}, 1); + --md-primary-fg-color--light: hsla(#{hex2hsl($clr-indigo-400)}, 1); + --md-primary-fg-color--dark: hsla(#{hex2hsl($clr-indigo-700)}, 1); + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + + // Accent color shades + --md-accent-fg-color: hsla(#{hex2hsl($clr-indigo-a200)}, 1); + --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1); + --md-accent-bg-color: hsla(0, 0%, 100%, 1); + --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); +} + +// ---------------------------------------------------------------------------- + +// Allow to explicitly use color schemes in nested content +[data-md-color-scheme="default"] { + @extend %root; + + // Indicate that the site is rendered with a light color scheme + color-scheme: light; + + // Hide images for dark mode + img[src$="#only-dark"], + img[src$="#gh-dark-mode-only"] { + display: none; + } +} + +// ---------------------------------------------------------------------------- +// Placeholders +// ---------------------------------------------------------------------------- + +// Default theme, i.e. light mode +%root { + + // Color hue in the range [0,360] - change this variable to alter the tone + // of the theme, e.g. to make it more redish or greenish + --md-hue: 225deg; + + // Default color shades + --md-default-fg-color: hsla(0, 0%, 0%, 0.87); + --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54); + --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32); + --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07); + --md-default-bg-color: hsla(0, 0%, 100%, 1); + --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7); + --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3); + --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12); + + // Code color shades + --md-code-fg-color: hsla(200, 18%, 26%, 1); + --md-code-bg-color: hsla(200, 0%, 96%, 1); + + // Code highlighting color shades + --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 1); + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.1); + --md-code-hl-number-color: hsla(0, 67%, 50%, 1); + --md-code-hl-special-color: hsla(340, 83%, 47%, 1); + --md-code-hl-function-color: hsla(291, 45%, 50%, 1); + --md-code-hl-constant-color: hsla(250, 63%, 60%, 1); + --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1); + --md-code-hl-string-color: hsla(150, 63%, 30%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + // Typeset color shades + --md-typeset-color: var(--md-default-fg-color); + + // Typeset `a` color shades + --md-typeset-a-color: var(--md-primary-fg-color); + + // Typeset `del` and `ins` color shades + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + // Typeset `kbd` color shades + --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); + --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); + --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); + + // Typeset `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); + + // Typeset `table` color shades + --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 0%, 0.035); + + // Admonition color shades + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + // Warning color shades + --md-warning-fg-color: hsla(0, 0%, 0%, 0.87); + --md-warning-bg-color: hsla(60, 100%, 80%, 1); + + // Footer color shades + --md-footer-fg-color: hsla(0, 0%, 100%, 1); + --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); + --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.45); + --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); + --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); + + // Shadow depth 1 + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + // Shadow depth 2 + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.1), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + // Shadow depth 3 + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); +} diff --git a/docs/src/templates/assets/stylesheets/main/_icons.scss b/docs/src/templates/assets/stylesheets/main/_icons.scss new file mode 100644 index 00000000..9853e93d --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/_icons.scss @@ -0,0 +1,37 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/main/_modifiers.scss b/docs/src/templates/assets/stylesheets/main/_modifiers.scss new file mode 100644 index 00000000..4b2b046a --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/_modifiers.scss @@ -0,0 +1,48 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // [tablet +]: Allow for rendering content as sidebars + @include break-from-device(tablet) { + + // Modifier to float block elements + .inline { + float: inline-start; + width: px2rem(234px); + margin-inline-end: px2rem(16px); + margin-top: 0; + margin-bottom: px2rem(16px); + + // Modifier to move to end (ltr: right, rtl: left) + &.end { + float: inline-end; + margin-inline: px2rem(16px) 0; + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/_resets.scss b/docs/src/templates/assets/stylesheets/main/_resets.scss new file mode 100644 index 00000000..c6fc4b28 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/_resets.scss @@ -0,0 +1,118 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Enforce correct box model and prevent adjustments of font size after +// orientation changes in IE and iOS +html { + box-sizing: border-box; + text-size-adjust: none; +} + +// All elements shall inherit the document default +*, +*::before, +*::after { + box-sizing: inherit; + + // [reduced motion]: Disable all transitions + @media (prefers-reduced-motion) { + transition: none !important; // stylelint-disable-line + } +} + +// Remove margin in all browsers +body { + margin: 0; +} + +// Reset tap outlines on iOS and Android +a, +button, +label, +input { + -webkit-tap-highlight-color: transparent; +} + +// Reset link styles +a { + color: inherit; + text-decoration: none; +} + +// Normalize horizontal separator styles +hr { + box-sizing: content-box; + display: block; + height: px2rem(1px); + padding: 0; + overflow: visible; + border: 0; +} + +// Normalize font-size in all browsers +small { + font-size: 80%; +} + +// Prevent subscript and superscript from affecting line-height +sub, +sup { + line-height: 1em; +} + +// Remove border on image +img { + border-style: none; +} + +// Reset table styles +table { + border-spacing: 0; + border-collapse: separate; +} + +// Reset table cell styles +td, +th { + font-weight: 400; + vertical-align: top; +} + +// Reset button styles +button { + padding: 0; + margin: 0; + font-family: inherit; + font-size: inherit; + background: transparent; + border: 0; +} + +// Reset input styles +input { + border: 0; + outline: none; +} diff --git a/docs/src/templates/assets/stylesheets/main/_typeset.scss b/docs/src/templates/assets/stylesheets/main/_typeset.scss new file mode 100644 index 00000000..1c322859 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/_typeset.scss @@ -0,0 +1,603 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules: font definitions +// ---------------------------------------------------------------------------- + +// Enable font-smoothing in Webkit and FF +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + // Font with fallback for body copy + --md-text-font-family: + var(--md-text-font, _), + -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; + + // Font with fallback for code + --md-code-font-family: + var(--md-code-font, _), + SFMono-Regular, Consolas, Menlo, monospace; +} + +// Define default fonts +body, +input, +aside { + font-family: var(--md-text-font-family); + font-feature-settings: "kern", "liga"; + color: var(--md-typeset-color); +} + +// Define monospaced fonts +code, +pre, +kbd { + font-family: var(--md-code-font-family); + font-feature-settings: "kern"; +} + +// ---------------------------------------------------------------------------- +// Rules: typesetted content +// ---------------------------------------------------------------------------- + +// General variables +:root { + --md-typeset-table-sort-icon: svg-load("material/sort.svg"); + --md-typeset-table-sort-icon--asc: svg-load("material/sort-ascending.svg"); + --md-typeset-table-sort-icon--desc: svg-load("material/sort-descending.svg"); +} + +// ---------------------------------------------------------------------------- + +// Content that is typeset - if possible, all margins, paddings and font sizes +// should be set in ems, so nested blocks (e.g. admonitions) render correctly. +.md-typeset { + font-size: px2rem(16px); + line-height: 1.6; + color-adjust: exact; + + // [print]: We'll use a smaller `font-size` for printing, so code examples + // don't break too early, and `16px` looks too big anyway. + @media print { + font-size: px2rem(13.6px); + } + + // Default spacing + ul, + ol, + dl, + figure, + blockquote, + pre { + margin-block: 1em; + } + + // Headline on level 1 + h1 { + margin: 0 0 px2em(40px, 32px); + font-size: px2em(32px); + font-weight: 300; + line-height: 1.3; + color: var(--md-default-fg-color--light); + letter-spacing: -0.01em; + } + + // Headline on level 2 + h2 { + margin: px2em(40px, 25px) 0 px2em(16px, 25px); + font-size: px2em(25px); + font-weight: 300; + line-height: 1.4; + letter-spacing: -0.01em; + } + + // Headline on level 3 + h3 { + margin: px2em(32px, 20px) 0 px2em(16px, 20px); + font-size: px2em(20px); + font-weight: 400; + line-height: 1.5; + letter-spacing: -0.01em; + } + + // Headline on level 3 following level 2 + h2 + h3 { + margin-top: px2em(16px, 20px); + } + + // Headline on level 4 + h4 { + margin: px2em(16px) 0; + font-weight: 700; + letter-spacing: -0.01em; + } + + // Headline on level 5-6 + h5, + h6 { + margin: px2em(16px, 12.8px) 0; + font-size: px2em(12.8px); + font-weight: 700; + color: var(--md-default-fg-color--light); + letter-spacing: -0.01em; + } + + // Headline on level 5 + h5 { + text-transform: uppercase; + } + + // Horizontal separator + hr { + display: flow-root; + margin: 1.5em 0; + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + } + + // Text link + a { + color: var(--md-typeset-a-color); + word-break: break-word; + + // Also enable color transition on pseudo elements + &, + &::before { + transition: color 125ms; + } + + // Text link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + + // Inline code block + code { + background-color: var(--md-accent-fg-color--transparent); + } + } + + // Inline code block + code { + color: currentcolor; + transition: background-color 125ms; + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + } + + // Code block + code, + pre, + kbd { + font-variant-ligatures: none; + color: var(--md-code-fg-color); + direction: ltr; + + // [print]: Wrap text and hide scollbars + @media print { + white-space: pre-wrap; + } + } + + // Inline code block + code { + padding: 0 px2em(4px, 13.6px); + font-size: px2em(13.6px); + word-break: break-word; + background-color: var(--md-code-bg-color); + border-radius: px2rem(2px); + box-decoration-break: clone; + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + } + + // Unformatted content + pre { + position: relative; + display: flow-root; + line-height: 1.4; + + // Code block + > code { + display: block; + padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); + margin: 0; + overflow: auto; + word-break: normal; + touch-action: auto; + outline-color: var(--md-accent-fg-color); + box-shadow: none; + box-decoration-break: slice; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Code block on hover + &:hover { + scrollbar-color: var(--md-accent-fg-color) transparent; + } + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } + + // Keyboard key + kbd { + display: inline-block; + padding: 0 px2em(8px, 12px); + font-size: px2em(12px); + color: var(--md-default-fg-color); + word-break: break-word; + vertical-align: text-top; + background-color: var(--md-typeset-kbd-color); + border-radius: px2rem(2px); + box-shadow: + 0 px2rem(2px) 0 px2rem(1px) var(--md-typeset-kbd-border-color), + 0 px2rem(2px) 0 var(--md-typeset-kbd-border-color), + 0 px2rem(-2px) px2rem(4px) var(--md-typeset-kbd-accent-color) inset; + } + + // Text highlighting marker + mark { + color: inherit; + word-break: break-word; + background-color: var(--md-typeset-mark-color); + box-decoration-break: clone; + } + + // Abbreviation + abbr { + text-decoration: none; + cursor: help; + border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light); + + // Show tooltip for touch devices + @media (hover: none) { + + // Tooltip + &[title]:is(:focus, :hover)::after { + position: absolute; + inset-inline: px2rem(16px); + padding: px2rem(4px) px2rem(6px); + margin-top: 2em; + font-size: px2rem(14px); + color: var(--md-default-bg-color); + content: attr(title); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + } + } + } + + // Small text + small { + opacity: 0.75; + } + + // Superscript and subscript + sup, + sub { + margin-inline-start: px2em(1px, 12.8px); + } + + // Blockquotes, possibly nested + blockquote { + padding-inline-start: px2rem(12px); + margin-inline: 0; + color: var(--md-default-fg-color--light); + border-inline-start: px2rem(4px) solid var(--md-default-fg-color--lighter); + } + + // Unordered list + ul { + list-style-type: disc; + } + + // Unordered and ordered list + ul, + ol { + padding: 0; + margin-inline-start: px2em(10px); + + // Adjust display mode if not hidden + &:not([hidden]) { + display: flow-root; + } + + // Nested ordered list + ol { + list-style-type: lower-alpha; + + // Triply nested ordered list + ol { + list-style-type: lower-roman; + } + } + + // List element + li { + margin-inline-start: px2em(20px); + margin-bottom: 0.5em; + + // Adjust spacing + p, + blockquote { + margin: 0.5em 0; + } + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + + // Nested list + :is(ul, ol) { + margin-block: 0.5em; + margin-inline-start: px2em(10px); + } + } + } + + // Definition list + dd { + margin-block: 1em 1.5em; + margin-inline-start: px2em(30px); + } + + // Image or video + img, + svg, + video { + max-width: 100%; + height: auto; + } + + // Image + img { + + // Adjust spacing when left-aligned + &[align="left"] { + margin: 1em; + margin-left: 0; + } + + // Adjust spacing when right-aligned + &[align="right"] { + margin: 1em; + margin-right: 0; + } + + // Adjust spacing when sole children + &[align]:only-child { + margin-top: 0; + } + } + + // Figure + figure { + display: flow-root; + width: fit-content; + max-width: 100%; + margin: 1em auto; + text-align: center; + + // Figure images + img { + display: block; + } + } + + // Figure caption + figcaption { + max-width: px2rem(480px); + margin: 1em auto; + font-style: italic; + } + + // Limit width to container + iframe { + max-width: 100%; + } + + // Data table + table:not([class]) { + display: inline-block; + max-width: 100%; + overflow: auto; + font-size: px2rem(12.8px); + touch-action: auto; + background-color: var(--md-default-bg-color); + border: px2rem(1px) solid var(--md-typeset-table-color); + border-radius: px2rem(2px); + + // [print]: Reset display mode so table header wraps when printing + @media print { + display: table; + } + + // Due to margin collapse because of the necessary inline-block hack, we + // cannot increase the bottom margin on the table, so we just increase the + // top margin on the following element + + * { + margin-top: 1.5em; + } + + // Elements in table heading and cell + :is(th, td) > * { + + // Adjust spacing on first child + &:first-child { + margin-top: 0; + } + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + } + + // Table heading and cell + :is(th, td):not([align]) { + text-align: left; + + // Adjust for right-to-left languages + [dir="rtl"] & { + text-align: right; + } + } + + // Table heading + th { + min-width: px2rem(100px); + padding: px2em(12px, 12.8px) px2em(16px, 12.8px); + font-weight: 700; + vertical-align: top; + } + + // Table cell + td { + padding: px2em(12px, 12.8px) px2em(16px, 12.8px); + vertical-align: top; + border-top: px2rem(1px) solid var(--md-typeset-table-color); + } + + // Table body row + tbody tr { + transition: background-color 125ms; + + // Table row on hover + &:hover { + background-color: var(--md-typeset-table-color--light); + box-shadow: 0 px2rem(1px) 0 var(--md-default-bg-color) inset; + } + } + + // Text link in table + a { + word-break: normal; + } + } + + // Sortable table + table th[role="columnheader"] { + cursor: pointer; + + // Sort icon + &::after { + display: inline-block; + width: 1.2em; + height: 1.2em; + margin-inline-start: 0.5em; + vertical-align: text-bottom; + content: ""; + transition: background-color 125ms; + mask-image: var(--md-typeset-table-sort-icon); + mask-repeat: no-repeat; + mask-size: contain; + } + + // Show sort icon on hover + &:hover::after { + background-color: var(--md-default-fg-color--lighter); + } + + // Sort ascending icon + &[aria-sort="ascending"]::after { + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-typeset-table-sort-icon--asc); + } + + // Sort descending icon + &[aria-sort="descending"]::after { + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-typeset-table-sort-icon--desc); + } + } + + // Data table scroll wrapper + &__scrollwrap { + margin: 1em px2rem(-16px); + overflow-x: auto; + touch-action: auto; + } + + // Data table wrapper + &__table { + display: inline-block; + padding: 0 px2rem(16px); + margin-bottom: 0.5em; + + // [print]: Reset display mode so table header wraps when printing + @media print { + display: block; + } + + // Data table + html & table { + display: table; + width: 100%; + margin: 0; + overflow: hidden; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level unformatted content + .md-content__inner > pre { + margin: 1em px2rem(-16px); + + // Code block + code { + border-radius: 0; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_author.scss b/docs/src/templates/assets/stylesheets/main/components/_author.scss new file mode 100644 index 00000000..111baf40 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_author.scss @@ -0,0 +1,86 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Author, i.e., GitHub user + .md-author { + position: relative; + display: block; + flex-shrink: 0; + width: px2rem(32px); + height: px2rem(32px); + overflow: hidden; + transition: + color 125ms, + transform 125ms; + + // Author image + img { + display: block; + border-radius: 100%; + } + + // More authors + &--more { + font-size: px2rem(12px); + font-weight: 700; + line-height: px2rem(32px); + color: var(--md-default-fg-color--lighter); + text-align: center; + background: var(--md-default-fg-color--lightest); + } + + // Enlarge image + &--long { + width: px2rem(48px); + height: px2rem(48px); + } + } + + // Author link + a.md-author { + transform: scale(1); + + // Author image + img { + filter: grayscale(100%) opacity(75%); + transition: filter 125ms; + } + + // Author on focus/hover + &:is(:focus, :hover) { + z-index: 1; + transform: scale(1.1); + + // Author image + img { + filter: grayscale(0%); + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_banner.scss b/docs/src/templates/assets/stylesheets/main/components/_banner.scss new file mode 100644 index 00000000..8fe08c0f --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_banner.scss @@ -0,0 +1,68 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Banner for announcements and warnings +.md-banner { + overflow: auto; + color: var(--md-footer-fg-color); + background-color: var(--md-footer-bg-color); + + // [print]: Hide banner + @media print { + display: none; + } + + // Banner with warning + &--warning { + color: var(--md-warning-fg-color); + background-color: var(--md-warning-bg-color); + } + + // Banner wrapper + &__inner { + padding: 0 px2rem(16px); + margin: px2rem(12px) auto; + font-size: px2rem(14px); + } + + // Banner button + &__button { + float: inline-end; + color: inherit; + cursor: pointer; + transition: opacity 250ms; + + // [no-js]: Hide button + .no-js & { + display: none; + } + + // Button on hover + &:hover { + opacity: 0.7; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_base.scss b/docs/src/templates/assets/stylesheets/main/components/_base.scss new file mode 100644 index 00000000..33f834ed --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_base.scss @@ -0,0 +1,182 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules: base grid and containers +// ---------------------------------------------------------------------------- + +// Stretch container to viewport and set base `font-size` +html { + height: 100%; + overflow-x: hidden; + // Hack: normally, we would set the base `font-size` to `62.5%`, so we can + // base all calculations on `10px`, but Chromium and Chrome define a minimal + // `font-size` of `12px` if the system language is set to Chinese. For this + // reason we just double the `font-size` and set it to `20px`. + // + // See https://github.com/squidfunk/mkdocs-material/issues/911 + font-size: 125%; + + // [screen medium +]: Set base `font-size` to `11px` + @include break-from-device(screen medium) { + font-size: 137.5%; + } + + // [screen large +]: Set base `font-size` to `12px` + @include break-from-device(screen large) { + font-size: 150%; + } +} + +// Stretch body to container - flexbox is used, so the footer will always be +// aligned to the bottom of the viewport +body { + position: relative; + display: flex; + flex-direction: column; + width: 100%; + min-height: 100%; + // Hack: reset `font-size` to `10px`, so the spacing for all inline elements + // is correct again. Otherwise the spacing would be based on `20px`. + font-size: px2rem(10px); + background-color: var(--md-default-bg-color); + + // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) + @media print { + display: block; + } + + // Body in locked state + &[data-md-scrolllock] { + + // [tablet portrait -]: Omit scroll bubbling + @include break-to-device(tablet portrait) { + position: fixed; + } + } +} + +// ---------------------------------------------------------------------------- + +// Grid container - this class is applied to wrapper elements within the +// header, content area and footer, and makes sure that their width is limited +// to `1220px`, and they are rendered centered if the screen is larger. +.md-grid { + max-width: px2rem(1220px); + margin-inline: auto; +} + +// Main container +.md-container { + display: flex; + flex-direction: column; + flex-grow: 1; + + // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) + @media print { + display: block; + } +} + +// Main area - stretch to remaining space of container +.md-main { + flex-grow: 1; + + // Main area wrapper + &__inner { + display: flex; + height: 100%; + margin-top: px2rem(24px + 6px); + } +} + +// Add ellipsis in case of overflowing text +.md-ellipsis { + overflow: hidden; + text-overflow: ellipsis; +} + +// ---------------------------------------------------------------------------- +// Rules: navigational elements +// ---------------------------------------------------------------------------- + +// Toggle - this class is applied to checkbox elements, which are used to +// implement the CSS-only drawer and navigation, as well as the search +.md-toggle { + display: none; +} + +// Option - this class is applied to radio elements, which are used to +// implement the color palette toggle +.md-option { + position: absolute; + width: 0; + height: 0; + opacity: 0; + + // Option label for checked radio button + &:checked + label:not([hidden]) { + display: block; + } + + // Show outline for keyboard devices + &.focus-visible + label { + outline-style: auto; + outline-color: var(--md-accent-fg-color); + } +} + +// Skip link +.md-skip { + position: fixed; + // Hack: if we don't set the negative `z-index`, the skip link will force the + // creation of new layers when code blocks are near the header on scrolling + z-index: -1; + padding: px2rem(6px) px2rem(10px); + margin: px2rem(10px); + font-size: px2rem(12.8px); + color: var(--md-default-bg-color); + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + opacity: 0; + transform: translateY(px2rem(8px)); + + // Show skip link on focus + &:focus { + z-index: 10; + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 175ms 75ms; + transform: translateY(0); + } +} + +// ---------------------------------------------------------------------------- +// Rules: print styles +// ---------------------------------------------------------------------------- + +// Add margins to page +@page { + margin: 25mm; +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_clipboard.scss b/docs/src/templates/assets/stylesheets/main/components/_clipboard.scss new file mode 100644 index 00000000..c07c9c67 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_clipboard.scss @@ -0,0 +1,102 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Clipboard button variables +:root { + --md-clipboard-icon: svg-load("material/content-copy.svg"); +} + +// ---------------------------------------------------------------------------- + +// Clipboard button +.md-clipboard { + position: absolute; + top: px2em(8px); + right: px2em(8px); + z-index: 1; + width: px2em(24px); + height: px2em(24px); + color: var(--md-default-fg-color--lightest); + cursor: pointer; + border-radius: px2rem(2px); + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(2px); + transition: color 250ms; + + // [print]: Hide button + @media print { + display: none; + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Darken color on code block hover + :hover > & { + color: var(--md-default-fg-color--light); + } + + // Button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Button icon - the width and height are defined in `em`, so the size is + // automatically adjusted for nested code blocks (e.g. in admonitions) + &::after { + display: block; + width: px2em(18px); + height: px2em(18px); + margin: 0 auto; + content: ""; + background-color: currentcolor; + mask-image: var(--md-clipboard-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Inline clipboard button + &--inline { + cursor: pointer; + + // Code block + code { + transition: + color 250ms, + background-color 250ms; + } + + // Code block on focus/hover + &:is(:focus, :hover) code { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_consent.scss b/docs/src/templates/assets/stylesheets/main/components/_consent.scss new file mode 100644 index 00000000..5502460c --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_consent.scss @@ -0,0 +1,127 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Show consent +@keyframes consent { + 0% { + opacity: 0; + transform: translateY(100%); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +// Show consent overlay +@keyframes overlay { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Consent +.md-consent { + + // Consent overlay + &__overlay { + position: fixed; + top: 0; + z-index: 5; + width: 100%; + height: 100%; + background-color: hsla(0, 0%, 0%, 0.54); + opacity: 1; + backdrop-filter: blur(px2rem(2px)); + animation: overlay 250ms both; + } + + // Consent wrapper + &__inner { + position: fixed; + bottom: 0; + z-index: 5; + width: 100%; + max-height: 100%; + padding: 0; + overflow: auto; + background-color: var(--md-default-bg-color); + border: 0; + border-radius: px2rem(2px); + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); + animation: consent 500ms cubic-bezier(0.1, 0.7, 0.1, 1) both; + } + + // Consent form + &__form { + padding: px2rem(16px); + } + + // Consent settings + &__settings { + display: none; + margin: 1em 0; + + // Show settings + input:checked + & { + display: block; + } + } + + // Consent controls + &__controls { + margin-bottom: px2rem(16px); + + // Consent control button + .md-typeset & .md-button { + display: inline; + + // [tablet +]: Align buttons horizontally + @include break-to-device(mobile) { + display: block; + width: 100%; + margin-top: px2rem(8px); + text-align: center; + } + } + } + + // Ensure users realize that labels are clickaböe + label { + cursor: pointer; + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_content.scss b/docs/src/templates/assets/stylesheets/main/components/_content.scss new file mode 100644 index 00000000..7c945749 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_content.scss @@ -0,0 +1,97 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Content area +.md-content { + flex-grow: 1; + // Hack: we must use `min-width: 0`, so the content area is capped by the + // dimensions of its parent. Otherwise, long code blocks might lead to a + // wider content area which will overflow. See https://bit.ly/3bP3f8k + min-width: 0; + + // Content wrapper + &__inner { + padding-top: px2rem(12px); + margin: 0 px2rem(16px) px2rem(24px); + + // [screen +]: Adjust spacing between content area and sidebars + @include break-from-device(screen) { + + // Sidebar with navigation is visible + .md-sidebar--primary:not([hidden]) ~ .md-content > & { + margin-inline-start: px2rem(24px); + } + + // Sidebar with table of contents is visible + .md-sidebar--secondary:not([hidden]) ~ .md-content > & { + margin-inline-end: px2rem(24px); + } + } + + // Hack: add pseudo element for spacing, as the overflow of the content + // container may not be hidden due to an imminent offset error on targets + &::before { + display: block; + height: px2rem(8px); + content: ""; + } + + // Adjust spacing on last child + > :last-child { + margin-bottom: 0; + } + } + + // Button inside of the content area - these buttons are meant for actions on + // a document-level, i.e. linking to related source code files, printing etc. + &__button { + float: inline-end; + padding: 0; + margin: px2rem(8px) 0; + margin-inline-start: px2rem(8px); + + // [print]: Hide buttons + @media print { + display: none; + } + + // Adjust default link color for icons + .md-typeset & { + color: var(--md-default-fg-color--lighter); + } + + // Align with body copy located next to icon + svg { + display: inline; + vertical-align: top; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: scaleX(-1); + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_dialog.scss b/docs/src/templates/assets/stylesheets/main/components/_dialog.scss new file mode 100644 index 00000000..16782ede --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_dialog.scss @@ -0,0 +1,65 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Dialog +.md-dialog { + position: fixed; + bottom: px2rem(16px); + z-index: 4; + min-width: px2rem(222px); + padding: px2rem(8px) px2rem(12px); + pointer-events: none; + background-color: var(--md-default-fg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z3); + opacity: 0; + transition: + transform 0ms 400ms, + opacity 400ms; + transform: translateY(100%); + inset-inline-end: px2rem(16px); + + // [print]: Hide dialog + @media print { + display: none; + } + + // Active dialog + &--active { + pointer-events: initial; + opacity: 1; + transition: + transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1), + opacity 400ms; + transform: translateY(0); + } + + // Dialog wrapper + &__inner { + font-size: px2rem(14px); + color: var(--md-default-bg-color); + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_feedback.scss b/docs/src/templates/assets/stylesheets/main/components/_feedback.scss new file mode 100644 index 00000000..bbcd00e9 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_feedback.scss @@ -0,0 +1,110 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Was this page helpful? +.md-feedback { + margin: 2em 0 1em; + text-align: center; + + // Feedback fieldset + fieldset { + padding: 0; + margin: 0; + border: none; + } + + // Feedback title + &__title { + margin: 1em auto; + font-weight: 700; + } + + // Feedback wrapper + &__inner { + position: relative; + } + + // Feedback list + &__list { + position: relative; + display: flex; + flex-wrap: wrap; + align-content: baseline; + justify-content: center; + + // Feedback icon on hover + &:hover .md-icon:not(:disabled) { + color: var(--md-default-fg-color--lighter); + } + + // Adjust height after submission + :disabled & { + min-height: px2rem(36px); + } + } + + // Feedback icon + &__icon { + flex-shrink: 0; + margin: 0 px2rem(2px); + color: var(--md-default-fg-color--light); + cursor: pointer; + transition: color 125ms; + + // Feedback icon on hover + &:not(:disabled).md-icon:hover { + color: var(--md-accent-fg-color); + } + + // Feedback icon after submit + &:disabled { + color: var(--md-default-fg-color--lightest); + pointer-events: none; + } + } + + // Feedback note + &__note { + position: relative; + opacity: 0; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: translateY(px2rem(8px)); + + // Feedback note value + > * { + max-width: px2rem(320px); + margin: 0 auto; + } + + // Show after submission + :disabled & { + opacity: 1; + transform: translateY(0); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_footer.scss b/docs/src/templates/assets/stylesheets/main/components/_footer.scss new file mode 100644 index 00000000..9fabc05b --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_footer.scss @@ -0,0 +1,201 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Footer +.md-footer { + color: var(--md-footer-fg-color); + background-color: var(--md-footer-bg-color); + + // [print]: Hide footer + @media print { + display: none; + } + + // Footer wrapper + &__inner { + justify-content: space-between; + padding: px2rem(4px); + overflow: auto; + + // Footer is visible + &:not([hidden]) { + display: flex; + } + } + + // Footer link to previous and next page + &__link { + display: flex; + // Hack: some browsers induce ellipsis on flex children that are set to + // `overflow: hidden` and `text-overflow: ellipsis`. Enforcing growth by + // a tiny factor seems to get rid of the ellipsis and renders the text as + // it should - see https://bit.ly/2ZUCXQ8 + flex-grow: 0.01; + align-items: end; + max-width: 100%; + margin-block: px2rem(20px) px2rem(8px); + overflow: hidden; + outline-color: var(--md-accent-fg-color); + transition: opacity 250ms; + + // Footer link on focus/hover + &:is(:focus, :hover) { + opacity: 0.7; + } + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + + // [mobile -]: Adjust width to 25/75 and hide title + @include break-to-device(mobile) { + + // Footer link to previous page + &--prev { + flex-shrink: 0; + + // Hide footer title + .md-footer__title { + display: none; + } + } + } + + // Footer link to next page + &--next { + margin-inline-start: auto; + text-align: right; + + // Adjust for right-to-left languages + [dir="rtl"] & { + text-align: left; + } + } + } + + // Footer title + &__title { + flex-grow: 1; + max-width: calc(100% - #{px2rem(48px)}); + padding: 0 px2rem(20px); + margin-bottom: px2rem(14px); + font-size: px2rem(18px); + white-space: nowrap; + } + + // Footer link button + &__button { + padding: px2rem(8px); + margin: px2rem(4px); + } + + // Footer link direction (i.e. prev and next) + &__direction { + font-size: px2rem(12.8px); + opacity: 0.7; + } +} + +// Footer metadata +.md-footer-meta { + background-color: var(--md-footer-bg-color--dark); + + // Footer metadata wrapper + &__inner { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: px2rem(4px); + } + + // Lighten color for non-hovered text links + html &.md-typeset a { + color: var(--md-footer-fg-color--light); + + // Text link on focus/hover + &:is(:focus, :hover) { + color: var(--md-footer-fg-color); + } + } +} + +// ---------------------------------------------------------------------------- + +// Copyright and theme information +.md-copyright { + width: 100%; + padding: px2rem(8px) 0; + margin: auto px2rem(12px); + font-size: px2rem(12.8px); + color: var(--md-footer-fg-color--lighter); + + // [tablet portrait +]: Show copyright and social links in one line + @include break-from-device(tablet portrait) { + width: auto; + } + + // Footer copyright highlight - this is the upper part of the copyright and + // theme information, which will include a darker color than the theme link + &__highlight { + color: var(--md-footer-fg-color--light); + } +} + +// ---------------------------------------------------------------------------- + +// Social links +.md-social { + display: inline-flex; + gap: px2rem(4px); + padding: px2rem(4px) 0 px2rem(12px); + margin: 0 px2rem(8px); + + // [tablet portrait +]: Show copyright and social links in one line + @include break-from-device(tablet portrait) { + padding: px2rem(12px) 0; + } + + // Footer social link + &__link { + display: inline-block; + width: px2rem(32px); + height: px2rem(32px); + text-align: center; + + // Adjust line-height to match height for correct alignment + &::before { + line-height: 1.9; + } + + // Fill icon with current color + svg { + max-height: px2rem(16px); + vertical-align: -25%; + fill: currentcolor; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_form.scss b/docs/src/templates/assets/stylesheets/main/components/_form.scss new file mode 100644 index 00000000..49b59e42 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_form.scss @@ -0,0 +1,83 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Form button + .md-button { + display: inline-block; + padding: px2em(10px) px2em(32px); + font-weight: 700; + color: var(--md-primary-fg-color); + cursor: pointer; + border: px2rem(2px) solid currentcolor; + border-radius: px2rem(2px); + transition: + color 125ms, + background-color 125ms, + border-color 125ms; + + // Primary button + &--primary { + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + border-color: var(--md-primary-fg-color); + } + + // Button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + border-color: var(--md-accent-fg-color); + } + } + + // Form input + .md-input { + height: px2rem(36px); + padding: 0 px2rem(12px); + font-size: px2rem(16px); + border-bottom: px2rem(2px) solid var(--md-default-fg-color--lighter); + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + box-shadow: var(--md-shadow-z1); + transition: + border 250ms, + box-shadow 250ms; + + // Input on focus/hover + &:is(:focus, :hover) { + border-bottom-color: var(--md-accent-fg-color); + box-shadow: var(--md-shadow-z2); + } + + // Stretch to full width + &--stretch { + width: 100%; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_header.scss b/docs/src/templates/assets/stylesheets/main/components/_header.scss new file mode 100644 index 00000000..e51f3f99 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_header.scss @@ -0,0 +1,270 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Header - by default, the header will be sticky and stay always on top of the +// viewport. If this behavior is not desired, just set `position: static`. +.md-header { + position: sticky; + inset-inline: 0; + top: 0; + z-index: 4; + display: block; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + // Hack: reduce jitter by adding a transparent box shadow of the same size + // so the size of the layer doesn't change during animation + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0); + + // [print]: Hide header + @media print { + display: none; + } + + // Header is hidden + &[hidden] { + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + box-shadow 250ms; + transform: translateY(-100%); + } + + // Header in shadow state, i.e. shadow is visible + &--shadow { + box-shadow: + 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), + 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + box-shadow 250ms; + } + + // Header wrapper + &__inner { + display: flex; + align-items: center; + padding: 0 px2rem(4px); + } + + // Header button + &__button { + position: relative; + z-index: 1; + padding: px2rem(8px); + margin: px2rem(4px); + color: currentcolor; + vertical-align: middle; + cursor: pointer; + outline-color: var(--md-accent-fg-color); + transition: opacity 250ms; + + // Button on hover + &:hover { + opacity: 0.7; + } + + // Header button is visible + &:not([hidden]) { + display: inline-block; + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Button with logo, pointing to `config.site_url` + &.md-logo { + padding: px2rem(8px); + margin: px2rem(4px); + + // [tablet -]: Hide button + @include break-to-device(tablet) { + display: none; + } + + // Image or icon + :is(img, svg) { + display: block; + width: auto; + height: px2rem(24px); + fill: currentcolor; + } + } + + // Button for search + &[for="__search"] { + + // [tablet landscape +]: Hide button + @include break-from-device(tablet landscape) { + display: none; + } + + // [no-js]: Hide button + .no-js & { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + } + + // Button for drawer + &[for="__drawer"] { + + // [screen +]: Hide button + @include break-from-device(screen) { + display: none; + } + } + } + + // Header topic + &__topic { + position: absolute; + display: flex; + max-width: 100%; + white-space: nowrap; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + + // Second header topic - title of the current page + & + & { + z-index: -1; + pointer-events: none; + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + transform: translateX(px2rem(25px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-25px)); + } + } + + // Adjust font weight of site title + &:first-child { + font-weight: 700; + } + } + + // Header title + &__title { + flex-grow: 1; + height: px2rem(48px); + margin-inline-start: px2rem(20px); + margin-inline-end: px2rem(8px); + font-size: px2rem(18px); + line-height: px2rem(48px); + + // Header title in active state, i.e. page title is visible + &--active .md-header__topic { + z-index: -1; + pointer-events: none; + opacity: 0; + transition: + transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), + opacity 150ms; + transform: translateX(px2rem(-25px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(25px)); + } + + // Second header topic - title of the current page + + .md-header__topic { + z-index: 0; + pointer-events: initial; + opacity: 1; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: translateX(0); + } + } + + // Add ellipsis in case of overflowing text + > .md-header__ellipsis { + position: relative; + width: 100%; + height: 100%; + } + } + + // Header option + &__option { + display: flex; + flex-shrink: 0; + max-width: 100%; + white-space: nowrap; + transition: + max-width 0ms 250ms, + opacity 250ms 250ms; + + // Hide toggle when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + max-width: 0; + opacity: 0; + transition: + max-width 0ms, + opacity 0ms; + } + + // Hack: Firefox 117 introduces a bug where the browser scrolls the page by + // a small amount to the top every time the header button is focused. After + // investigating, we're confident that it seems to be caused by the input + // field being too close to the border - see https://t.ly/APO8l + > input { + bottom: 0; + } + } + + // Repository information container + &__source { + display: none; + + // [tablet landscape +]: Show repository information + @include break-from-device(tablet landscape) { + display: block; + width: px2rem(234px); + max-width: px2rem(234px); + margin-inline-start: px2rem(20px); + } + + // [screen +]: Adjust spacing of search bar + @include break-from-device(screen) { + margin-inline-start: px2rem(28px); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_meta.scss b/docs/src/templates/assets/stylesheets/main/components/_meta.scss new file mode 100644 index 00000000..aaeae8df --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_meta.scss @@ -0,0 +1,67 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Metadata +.md-meta { + font-size: px2rem(14px); + line-height: 1.3; + color: var(--md-default-fg-color--light); + + // Metadata list + &__list { + display: inline-flex; + flex-wrap: wrap; + padding: 0; + margin: 0; + list-style: none; + } + + // Metadata item separator + &__item:not(:last-child)::after { + margin-inline: px2rem(4px); + content: "·"; + } + + // Metadata link + &__link { + color: var(--md-typeset-a-color); + + // Metadata link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } +} + +// Draft +.md-draft { + display: inline-block; + padding-inline: px2em(8px, 14px); + font-weight: 700; + color: hsla(255, 100%, 100%); + background-color: $clr-red-a400; + border-radius: px2em(2px); +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_nav.scss b/docs/src/templates/assets/stylesheets/main/components/_nav.scss new file mode 100644 index 00000000..673918af --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_nav.scss @@ -0,0 +1,761 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Navigation variables +:root { + --md-nav-icon--prev: svg-load("material/arrow-left.svg"); + --md-nav-icon--next: svg-load("material/chevron-right.svg"); + --md-toc-icon: svg-load("material/table-of-contents.svg"); +} + +// ---------------------------------------------------------------------------- + +// Navigation +.md-nav { + font-size: px2rem(14px); + line-height: 1.3; + + // Navigation title + &__title { + display: block; + padding: 0 px2rem(12px); + overflow: hidden; + font-weight: 700; + color: var(--md-default-fg-color--light); + text-overflow: ellipsis; + + // Navigaton button + .md-nav__button { + display: none; + + // Stretch images based on height, as it's the smaller dimension + img { + width: auto; + height: 100%; + } + + // Button with logo, pointing to `config.site_url` + &.md-logo { + + // Image or icon + :is(img, svg) { + display: block; + width: auto; + max-width: 100%; + height: px2rem(48px); + object-fit: contain; + fill: currentcolor; + } + } + } + } + + // Navigation list + &__list { + padding: 0; + margin: 0; + list-style: none; + } + + // Navigation link + &__link { + display: flex; + gap: px2rem(8px); + align-items: flex-start; + margin-top: 0.625em; + transition: color 125ms; + scroll-snap-align: start; + + // Navigation link that was passed + &--passed { + color: var(--md-default-fg-color--light); + } + + // Active link + .md-nav__item &--active { + + // Also enable color transitions on inline code blocks + &, + code { + color: var(--md-typeset-a-color); + } + } + + // Navigation link title + .md-ellipsis { + // Hack: Safari exhibits a bug where the text will sometimes disappear + // and the element will become unclickable. Setting `position: relative` + // seems to fix the issue - see https://bit.ly/3HljM1T + position: relative; + } + + // Always align navigation icons to the end + .md-icon:last-child { + margin-inline-start: auto; + } + + // Navigation link icon + svg { + flex-shrink: 0; + height: 1.3em; + fill: currentcolor; + } + + // Navigation link on focus/hover + &:is([href], [for]):is(:focus, :hover) { + color: var(--md-accent-fg-color); + cursor: pointer; + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Navigation link for table of contents + .md-nav--primary &[for="__toc"] { + display: none; + + // Table of contents icon + .md-icon::after { + display: block; + width: 100%; + height: 100%; + mask-image: var(--md-toc-icon); + background-color: currentcolor; + } + + // Hide table of contents + ~ .md-nav { + display: none; + } + } + } + + // Navigation container (for section index pages) + &__container > .md-nav__link { + margin-top: 0; + + // Stretch first child + &:first-child { + flex-grow: 1; + // Hack: if a very long word is used, it can push the arrow out of sight. + // Setting this property contains the text - see https://t.ly/E02vp + min-width: 0; + } + } + + // Navigation icon + &__icon { + flex-shrink: 0; + } + + // Repository information container + &__source { + display: none; + } + + // [tablet -]: Layered navigation + @include break-to-device(tablet) { + + // Primary and nested navigation + &--primary, + &--primary & { + position: absolute; + inset-inline: 0; + top: 0; + z-index: 1; + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--md-default-bg-color); + } + + // Primary navigation + &--primary { + + // Navigation title and item + :is(.md-nav__title, .md-nav__item) { + font-size: px2rem(16px); + line-height: 1.5; + } + + // Navigation title + .md-nav__title { + position: relative; + height: px2rem(112px); + padding: px2rem(60px) px2rem(16px) px2rem(4px); + line-height: px2rem(48px); + color: var(--md-default-fg-color--light); + white-space: nowrap; + cursor: pointer; + background-color: var(--md-default-fg-color--lightest); + + // Navigation icon + .md-nav__icon { + position: absolute; + top: px2rem(8px); + inset-inline-start: px2rem(8px); + display: block; + width: px2rem(24px); + height: px2rem(24px); + margin: px2rem(4px); + + // Navigation icon in link to previous level + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-nav-icon--prev); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Navigation list + ~ .md-nav__list { + overflow-y: auto; + touch-action: pan-y; + background-color: var(--md-default-bg-color); + box-shadow: + 0 px2rem(1px) 0 var(--md-default-fg-color--lightest) inset; + scroll-snap-type: y mandatory; + + // Omit border on first child + > :first-child { + border-top: 0; + } + } + + // Top-level navigation title + &[for="__drawer"] { + font-weight: 700; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + } + + // Button with logo, pointing to `config.site_url` + .md-logo { + position: absolute; + inset-inline: px2rem(4px); + top: px2rem(4px); + display: block; + padding: px2rem(8px); + margin: px2rem(4px); + } + } + + // Navigation list + .md-nav__list { + flex: 1; + } + + // Navigation item + .md-nav__item { + border-top: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // Navigation link in active navigation + &--active > .md-nav__link { + color: var(--md-typeset-a-color); + + // Navigation link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } + } + + // Navigation link + .md-nav__link { + padding: px2rem(12px) px2rem(16px); + margin-top: 0; + + // Navigation link icon + svg { + margin-top: 0.1em; + } + + // Adjust spacing on nested link + > .md-nav__link { + padding: 0; + } + + // Navigation icon + .md-nav__icon { + width: px2rem(24px); + height: px2rem(24px); + margin-inline-end: px2rem(-4px); + font-size: px2rem(24px); + + // Navigation icon in link to next level + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-nav-icon--next); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + } + + // Flip icon vertically + .md-nav__icon { + + // Adjust for right-to-left languages + [dir="rtl"] &::after { + transform: scale(-1); + } + } + + // Table of contents contained in primary navigation + .md-nav--secondary { + + // Navigation on level 2-6 + .md-nav { + position: static; + background-color: transparent; + + // Navigation link on level 3 + .md-nav__link { + padding-inline-start: px2rem(28px); + } + + // Navigation link on level 4 + .md-nav .md-nav__link { + padding-inline-start: px2rem(40px); + } + + // Navigation link on level 5 + .md-nav .md-nav .md-nav__link { + padding-inline-start: px2rem(52px); + } + + // Navigation link on level 6 + .md-nav .md-nav .md-nav .md-nav__link { + padding-inline-start: px2rem(64px); + } + } + } + } + + // Table of contents + &--secondary { + background-color: transparent; + } + + // Hide nested navigation + &__toggle ~ & { + display: flex; + opacity: 0; + transition: + transform 250ms cubic-bezier(0.8, 0, 0.6, 1), + opacity 125ms 50ms; + transform: translateX(100%); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(-100%); + } + } + + // Show nested navigation when toggle is active + &__toggle:checked ~ & { + opacity: 1; + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms 125ms; + transform: translateX(0); + + // Navigation list + > .md-nav__list { + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + } + } + } + + // [tablet portrait -]: Layered navigation with table of contents + @include break-to-device(tablet portrait) { + + // Show link to table of contents + &--primary &__link[for="__toc"] { + display: flex; + + // Show table of contents icon + .md-icon::after { + content: ""; + } + + // Hide navigation link to current page + + .md-nav__link { + display: none; + } + + // Show table of contents + ~ .md-nav { + display: flex; + } + } + + // Repository information container + &__source { + display: block; + padding: 0 px2rem(4px); + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color--dark); + } + } + + // [tablet landscape]: Layered navigation with table of contents + @include break-at-device(tablet landscape) { + + // Show link to integrated table of contents + &--integrated &__link[for="__toc"] { + display: flex; + + // Show table of contents icon + .md-icon::after { + content: ""; + } + + // Hide navigation link to current page + + .md-nav__link { + display: none; + } + + // Show table of contents + ~ .md-nav { + display: flex; + } + } + } + + // [tablet landscape +]: Tree-like table of contents + @include break-from-device(tablet landscape) { + margin-bottom: px2rem(-8px); + + // Table of contents + &--secondary { + + // Navigation title + .md-nav__title { + position: sticky; + top: 0; + // Hack: because of the hack that we need to make .md-ellipsis work in + // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm + z-index: 1; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Adjust snapping behavior + &[for="__toc"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + + // Adjust spacing for navigation list - same reason as below + .md-nav__list { + padding-inline-start: px2rem(12px); + padding-bottom: px2rem(8px); + } + + // Adjust spacing for navigation link - before this change, we set spacing + // on the left and right of a navigation item, but this led to the problem + // of cropped focus outlines, because we must set `overflow: hidden` on + // the navigation list for smooth expand and collapse transitions. + .md-nav__item > .md-nav__link { + margin-inline-end: px2rem(8px); + } + } + } + + // [screen +]: Tree-like navigation + @include break-from-device(screen) { + margin-bottom: px2rem(-8px); + transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1); + + // Primary navigation + &--primary { + + // Navigation title + .md-nav__title { + position: sticky; + top: 0; + // Hack: because of the hack that we need to make .md-ellipsis work in + // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm + z-index: 1; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Adjust snapping behavior + &[for="__drawer"] { + scroll-snap-align: start; + } + + // Hide navigation icon + .md-nav__icon { + display: none; + } + } + + // Adjust spacing for navigation list - same reason as below + .md-nav__list { + padding-inline-start: px2rem(12px); + padding-bottom: px2rem(8px); + } + + // Adjust spacing for navigation link - before this change, we set spacing + // on the left and right of a navigation item, but this led to the problem + // of cropped focus outlines, because we must set `overflow: hidden` on + // the navigation list for smooth expand and collapse transitions. + .md-nav__item > .md-nav__link { + margin-inline-end: px2rem(8px); + } + } + + // Hide nested navigation + &__toggle ~ & { + display: grid; + grid-template-rows: 0fr; + visibility: collapse; + opacity: 0; + transition: + grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), + opacity 250ms, + visibility 0ms 250ms; + + // Navigation list + > .md-nav__list { + overflow: hidden; + } + } + + // Show nested navigation when toggle is active or indeterminate + &__toggle:is(:checked, :indeterminate) ~ & { + grid-template-rows: 1fr; + visibility: visible; + opacity: 1; + transition: + grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), + opacity 150ms 100ms, + visibility 0ms; + } + + // Hide navigation title in nested navigation + &__item--nested > & > &__title { + display: none; + } + + // Navigation section + &__item--section { + display: block; + margin: 1.25em 0; + + // Adjust spacing on last child + &:last-child { + margin-bottom: 0; + } + + // Show navigation link as title + > .md-nav__link { + font-weight: 700; + + // Make labels discernable from links + &[for] { + color: var(--md-default-fg-color--light); + } + + // Omit clicks if not a section index page + &:not(.md-nav__container) { + pointer-events: none; + } + + // Hide navigation icon + > [for], + .md-icon { + display: none; + } + } + + // Navigation + > .md-nav { + display: block; + margin-inline-start: px2rem(-12px); + visibility: visible; + opacity: 1; + + // Adjust spacing on next level item + > .md-nav__list > .md-nav__item { + padding: 0; + } + } + } + + // Navigation icon + &__icon { + width: px2rem(18px); + height: px2rem(18px); + border-radius: 100%; + transition: background-color 250ms; + + // Navigation icon on hover + &:hover { + background-color: var(--md-accent-fg-color--transparent); + } + + // Navigation icon content + &::after { + display: inline-block; + width: 100%; + height: 100%; + vertical-align: px2rem(-2px); + content: ""; + background-color: currentcolor; + border-radius: 100%; + transition: transform 250ms; + mask-image: var(--md-nav-icon--next); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + + // Navigation icon - rotate icon when toggle is active or indeterminate + .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link &, + .md-nav__item--nested .md-nav__toggle:indeterminate ~ .md-nav__link & { + transform: rotate(90deg); + } + } + } + + // Modifier for when navigation tabs are rendered + &--lifted { + + // Hide site title + > .md-nav__title { + display: none; + } + + // Hide level 0 navigation items + > .md-nav__list > .md-nav__item { + display: none; + + // Active parent navigation item + &--active { + display: block; + + // Show navigation link as title + > .md-nav__link { + position: sticky; + top: 0; + z-index: 1; + margin-top: 0; + background: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); + + // Omit clicks if not a section index page + &:not(.md-nav__container) { + pointer-events: none; + } + } + + // Adjust spacing for navigation section + &.md-nav__item--section { + margin: 0; + } + } + + // Adjust spacing for nested navigation + > .md-nav { + margin-inline-start: px2rem(-12px); + } + + // Make labels discernable from links + > [for] { + color: var(--md-default-fg-color--light); + } + } + + // Hack: Always show active navigation tab on breakpoint screen, despite + // of checkbox being checked or not - see https://t.ly/Qc311 + .md-nav[data-md-level="1"] { + grid-template-rows: 1fr; + visibility: visible; + opacity: 1; + } + } + + // Modifier for when table of contents is rendered in primary navigation + &--integrated > .md-nav__list > .md-nav__item--active { + + // Add spacing to container for non-nested navigation items + &:not(.md-nav__item--nested) { + padding: 0 px2rem(12px); + + // Remove padding as it's given by container + > .md-nav__link { + padding: 0; + } + } + + // Show integrated table of contents + .md-nav--secondary { + display: block; + margin-bottom: 1.25em; + visibility: visible; + border-inline-start: px2rem(1px) solid var(--md-primary-fg-color); + opacity: 1; + + // Navigation list + > .md-nav__list { + padding-bottom: 0; + overflow: visible; + } + + // Hide table of contents title + > .md-nav__title { + display: none; + } + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_pagination.scss b/docs/src/templates/assets/stylesheets/main/components/_pagination.scss new file mode 100644 index 00000000..a010bf43 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_pagination.scss @@ -0,0 +1,85 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Pagination +.md-pagination { + display: flex; + gap: px2rem(8px); + align-items: center; + justify-content: center; + font-size: px2rem(16px); + font-weight: 700; + + // Pagination item + > * { + display: flex; + align-items: center; + justify-content: center; + min-width: px2rem(36px); + height: px2rem(36px); + text-align: center; + border-radius: px2rem(4px); + } + + // Active pagination item + &__current { + color: var(--md-default-fg-color--light); + background-color: var(--md-default-fg-color--lightest); + } + + // Pagination link + &__link { + transition: + color 125ms, + background-color 125ms; + + // Pagination link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + + // Pagination icon + svg { + color: var(--md-accent-fg-color); + } + } + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Pagination icon + svg { + display: block; + width: px2rem(24px); + max-height: 100%; + color: var(--md-default-fg-color--lighter); + fill: currentcolor; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_post.scss b/docs/src/templates/assets/stylesheets/main/components/_post.scss new file mode 100644 index 00000000..cf6ce019 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_post.scss @@ -0,0 +1,196 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Post +.md-post { + + // Post backlink + &__back { + padding-bottom: px2rem(24px); + margin-bottom: px2rem(24px); + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + + // [tablet -]: Hide post backlink + @include break-to-device(tablet) { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + + // Flip icon vertically + svg { + transform: scaleX(-1); + } + } + } + + // Post authors + &__authors { + display: flex; + flex-direction: column; + gap: px2rem(12px); + margin: 0 px2rem(12px) px2rem(24px); + } + + // Post metadata + .md-post__meta { + + // Navigation link + a { + transition: color 125ms; + + // Navigation link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + } + } + + // Post navigation title @todo - generalize + &__title { + font-weight: 700; + color: var(--md-default-fg-color--light); + } + + // Post excerpt + &--excerpt { + margin-bottom: px2rem(64px); + + // Post excerpt header + .md-post__header { + display: flex; + gap: px2rem(12px); + align-items: center; + min-height: px2rem(32px); + } + + // Post excerpt authors + .md-post__authors { + display: inline-flex; + flex-direction: row; + gap: px2rem(4px); + align-items: center; + min-height: px2rem(48px); + margin: 0; + } + + // Post excerpt metadata + .md-post__meta .md-meta__list { + margin-inline-end: px2rem(8px); + } + + // Post excerpt content + .md-post__content > :first-child { + --md-scroll-margin: #{px2rem(120px)}; + + margin-top: 0; + } + } + + // Add margin to table of contents + > .md-nav--secondary { + margin: 1em 0; + } +} + +// ---------------------------------------------------------------------------- + +// Post author profile +.md-profile { + display: flex; + gap: px2rem(12px); + align-items: center; + width: 100%; + font-size: px2rem(14px); + line-height: 1.4; + + // Post author description + &__description { + flex-grow: 1; + } +} + +// ---------------------------------------------------------------------------- + +// Content area for post +.md-content--post { + display: flex; + + // [tablet -]: Switch to inverted column layout + @include break-to-device(tablet) { + flex-flow: column-reverse; + } + + // Content wrapper + > .md-content__inner { + min-width: 0; + + // [screen +]: Adjust spacing between content area and sidebars + @include break-from-device(screen) { + margin-inline-start: px2rem(24px); + } + } +} + +// Sidebar for post +.md-sidebar.md-sidebar--post { + + // [tablet -]: Adjust spacing + @include break-to-device(tablet) { + position: initial; + width: 100%; + padding: 0; + + .md-sidebar__inner { + padding: 0; + } + + .md-post__meta { + margin-inline: px2rem(12px); + } + + .md-nav__item { + display: inline; + border: none; + } + + .md-nav__list { + display: inline-flex; + flex-wrap: wrap; + gap: px2rem(12px); + padding-block: px2rem(12px); + } + + .md-nav__link { + padding: 0; + } + + .md-nav { + position: initial; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_progress.scss b/docs/src/templates/assets/stylesheets/main/components/_progress.scss new file mode 100644 index 00000000..7386ae33 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_progress.scss @@ -0,0 +1,53 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Progress variables +:root { + --md-progress-value: 0; + --md-progress-delay: 400ms; +} + +// ---------------------------------------------------------------------------- + +// Progress indicator +.md-progress { + position: fixed; + top: 0; + z-index: 4; + width: 100%; + height: px2rem(1.5px); + background: var(--md-primary-bg-color); + opacity: + min( + clamp(0, var(--md-progress-value), 1), + clamp(0, 100 - var(--md-progress-value), 1) + ); + transition: + transform 500ms cubic-bezier(0.19, 1, 0.22, 1), + opacity 250ms var(--md-progress-delay); + transform: scaleX(calc(var(--md-progress-value) * 1%)); + transform-origin: left; +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_search.scss b/docs/src/templates/assets/stylesheets/main/components/_search.scss new file mode 100644 index 00000000..e0f36b0c --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_search.scss @@ -0,0 +1,707 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Search variables +:root { + --md-search-result-icon: svg-load("material/file-search-outline.svg"); +} + +// ---------------------------------------------------------------------------- + +// Search +.md-search { + position: relative; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding: px2rem(4px) 0; + } + + // [no-js]: Hide search + .no-js & { + display: none; + } + + // Search overlay + &__overlay { + z-index: 1; + opacity: 0; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + position: absolute; + top: px2rem(-20px); + width: px2rem(40px); + height: px2rem(40px); + overflow: hidden; + pointer-events: none; + background-color: var(--md-default-bg-color); + border-radius: px2rem(20px); + transition: + transform 300ms 100ms, + opacity 200ms 200ms; + transform-origin: center; + inset-inline-start: px2rem(-44px); + + // Show overlay when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + opacity: 1; + transition: + transform 400ms, + opacity 100ms; + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + position: fixed; + top: 0; + width: 0; + height: 0; + cursor: pointer; + background-color: hsla(0, 0%, 0%, 0.54); + transition: + width 0ms 250ms, + height 0ms 250ms, + opacity 250ms; + inset-inline-start: 0; + + // Show overlay when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + width: 100%; + // Hack: when the header is translated upon scrolling, a new layer is + // induced, which means that the height will now refer to the height of + // the header, albeit positioning is fixed. This should be mitigated + // in all cases when setting the height to 2x the viewport. + height: 200vh; + opacity: 1; + transition: + width 0ms, + height 0ms, + opacity 250ms; + } + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + + // [mobile portrait -]: Scale up 45 times + @include break-to-device(mobile portrait) { + transform: scale(45); + } + + // [mobile landscape]: Scale up 60 times + @include break-at-device(mobile landscape) { + transform: scale(60); + } + + // [tablet portrait]: Scale up 75 times + @include break-at-device(tablet portrait) { + transform: scale(75); + } + } + } + + // Search wrapper + &__inner { + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + position: fixed; + top: 0; + z-index: 2; + width: 0; + height: 0; + overflow: hidden; + opacity: 0; + transition: + width 0ms 300ms, + height 0ms 300ms, + transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 150ms 150ms; + transform: translateX(5%); + inset-inline-start: 0; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(-5%); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + width: 100%; + height: 100%; + opacity: 1; + transition: + width 0ms 0ms, + height 0ms 0ms, + transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms 150ms; + transform: translateX(0); + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + position: relative; + float: inline-end; + width: px2rem(234px); + padding: px2rem(2px) 0; + transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + + // [tablet landscape]: Omit overlaying header title + @include break-at-device(tablet landscape) { + width: px2rem(468px); + } + + // [screen +]: Match width of content area + @include break-from-device(screen) { + width: px2rem(688px); + } + } + } + + // Search form + &__form { + position: relative; + z-index: 2; + height: px2rem(48px); + background-color: var(--md-default-bg-color); + box-shadow: 0 0 px2rem(12px) transparent; + transition: + color 250ms, + background-color 250ms; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + height: px2rem(36px); + background-color: hsla(0, 0%, 0%, 0.26); + border-radius: px2rem(2px); + + // Search form on hover + &:hover { + background-color: hsla(0, 0%, 100%, 0.12); + } + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px) px2rem(2px) 0 0; + box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07); + } + } + + // Search input + &__input { + position: relative; + z-index: 2; + width: 100%; + height: 100%; + padding-inline: px2rem(72px) px2rem(44px); + font-size: px2rem(18px); + text-overflow: ellipsis; + background: transparent; + + // Search placeholder + &::placeholder { + transition: color 250ms; + } + + // Search icon and placeholder + ~ .md-search__icon, + &::placeholder { + color: var(--md-default-fg-color--light); + } + + // Remove the "x" rendered by Internet Explorer + &::-ms-clear { + display: none; + } + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + width: 100%; + height: px2rem(48px); + font-size: px2rem(18px); + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + font-size: px2rem(16px); + color: inherit; + + // Search placeholder + &::placeholder { + color: var(--md-primary-bg-color--light); + } + + // Search icon + + .md-search__icon { + color: var(--md-primary-bg-color); + } + + // Adjust appearance when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + text-overflow: clip; + + // Search icon and placeholder + + .md-search__icon { + color: var(--md-default-fg-color--light); + } + + // Search placeholder + &::placeholder { + color: transparent; + } + } + } + } + + // Search icon + &__icon { + display: inline-block; + width: px2rem(24px); + height: px2rem(24px); + cursor: pointer; + transition: + color 250ms, + opacity 250ms; + + // Search icon on hover + &:hover { + opacity: 0.7; + } + + // Search focus button + &[for="__search"] { + position: absolute; + top: px2rem(6px); + inset-inline-start: px2rem(10px); + z-index: 2; + + // Adjust for right-to-left languages + [dir="rtl"] & svg { + transform: scaleX(-1); + } + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(12px); + inset-inline-start: px2rem(16px); + + // Hide the magnifying glass + svg:first-child { + display: none; + } + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + pointer-events: none; + + // Hide the back arrow + svg:last-child { + display: none; + } + } + } + } + + // Search options + &__options { + position: absolute; + top: px2rem(6px); + inset-inline-end: px2rem(10px); + z-index: 2; + pointer-events: none; + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(12px); + inset-inline-end: px2rem(16px); + } + + // Search option buttons + > .md-icon { + margin-inline-start: px2rem(4px); + color: var(--md-default-fg-color--light); + opacity: 0; + transition: + transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 150ms; + transform: scale(0.75); + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Show buttons when search is active and input non-empty + [data-md-toggle="search"]:checked ~ .md-header // stylelint-disable-line + .md-search__input:valid ~ & { + pointer-events: initial; + opacity: 1; + transform: scale(1); + + // Search focus icon + &:hover { + opacity: 0.7; + } + } + } + } + + // Search suggestions + &__suggest { + position: absolute; + top: 0; + display: flex; + align-items: center; + width: 100%; + height: 100%; + padding-inline: px2rem(72px) px2rem(44px); + font-size: px2rem(18px); + color: var(--md-default-fg-color--lighter); + white-space: nowrap; + opacity: 0; + transition: opacity 50ms; + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + font-size: px2rem(16px); + } + + // Show suggestions when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + opacity: 1; + transition: opacity 300ms 100ms; + } + } + + // Search output + &__output { + position: absolute; + z-index: 1; + width: 100%; + overflow: hidden; + border-end-start-radius: px2rem(2px); + border-end-end-radius: px2rem(2px); + + // [tablet portrait -]: Search modal + @include break-to-device(tablet portrait) { + top: px2rem(48px); + bottom: 0; + } + + // [tablet landscape +]: Header-embedded search + @include break-from-device(tablet landscape) { + top: px2rem(38px); + opacity: 0; + transition: opacity 400ms; + + // Show output when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + box-shadow: var(--md-shadow-z3); + opacity: 1; + } + } + } + + // Search scroll wrapper + &__scrollwrap { + height: 100%; + overflow-y: auto; + // Hack: Chrome 88+ has weird overscroll behavior. Overall, scroll snapping + // seems to be something that is not ready for prime time on some browsers. + // scroll-snap-type: y mandatory; + touch-action: pan-y; + background-color: var(--md-default-bg-color); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // Mitigiate excessive repaints on non-retina devices + @media (max-resolution: 1dppx) { + transform: translateZ(0); + } + + // [tablet landscape]: Set fixed width to omit unnecessary reflow + @include break-at-device(tablet landscape) { + width: px2rem(468px); + } + + // [screen +]: Set fixed width to omit unnecessary reflow + @include break-from-device(screen) { + width: px2rem(688px); + } + + // [tablet landscape +]: Limit height to viewport + @include break-from-device(tablet landscape) { + max-height: 0; + scrollbar-width: thin; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Show scroll wrapper when search is active + [data-md-toggle="search"]:checked ~ .md-header & { + max-height: 75vh; + } + + // Search scroll wrapper on hover + &:hover { + scrollbar-color: var(--md-accent-fg-color) transparent; + } + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } +} + +// Search result +.md-search-result { + color: var(--md-default-fg-color); + word-break: break-word; + + // Search result metadata + &__meta { + padding: 0 px2rem(16px); + font-size: px2rem(12.8px); + line-height: px2rem(36px); + color: var(--md-default-fg-color--light); + background-color: var(--md-default-fg-color--lightest); + scroll-snap-align: start; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result list + &__list { + padding: 0; + margin: 0; + list-style: none; + // Hack: omit accidental text selection on fast toggle of more button + user-select: none; + } + + // Search result item + &__item { + box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest); + + // Omit border on first child + &:first-child { + box-shadow: none; + } + } + + // Search result link + &__link { + display: block; + outline: none; + transition: background-color 250ms; + scroll-snap-align: start; + + // Search result link on focus/hover + &:is(:focus, :hover) { + background-color: var(--md-accent-fg-color--transparent); + } + + // Adjust spacing on last child of last link + &:last-child p:last-child { + margin-bottom: px2rem(12px); + } + } + + // Search result more container + &__more > summary { + position: sticky; + top: 0; + z-index: 1; + display: block; + cursor: pointer; + outline: none; + scroll-snap-align: start; + + // Hide native details marker + &::marker { + display: none; + } + + // Hide native details marker - legacy, must be split into a seprate rule, + // so older browsers don't consider the selector list as invalid + &::-webkit-details-marker { + display: none; + } + + // Search result more button + > div { + padding: px2em(12px) px2rem(16px); + font-size: px2rem(12.8px); + color: var(--md-typeset-a-color); + transition: + color 250ms, + background-color 250ms; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result more link on focus/hover + &:is(:focus, :hover) > div { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + } + + // Adjust background for more container in open state + &__more[open] > summary { + background-color: var(--md-default-bg-color); + // box-shadow: 0 px2rem(-1px) hsla(0, 0%, 0%, 0.07) inset; + } + + // Search result article + &__article { + position: relative; + padding: 0 px2rem(16px); + overflow: hidden; + + // [tablet landscape +]: Adjust spacing + @include break-from-device(tablet landscape) { + padding-inline-start: px2rem(44px); + } + } + + // Search result icon + &__icon { + position: absolute; + inset-inline-start: 0; + width: px2rem(24px); + height: px2rem(24px); + margin: px2rem(10px); + color: var(--md-default-fg-color--light); + + // [tablet portrait -]: Hide icon + @include break-to-device(tablet portrait) { + display: none; + } + + // Search result icon content + &::after { + display: inline-block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + mask-image: var(--md-search-result-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: scaleX(-1); + } + } + } + + // Typesetted content + .md-typeset { + font-size: px2rem(12.8px); + line-height: 1.6; + color: var(--md-default-fg-color--light); + + // Search result article title + h1 { + margin: px2rem(11px) 0; + font-size: px2rem(16px); + font-weight: 400; + line-height: 1.4; + color: var(--md-default-fg-color); + + // Search term highlighting + mark { + text-decoration: none; + } + } + + // Search result section title + h2 { + margin: 0.5em 0; + font-size: px2rem(12.8px); + font-weight: 700; + line-height: 1.6; + color: var(--md-default-fg-color); + + // Search term highlighting + mark { + text-decoration: none; + } + } + } + + // Search result terms + &__terms { + display: block; + margin: 0.5em 0; + font-size: px2rem(12.8px); + font-style: italic; + color: var(--md-default-fg-color); + } + + // Search term highlighting + mark { + color: var(--md-accent-fg-color); + text-decoration: underline; + background-color: transparent; + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_select.scss b/docs/src/templates/assets/stylesheets/main/components/_select.scss new file mode 100644 index 00000000..ed597a39 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_select.scss @@ -0,0 +1,115 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Selection +.md-select { + position: relative; + z-index: 1; + + // Selection tooltip + &__inner { + position: absolute; + top: calc(100% - #{px2rem(4px)}); + left: 50%; + max-height: 0; + margin-top: px2rem(4px); + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + transform 250ms 375ms, + opacity 250ms 250ms, + max-height 0ms 500ms; + transform: translate3d(-50%, px2rem(6px), 0); + + // Selection bubble on parent focus/hover + .md-select:is(:focus-within, :hover) & { + max-height: px2rem(200px); + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + max-height 0ms; + transform: translate3d(-50%, 0, 0); + } + + // Selection bubble handle + &::after { + position: absolute; + top: 0; + left: 50%; + width: 0; + height: 0; + margin-top: px2rem(-4px); + margin-left: px2rem(-4px); + content: ""; + border: px2rem(4px) solid transparent; + border-top: 0; + border-bottom-color: var(--md-default-bg-color); + } + } + + // Selection list + &__list { + max-height: inherit; + padding: 0; + margin: 0; + overflow: auto; + font-size: px2rem(16px); + list-style-type: none; + border-radius: px2rem(2px); + } + + // Selection item + &__item { + line-height: px2rem(36px); + } + + // Selection link + &__link { + display: block; + width: 100%; + padding-inline: px2rem(12px) px2rem(24px); + cursor: pointer; + outline: none; + transition: + background-color 250ms, + color 250ms; + scroll-snap-align: start; + + // Link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Link on focus + &:focus { + background-color: var(--md-default-fg-color--lightest); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_sidebar.scss b/docs/src/templates/assets/stylesheets/main/components/_sidebar.scss new file mode 100644 index 00000000..8a320c04 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_sidebar.scss @@ -0,0 +1,209 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Sidebar +.md-sidebar { + position: sticky; + top: px2rem(48px); + flex-shrink: 0; + align-self: flex-start; + width: px2rem(242px); + padding: px2rem(24px) 0; + + // [print]: Hide sidebar + @media print { + display: none; + } + + // Primary sidebar with navigation + &--primary { + + // [tablet -]: Show navigation as drawer + @include break-to-device(tablet) { + position: fixed; + top: 0; + z-index: 5; + display: block; + width: px2rem(242px); + height: 100%; + background-color: var(--md-default-bg-color); + transition: + transform 250ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 250ms; + transform: translateX(0); + inset-inline-start: px2rem(-242px); + + // Show sidebar when drawer is active + [data-md-toggle="drawer"]:checked ~ .md-container & { + box-shadow: var(--md-shadow-z3); + transform: translateX(px2rem(242px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-242px)); + } + } + + // Stretch scroll wrapper for primary sidebar + .md-sidebar__scrollwrap { + position: absolute; + inset: 0; + margin: 0; + scroll-snap-type: none; + overflow: hidden; + } + } + } + + // [screen +]: Show navigation as sidebar + @include break-from-device(screen) { + height: 0; + + // [no-js]: Switch to native sticky behavior + .no-js & { + height: auto; + } + + // Adjust spacing for sticky navigation tabs + .md-header--lifted ~ .md-container & { + top: px2rem(96px); + } + } + + // Secondary sidebar with table of contents + &--secondary { + display: none; + order: 2; + + // [tablet landscape +]: Show table of contents as sidebar + @include break-from-device(tablet landscape) { + height: 0; + + // [no-js]: Switch to native sticky behavior + .no-js & { + height: auto; + } + + // Sidebar is visible + &:not([hidden]) { + display: block; + } + + // Ensure smooth scrolling on iOS + .md-sidebar__scrollwrap { + touch-action: pan-y; + } + } + } + + // Sidebar scroll wrapper + &__scrollwrap { + margin: 0 px2rem(4px); + overflow-y: auto; + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + // Hack: Chrome 81+ exhibits a strange bug, where it scrolls the container + // to the bottom if `scroll-snap-type` is set on the initial render. For + // this reason, we disable scroll snapping until this is resolved (#1667). + // scroll-snap-type: y mandatory; + scrollbar-width: thin; + scrollbar-gutter: stable; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + + // Webkit scrollbar + &::-webkit-scrollbar { + width: px2rem(4px); + height: px2rem(4px); + } + + // Sidebar scroll wrapper on focus/hover + &:is(:focus-within, :hover) { + scrollbar-color: var(--md-accent-fg-color) transparent; + + // Webkit scrollbar thumb + &::-webkit-scrollbar-thumb { + background-color: var(--md-default-fg-color--lighter); + + // Webkit scrollbar thumb on hover + &:hover { + background-color: var(--md-accent-fg-color); + } + } + } + } + + // Hack: the scrollbar is only visible when the sidebar's contents overflow, + // which is nice, but leads to the problem where the chevrons of expandable + // sections will jump by `4px` when the sidebar is shown. We wanted to fix + // this problem for so long, but haven't found a clean way of doing it. + // Until now. The following declaration is only applied to Webkit browsers + // (e.g. Chrome and Safari), which support styling of scrollbars. The trick + // is to add conditional padding on the side of the scrollbar only if the + // sidebar's content doesn't overflow. This hack is inspired and adapted + // from Ayke van Laëthem's year old trick – see https://bit.ly/3Sb1qql + @supports selector(::-webkit-scrollbar) { + + // Sidebar scroll wrapper + &__scrollwrap { + scrollbar-gutter: auto; + } + + // Sidebar wrapper + &__inner { + padding-inline-end: calc(100% - #{px2rem(230px)}); + } + } +} + +// [tablet -]: Show overlay on active drawer +@include break-to-device(tablet) { + + // Drawer overlay + .md-overlay { + position: fixed; + top: 0; + z-index: 5; + width: 0; + height: 0; + background-color: hsla(0, 0%, 0%, 0.54); + opacity: 0; + transition: + width 0ms 250ms, + height 0ms 250ms, + opacity 250ms; + + // Show overlay when drawer is active + [data-md-toggle="drawer"]:checked ~ & { + width: 100%; + height: 100%; + opacity: 1; + transition: + width 0ms, + height 0ms, + opacity 250ms; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_source.scss b/docs/src/templates/assets/stylesheets/main/components/_source.scss new file mode 100644 index 00000000..a2b72009 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_source.scss @@ -0,0 +1,182 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Show repository facts +@keyframes facts { + 0% { + height: 0; + } + + 100% { + height: px2rem(13px); + } +} + +// Show repository fact +@keyframes fact { + 0% { + opacity: 0; + transform: translateY(100%); + } + + 50% { + opacity: 0; + } + + 100% { + opacity: 1; + transform: translateY(0%); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Repository information variables +:root { + --md-source-forks-icon: svg-load("octicons/repo-forked-16.svg"); + --md-source-repositories-icon: svg-load("octicons/repo-16.svg"); + --md-source-stars-icon: svg-load("octicons/star-16.svg"); + --md-source-version-icon: svg-load("octicons/tag-16.svg"); +} + +// ---------------------------------------------------------------------------- + +// Repository information +.md-source { + display: block; + font-size: px2rem(13px); + line-height: 1.2; + white-space: nowrap; + outline-color: var(--md-accent-fg-color); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + transition: opacity 250ms; + + // Repository information on hover + &:hover { + opacity: 0.7; + } + + // Repository icon + &__icon { + display: inline-block; + width: px2rem(40px); + height: px2rem(48px); + vertical-align: middle; + + // Align with margin only (as opposed to normal button alignment) + svg { + margin-inline-start: px2rem(12px); + margin-top: px2rem(12px); + } + + // Adjust spacing if icon is present + + .md-source__repository { + padding-inline-start: px2rem(40px); + margin-inline-start: px2rem(-40px); + } + } + + // Repository name + &__repository { + display: inline-block; + max-width: calc(100% - #{px2rem(24px)}); + margin-inline-start: px2rem(12px); + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + } + + // Repository facts + &__facts { + display: flex; + gap: px2rem(8px); + width: 100%; + padding: 0; + margin: px2rem(2px) 0 0; + overflow: hidden; + font-size: px2rem(11px); + list-style-type: none; + opacity: 0.75; + + // Show after the data was loaded + .md-source__repository--active & { + animation: facts 250ms ease-in; + } + } + + // Repository fact + &__fact { + overflow: hidden; + text-overflow: ellipsis; + + // Show after the data was loaded + .md-source__repository--active & { + animation: fact 400ms ease-out; + } + + // Repository fact icon + &::before { + display: inline-block; + width: px2rem(12px); + height: px2rem(12px); + margin-inline-end: px2rem(2px); + vertical-align: text-top; + content: ""; + background-color: currentcolor; + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Adjust spacing for 2nd+ fact + &:nth-child(1n+2) { + flex-shrink: 0; + } + + // Repository fact: version + &--version::before { + mask-image: var(--md-source-version-icon); + } + + // Repository fact: stars + &--stars::before { + mask-image: var(--md-source-stars-icon); + } + + // Repository fact: forks + &--forks::before { + mask-image: var(--md-source-forks-icon); + } + + // Repository fact: repositories + &--repositories::before { + mask-image: var(--md-source-repositories-icon); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_status.scss b/docs/src/templates/assets/stylesheets/main/components/_status.scss new file mode 100644 index 00000000..9e096021 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_status.scss @@ -0,0 +1,73 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Status variables +:root { + --md-status: svg-load("material/information-outline.svg"); + --md-status--new: svg-load("material/alert-decagram.svg"); + --md-status--deprecated: svg-load("material/trash-can.svg"); + --md-status--encrypted: svg-load("material/shield-lock.svg"); +} + +// ---------------------------------------------------------------------------- + +// Status +.md-status { + + // Status icon + &::after { + display: inline-block; + width: px2em(18px); + height: px2em(18px); + vertical-align: text-bottom; + content: ""; + background-color: var(--md-default-fg-color--light); + mask-image: var(--md-status); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Status icon on hover + &:hover::after { + background-color: currentcolor; + } + + // Status: new + &--new::after { + mask-image: var(--md-status--new); + } + + // Status: deprecated + &--deprecated::after { + mask-image: var(--md-status--deprecated); + } + + // Status: encrypted + &--encrypted::after { + mask-image: var(--md-status--encrypted); + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_tabs.scss b/docs/src/templates/assets/stylesheets/main/components/_tabs.scss new file mode 100644 index 00000000..0da3384b --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_tabs.scss @@ -0,0 +1,133 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Navigation tabs +.md-tabs { + // Must be higher than the z-index of the back-to-top button, or the button + // will overlay the navigation tabs bar when scrolling up fast. + z-index: 3; + display: block; + width: 100%; + overflow: auto; + line-height: 1.3; + color: var(--md-primary-bg-color); + background-color: var(--md-primary-fg-color); + + // [print]: Hide tabs + @media print { + display: none; + } + + // [tablet -]: Hide tabs + @include break-to-device(tablet) { + display: none; + } + + // Navigation tabs are hidden + &[hidden] { + pointer-events: none; + } + + // Navigation tabs list + &__list { + display: flex; + padding: 0; + margin: 0; + margin-inline-start: px2rem(4px); + overflow: auto; + white-space: nowrap; + list-style: none; + contain: content; + // Hack: don't show scrollbar when navigation tabs overflow, which should + // only happen in rare occasions, as adding too many top level sections is + // discouraged, since hiding content on horitontal axis doesn't lead to a + // good user experience. It's just harder to discover. + scrollbar-width: none; + + // Hack: see above + &::-webkit-scrollbar { + display: none; + } + } + + // Navigation tabs item + &__item { + height: px2rem(48px); + padding-inline: px2rem(12px); + + // Navigation tabs link in active navigation + &--active .md-tabs__link { + color: inherit; + opacity: 1; + } + } + + // Navigation tabs link - could be defined as block elements and aligned via + // line height, but this would imply more repaints when scrolling + &__link { + display: flex; + margin-top: px2rem(16px); + font-size: px2rem(14px); + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + // Hack: save a repaint when tabs are appearing on scrolling up + backface-visibility: hidden; + opacity: 0.7; + transition: + transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms; + + // Navigation tabs link on focus/hover + &:is(:focus, :hover) { + color: inherit; + opacity: 1; + } + + // Navigation tabs link icon + svg { + height: 1.3em; + margin-inline-end: px2rem(8px); + fill: currentcolor; + } + + // Delay transitions by a small amount + @for $i from 2 through 16 { + .md-tabs__item:nth-child(#{$i}) & { + transition-delay: 20ms * ($i - 1); + } + } + + // Hide tabs upon scrolling - disable transition to minimizes repaints + // while scrolling down, while scrolling up seems to be okay + .md-tabs[hidden] & { + opacity: 0; + transition: + transform 0ms 100ms, + opacity 100ms; + transform: translateY(50%); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_tag.scss b/docs/src/templates/assets/stylesheets/main/components/_tag.scss new file mode 100644 index 00000000..9f31829d --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_tag.scss @@ -0,0 +1,105 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tag variables +:root { + --md-tag-icon: svg-load("material/pound.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tag list + .md-tags { + display: inline-flex; + flex-wrap: wrap; + gap: px2em(8px); + margin-top: px2em(-2px); + margin-bottom: px2em(12px); + } + + // Tag + .md-tag { + display: inline-flex; + gap: px2em(8px); + align-items: center; + padding: px2em(4px, 12.8px) px2em(10px, 12.8px); + font-size: px2rem(12.8px); // Fallback + font-size: min(px2em(12.8px), px2rem(12.8px)); + font-weight: 700; + line-height: 1.6; + letter-spacing: initial; + background: var(--md-default-fg-color--lightest); + border-radius: px2rem(48px); + + // Linked tag + &[href] { + color: inherit; + outline: none; + -webkit-tap-highlight-color: transparent; + transition: + color 125ms, + background-color 125ms; + + // Linked tag on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + } + } + + // Tag inside headline + [id] > & { + vertical-align: text-top; + } + } + + // Tag icon + .md-tag-icon { + + // Tag icon content + &::before { + display: inline-block; + width: 1.2em; + height: 1.2em; + vertical-align: text-bottom; + content: ""; + background-color: var(--md-default-fg-color--lighter); + transition: background-color 125ms; + mask-image: var(--md-tag-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Linked tag on focus/hover + &[href]:is(:focus, :hover)::before { + background-color: var(--md-accent-bg-color); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_tooltip.scss b/docs/src/templates/assets/stylesheets/main/components/_tooltip.scss new file mode 100644 index 00000000..421e5858 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_tooltip.scss @@ -0,0 +1,292 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// Continuous pulse animation +@keyframes pulse { + 0% { + transform: scale(0.95); + } + + 75% { + transform: scale(1); + } + + 100% { + transform: scale(0.95); + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tooltip variables +:root { + --md-annotation-bg-icon: svg-load("material/circle.svg"); + --md-annotation-icon: svg-load("material/plus-circle.svg"); + --md-tooltip-width: #{px2rem(400px)}; +} + +// ---------------------------------------------------------------------------- + +// Tooltip +.md-tooltip { + position: absolute; + top: var(--md-tooltip-y); + left: + clamp( + var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)}, + var(--md-tooltip-x), + 100vw + + var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)} - + var(--md-tooltip-width) - + 2 * #{px2rem(16px)} + ); + // Hack: set an explicit `z-index` so we can transition it to ensure that any + // following elements are not overlaying the tooltip during the transition. + z-index: 0; + width: var(--md-tooltip-width); + max-width: calc(100vw - 2 * #{px2rem(16px)}); + font-family: var(--md-text-font-family); + color: var(--md-default-fg-color); + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + transform 0ms 250ms, + opacity 250ms, + z-index 250ms; + transform: translateY(px2rem(-8px)); + // Hack: promote to own layer to reduce jitter + backface-visibility: hidden; + + // Active tooltip + &--active { + z-index: 2; + opacity: 1; + transition: + transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), + opacity 250ms, + z-index 0ms; + transform: translateY(0); + } + + // Show outline on target and for keyboard devices + :is(.focus-visible > &, &:target) { + outline: var(--md-accent-fg-color) auto; + } + + // Tooltip wrapper + &__inner { + padding: px2rem(16px); + font-size: px2rem(12.8px); + + // Adjust spacing on first child + &.md-typeset > :first-child { + margin-top: 0; + } + + // Adjust spacing on last child + &.md-typeset > :last-child { + margin-bottom: 0; + } + } +} + +// ---------------------------------------------------------------------------- + +// Annotation +.md-annotation { + font-weight: 400; + white-space: normal; + vertical-align: text-bottom; + outline: none; + + // Adjust for right-to-left languages + [dir="rtl"] & { + direction: rtl; + } + + // Annotation index in code block + code & { + font-family: var(--md-code-font-family); + font-size: inherit; + } + + // Annotation is not hidden (e.g. when copying) + &:not([hidden]) { + display: inline-block; + // Hack: ensure that the line height doesn't exceed the line height of the + // hosting line, because it will lead to dancing pixels. + line-height: 1.25; + } + + // Annotation index + &__index { + position: relative; + z-index: 0; + display: inline-block; + margin-inline: 0.4ch; + vertical-align: text-top; + cursor: pointer; + user-select: none; + outline: none; + + // Hack: increase specificity to override default for anchors in typesetted + // content, because transitions are defined on anchor elements + .md-annotation & { + transition: z-index 250ms; + } + + // Hack: Work around Firefox bug that renders a subpixel outline when + // rotating a mask image element. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 + overflow: hidden; // stylelint-disable-line order/properties-order + border-radius: 0.01px; + + // [screen]: Render annotation markers as icons + @media screen { + width: 2.2ch; + + // Annotation is visible + [data-md-visible] > & { + animation: pulse 2000ms infinite; + } + + // Annotation marker background + &::before { + position: absolute; + top: -0.1ch; + z-index: -1; + width: 2.2ch; + height: 2.2ch; + content: ""; + background: var(--md-default-bg-color); + mask-image: var(--md-annotation-bg-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Annotation marker – the marker must be positioned absolutely behind + // the index, because it shouldn't impact the rendering of a code block. + // Otherwise, small rounding differences in browsers can sometimes mess up + // alignment of text following an annotation. + &::after { + position: absolute; + top: -0.1ch; + z-index: -1; + width: 2.2ch; + height: 2.2ch; + content: ""; + background-color: var(--md-default-fg-color--lighter); + transition: + background-color 250ms, + transform 250ms; + // Hack: promote to own layer to reduce jitter + transform: scale(1.0001); + mask-image: var(--md-annotation-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Annotation marker for active tooltip + .md-tooltip--active + & { + transform: rotate(45deg); + } + + // Annotation marker for active tooltip or on hover + :is(.md-tooltip--active + &, :hover > &) { + background-color: var(--md-accent-fg-color); + } + } + } + + // Annotation index for active tooltip + .md-tooltip--active + & { + z-index: 2; + transition-duration: 0ms; + animation-play-state: paused; + } + + // Annotation marker + [data-md-annotation-id] { + display: inline-block; + + // [print]: Render annotation markers as numbers + @media print { + padding: 0 0.6ch; + font-weight: 700; + color: var(--md-default-bg-color); + white-space: nowrap; + background: var(--md-default-fg-color--lighter); + border-radius: 2ch; + + // Annotation marker content + &::after { + content: attr(data-md-annotation-id); + } + } + } + } +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Annotation list + .md-annotation-list { + list-style: none; + counter-reset: xxx; + + // Annotation list item + li { + position: relative; + + // Annotation list marker + &::before { + position: absolute; + top: px2em(4px); + inset-inline-start: px2em(-34px); + min-width: 2ch; + height: 2ch; + padding: 0 0.6ch; + font-size: px2em(14.2px); + font-weight: 700; + line-height: 1.25; + color: var(--md-default-bg-color); + text-align: center; + content: counter(xxx); + counter-increment: xxx; + background: var(--md-default-fg-color--lighter); + border-radius: 2ch; + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_top.scss b/docs/src/templates/assets/stylesheets/main/components/_top.scss new file mode 100644 index 00000000..c24d44d1 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_top.scss @@ -0,0 +1,83 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Back-to-top button +.md-top { + position: fixed; + top: px2rem(48px + 16px); + z-index: 2; + display: block; + padding: px2rem(8px) px2rem(16px); + margin-inline-start: 50%; + font-size: px2rem(14px); + color: var(--md-default-fg-color--light); + cursor: pointer; + background-color: var(--md-default-bg-color); + border-radius: px2rem(32px); + outline: none; + box-shadow: var(--md-shadow-z2); + transition: + color 125ms, + background-color 125ms, + transform 125ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 125ms; + transform: translate(-50%, 0); + + // [print]: Hide back-to-top button + @media print { + display: none; + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translate(50%, 0); + } + + // Back-to-top button is hidden + &[hidden] { + pointer-events: none; + opacity: 0; + transition-duration: 0ms; + transform: translate(-50%, px2rem(4px)); + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translate(50%, px2rem(4px)); + } + } + + // Back-to-top button on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-bg-color); + background-color: var(--md-accent-fg-color); + } + + // Inline icon + svg { + display: inline-block; + vertical-align: -0.5em; + } +} diff --git a/docs/src/templates/assets/stylesheets/main/components/_version.scss b/docs/src/templates/assets/stylesheets/main/components/_version.scss new file mode 100644 index 00000000..3f85d6cd --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/components/_version.scss @@ -0,0 +1,150 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Keyframes +// ---------------------------------------------------------------------------- + +// See https://github.com/squidfunk/mkdocs-material/issues/2429 +@keyframes hoverfix { + 0% { + pointer-events: none; + } +} + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Version selection variables +:root { + --md-version-icon: svg-load("fontawesome/solid/caret-down.svg"); +} + +// ---------------------------------------------------------------------------- + +// Version selection +.md-version { + flex-shrink: 0; + height: px2rem(48px); + font-size: px2rem(16px); + + // Current selection + &__current { + position: relative; + // Hack: in general, we would use `vertical-align` to align the version at + // the bottom with the title, but since the list uses absolute positioning, + // this won't work consistently. Furthermore, we would need to use inline + // positioning to align the links, which looks jagged. + top: px2rem(1px); + margin-inline: px2rem(28px) px2rem(8px); + color: inherit; + cursor: pointer; + outline: none; + + // Version selection icon + &::after { + display: inline-block; + width: px2rem(8px); + height: px2rem(12px); + margin-inline-start: px2rem(8px); + content: ""; + background-color: currentcolor; + mask-image: var(--md-version-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Version selection list + &__list { + position: absolute; + top: px2rem(3px); + z-index: 3; + max-height: 0; + padding: 0; + margin: px2rem(4px) px2rem(16px); + overflow: auto; + color: var(--md-default-fg-color); + list-style-type: none; + background-color: var(--md-default-bg-color); + border-radius: px2rem(2px); + box-shadow: var(--md-shadow-z2); + opacity: 0; + transition: + max-height 0ms 500ms, + opacity 250ms 250ms; + scroll-snap-type: y mandatory; + + // Version selection list on parent focus/hover + .md-version:is(:focus-within, :hover) & { + max-height: px2rem(200px); + opacity: 1; + transition: + max-height 0ms, + opacity 250ms; + } + + // Fix hover on touch devices + @media (pointer: coarse), (hover: none) { + // Switch off on hover + .md-version:hover & { + animation: hoverfix 250ms forwards; + } + + // Enable on focus + .md-version:focus-within & { + animation: none; + } + } + } + + // Version selection item + &__item { + line-height: px2rem(36px); + } + + // Version selection link + &__link { + display: block; + width: 100%; + padding-inline: px2rem(12px) px2rem(24px); + white-space: nowrap; + cursor: pointer; + outline: none; + transition: + color 250ms, + background-color 250ms; + scroll-snap-align: start; + + // Link on focus/hover + &:is(:focus, :hover) { + color: var(--md-accent-fg-color); + } + + // Link on focus + &:focus { + background-color: var(--md-default-fg-color--lightest); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss new file mode 100644 index 00000000..bf517989 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss @@ -0,0 +1,195 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +@use "sass:color"; +@use "sass:list"; + +// ---------------------------------------------------------------------------- +// Variables +// ---------------------------------------------------------------------------- + +/// Admonition flavours +$admonitions: ( + "note": pencil-circle $clr-blue-a200, + "abstract": clipboard-text $clr-light-blue-a400, + "info": information $clr-cyan-a700, + "tip": fire $clr-teal-a700, + "success": check $clr-green-a700, + "question": help-circle $clr-light-green-a700, + "warning": alert $clr-orange-a400, + "failure": close $clr-red-a200, + "danger": lightning-bolt-circle $clr-red-a400, + "bug": shield-bug $clr-pink-a400, + "example": test-tube $clr-deep-purple-a200, + "quote": format-quote-close $clr-grey +) !default; + +// ---------------------------------------------------------------------------- +// Rules: layout +// ---------------------------------------------------------------------------- + +// Admonition variables +:root { + @each $name, $props in $admonitions { + --md-admonition-icon--#{$name}: + svg-load("material/#{list.nth($props, 1)}.svg"); + } +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Admonition - note that all styles also apply to details tags, which are + // rendered as collapsible admonitions with summary elements as titles. + .admonition { + display: flow-root; + padding: 0 px2rem(12px); + margin: px2em(20px, 12.8px) 0; + font-size: px2rem(12.8px); + color: var(--md-admonition-fg-color); + background-color: var(--md-admonition-bg-color); + border: px2rem(1.5px) solid $clr-blue-a200; + border-radius: px2rem(4px); + box-shadow: var(--md-shadow-z1); + transition: box-shadow 125ms; + page-break-inside: avoid; + + // [print]: Omit shadow as it may lead to rendering errors + @media print { + box-shadow: none; + } + + // Admonition on focus + &:focus-within { + box-shadow: 0 0 0 px2rem(4px) color.adjust($clr-blue-a200, $alpha: -0.9); + } + + // Hack: Chrome exhibits a weird issue where it will set nested elements to + // content-box. Doesn't happen in other browsers, so looks like a bug. + > * { + box-sizing: border-box; + } + + // Adjust vertical spacing for nested admonitions + .admonition { + margin-top: 1em; + margin-bottom: 1em; + } + + // Adjust spacing for contained table wrappers + .md-typeset__scrollwrap { + margin: 1em px2rem(-12px); + } + + // Adjust spacing for contained tables + .md-typeset__table { + padding: 0 px2rem(12px); + } + + // Adjust spacing for single-child tabbed block container + > .tabbed-set:only-child { + margin-top: 0; + } + + // Adjust spacing on last child + html & > :last-child { + margin-bottom: px2rem(12px); + } + } + + // Admonition title + .admonition-title { + position: relative; + padding-block: px2rem(8px); + padding-inline: px2rem(40px) px2rem(12px); + margin-block: 0; + margin-inline: px2rem(-12px); + font-weight: 700; + background-color: color.adjust($clr-blue-a200, $alpha: -0.9); + border: none; + border-inline-start-width: px2rem(4px); + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + + // Adjust spacing for title-only admonitions + html &:last-child { + margin-bottom: 0; + } + + // Admonition icon + &::before { + position: absolute; + top: px2em(10px); + width: px2rem(20px); + height: px2rem(20px); + content: ""; + background-color: $clr-blue-a200; + inset-inline-start: px2rem(12px); + mask-image: var(--md-admonition-icon--note); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Inline code block + code { + box-shadow: 0 0 0 px2rem(1px) var(--md-default-fg-color--lightest); + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: flavours +// ---------------------------------------------------------------------------- + +// Define admonition flavors +@each $name, $props in $admonitions { + $tint: list.nth($props, 2); + + // Admonition flavour + .md-typeset .admonition.#{$name} { + border-color: $tint; + + // Admonition on focus + &:focus-within { + box-shadow: 0 0 0 px2rem(4px) color.adjust($tint, $alpha: -0.9); + } + } + + // Admonition flavour title + .md-typeset .#{$name} > .admonition-title { + background-color: color.adjust($tint, $alpha: -0.9); + + // Admonition icon + &::before { + background-color: $tint; + mask-image: var(--md-admonition-icon--#{$name}); + } + + // Details marker + &::after { + color: $tint; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss new file mode 100644 index 00000000..59447d89 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss @@ -0,0 +1,146 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Footnotes variables +:root { + --md-footnotes-icon: svg-load("material/keyboard-return.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Footnote container + .footnote { + font-size: px2rem(12.8px); + color: var(--md-default-fg-color--light); + + // Footnote list - omit left indentation + > ol { + margin-inline-start: 0; + + // Footnote item - footnote items can contain lists, so we need to scope + // the spacing adjustments to the top-level footnote item. + > li { + transition: color 125ms; + + // Darken color on target + &:target { + color: var(--md-default-fg-color); + } + + // Show backreferences on footnote focus without transition + &:focus-within .footnote-backref { + opacity: 1; + transition: none; + transform: translateX(0); + } + + // Show backreferences on footnote hover/target + &:is(:hover, :target) .footnote-backref { + opacity: 1; + transform: translateX(0); + } + + // Adjust spacing on first child + > :first-child { + margin-top: 0; + } + } + } + } + + // Footnote reference + .footnote-ref { + font-size: px2em(12px, 16px); + font-weight: 700; + + // Hack: increase specificity to override default + html & { + outline-offset: px2rem(2px); + } + } + + // Show outline for all devices + [id^="fnref:"]:target > .footnote-ref { + outline: auto; + } + + // Footnote backreference + .footnote-backref { + display: inline-block; + // Hack: omit Unicode arrow for replacement with icon + font-size: 0; + color: var(--md-typeset-a-color); + vertical-align: text-bottom; + opacity: 0; + transition: + color 250ms, + transform 250ms 250ms, + opacity 125ms 250ms; + transform: translateX(px2rem(5px)); + + // [print]: Show footnote backreferences + @media print { + color: var(--md-typeset-a-color); + opacity: 1; + transform: translateX(0); + } + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: translateX(px2rem(-5px)); + } + + // Adjust color on hover + &:hover { + color: var(--md-accent-fg-color); + } + + // Footnote backreference icon + &::before { + display: inline-block; + width: px2rem(16px); + height: px2rem(16px); + content: ""; + background-color: currentcolor; + mask-image: var(--md-footnotes-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + + // Flip icon vertically + svg { + transform: scaleX(-1); + } + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss new file mode 100644 index 00000000..8284a5c0 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss @@ -0,0 +1,92 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss new file mode 100644 index 00000000..fe8ffd62 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss @@ -0,0 +1,52 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Arithmatex container + div.arithmatex { + overflow: auto; + + // [mobile -]: Align with body copy + @include break-to-device(mobile) { + margin: 0 px2rem(-16px); + } + + // Arithmatex content + > * { + width: min-content; + padding: 0 px2rem(16px); + margin-inline: auto !important; // stylelint-disable-line + touch-action: auto; + + // MathJax container - see https://bit.ly/3HR8YJ5 + mjx-container { + margin: 0 !important; // stylelint-disable-line + } + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss new file mode 100644 index 00000000..683705ce --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss @@ -0,0 +1,76 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Deletion + del.critic { + background-color: var(--md-typeset-del-color); + box-decoration-break: clone; + } + + // Addition + ins.critic { + background-color: var(--md-typeset-ins-color); + box-decoration-break: clone; + } + + // Comment + .critic.comment { + color: var(--md-code-hl-comment-color); + box-decoration-break: clone; + + // Comment opening mark + &::before { + content: "/* "; + } + + // Comment closing mark + &::after { + content: " */"; + } + } + + // Critic block + .critic.block { + display: block; + padding-inline: px2rem(16px); + margin: 1em 0; + overflow: auto; + box-shadow: none; + + // Adjust spacing on first child + > :first-child { + margin-top: 0.5em; + } + + // Adjust spacing on last child + > :last-child { + margin-bottom: 0.5em; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss new file mode 100644 index 00000000..8eea678a --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss @@ -0,0 +1,121 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Details variables +:root { + --md-details-icon: svg-load("material/chevron-right.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Details + details { + @extend .admonition; + + display: flow-root; + padding-top: 0; + overflow: visible; + + // Details title icon - rotate icon on transition to open state + &[open] > summary::after { + transform: rotate(90deg); + } + + // Adjust spacing for details in closed state + &:not([open]) { + padding-bottom: 0; + box-shadow: none; + + // Hack: we cannot set `overflow: hidden` on the `details` element (which + // is why we set it to `overflow: visible`, as the outline would not be + // visible when focusing. Therefore, we must set the border radius on the + // summary explicitly. + > summary { + border-radius: px2rem(2px); + } + } + } + + // Details title + summary { + @extend .admonition-title; + + display: block; + min-height: px2rem(20px); + padding-inline-end: px2rem(36px); + cursor: pointer; + border-start-start-radius: px2rem(2px); + border-start-end-radius: px2rem(2px); + + // Show outline for keyboard devices + &.focus-visible { + outline-color: var(--md-accent-fg-color); + outline-offset: px2rem(4px); + } + + // Hide outline for pointer devices + &:not(.focus-visible) { + outline: none; + -webkit-tap-highlight-color: transparent; + } + + // Details marker + &::after { + position: absolute; + top: px2em(10px); + width: px2rem(20px); + height: px2rem(20px); + content: ""; + background-color: currentcolor; + transition: transform 250ms; + transform: rotate(0deg); + inset-inline-end: px2rem(8px); + mask-image: var(--md-details-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + } + + // Hide native details marker - modern + &::marker { + display: none; + } + + // Hide native details marker - legacy, must be split into a seprate rule, + // so older browsers don't consider the selector list as invalid + &::-webkit-details-marker { + display: none; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss new file mode 100644 index 00000000..8b351013 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss @@ -0,0 +1,43 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss new file mode 100644 index 00000000..7d297677 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss @@ -0,0 +1,382 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules: syntax highlighting +// ---------------------------------------------------------------------------- + +// Code block +.highlight { + + // .o = Operator + // .ow = Operator, word + :is(.o, .ow) { + color: var(--md-code-hl-operator-color); + } + + .p { // Punctuation + color: var(--md-code-hl-punctuation-color); + } + + // .cpf = Comment, preprocessor file + // .l = Literal + // .s = Literal, string + // .sb = Literal, string backticks + // .sc = Literal, string char + // .s2 = Literal, string double + // .si = Literal, string interpol + // .s1 = Literal, string single + // .ss = Literal, string symbol + :is(.cpf, .l, .s, .sb, .sc, .s2, .si, .s1, .ss) { + color: var(--md-code-hl-string-color); + } + + // .cp = Comment, pre-processor + // .se = Literal, string escape + // .sh = Literal, string heredoc + // .sr = Literal, string regex + // .sx = Literal, string other + :is(.cp, .se, .sh, .sr, .sx) { + color: var(--md-code-hl-special-color); + } + + // .m = Number + // .mb = Number, binary + // .mf = Number, float + // .mh = Number, hex + // .mi = Number, integer + // .il = Number, integer long + // .mo = Number, octal + :is(.m, .mb, .mf, .mh, .mi, .il, .mo) { + color: var(--md-code-hl-number-color); + } + + // .k = Keyword, + // .kd = Keyword, declaration + // .kn = Keyword, namespace + // .kp = Keyword, pseudo + // .kr = Keyword, reserved + // .kt = Keyword, type + :is(.k, .kd, .kn, .kp, .kr, .kt) { + color: var(--md-code-hl-keyword-color); + } + + // .kc = Keyword, constant + // .n = Name + :is(.kc, .n) { + color: var(--md-code-hl-name-color); + } + + // .no = Name, constant + // .nb = Name, builtin + // .bp = Name, builtin pseudo + :is(.no, .nb, .bp) { + color: var(--md-code-hl-constant-color); + } + + // .nc = Name, class + // .ne = Name, exception + // .nf = Name, function + // .nn = Name, namespace + :is(.nc, .ne, .nf, .nn) { + color: var(--md-code-hl-function-color); + } + + // .nd = Name, decorator + // .ni = Name, entity + // .nl = Name, label + // .nt = Name, tag + :is(.nd, .ni, .nl, .nt) { + color: var(--md-code-hl-keyword-color); + } + + // .c = Comment + // .cm = Comment, multiline + // .c1 = Comment, single + // .ch = Comment, shebang + // .cs = Comment, special + // .sd = Literal, string doc + :is(.c, .cm, .c1, .ch, .cs, .sd) { + color: var(--md-code-hl-comment-color); + } + + // .na = Name, attribute + // .nv = Variable, + // .vc = Variable, class + // .vg = Variable, global + // .vi = Variable, instance + :is(.na, .nv, .vc, .vg, .vi) { + color: var(--md-code-hl-variable-color); + } + + // .ge = Generic, emph + // .gr = Generic, error + // .gh = Generic, heading + // .go = Generic, output + // .gp = Generic, prompt + // .gs = Generic, strong + // .gu = Generic, subheading + // .gt = Generic, traceback + :is(.ge, .gr, .gh, .go, .gp, .gs, .gu, .gt) { + color: var(--md-code-hl-generic-color); + } + + // .gd = Diff, delete + // .gi = Diff, insert + :is(.gd, .gi) { + padding: 0 px2em(2px); + margin: 0 px2em(-2px); + border-radius: px2rem(2px); + } + + .gd { // Diff, delete + background-color: var(--md-typeset-del-color); + } + + .gi { // Diff, insert + background-color: var(--md-typeset-ins-color); + } + + // Highlighted line + .hll { + display: block; + padding: 0 px2em(16px, 13.6px); + margin: 0 px2em(-16px, 13.6px); + background-color: var(--md-code-hl-color--light); + box-shadow: 2px 0 0 0 var(--md-code-hl-color) inset; + } + + // Code block title + span.filename { + position: relative; + display: flow-root; + padding: px2em(9px, 13.6px) px2em(16px, 13.6px); + margin-top: 1em; + font-size: px2em(13.6px); + font-weight: 700; + background-color: var(--md-code-bg-color); + border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); + border-top-left-radius: px2rem(2px); + border-top-right-radius: px2rem(2px); + + // Adjust spacing for code block + + pre { + margin-top: 0; + + // Remove rounded border on top side + > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + } + + // Code block line numbers (pymdownx-inline) + [data-linenos]::before { + position: sticky; + left: px2em(-16px, 13.6px); + // A `z-index` of 3 is necessary for ensuring that code block annotations + // don't overlay line numbers, as active annotations have a `z-index` of 2. + z-index: 3; + float: left; + padding-left: px2em(16px, 13.6px); + margin-right: px2em(16px, 13.6px); + margin-left: px2em(-16px, 13.6px); + color: var(--md-default-fg-color--light); + content: attr(data-linenos); + user-select: none; + background-color: var(--md-code-bg-color); + box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; + } + + // Code block line anchors - Chrome and Safari seem to have a strange bug + // where scroll margin is not applied to anchors inside code blocks. Setting + // positioning to absolute seems to fix the problem. Interestingly, this does + // not happen in Firefox. Furthermore we must set `visibility: hidden` or + // the copy to clipboard functionality will include an empty line between + // each set of lines. + code a[id] { + position: absolute; + visibility: hidden; + } + + // Copying in progress - this class is set before the content is copied and + // removed after copying is done to mitigate whitespace-related issues. + code[data-md-copying] { + + // Temporarily remove highlighted lines - see https://bit.ly/32iVGWh + .hll { + display: contents; + } + + // Temporarily remove annotations + .md-annotation { + display: none; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: layout +// ---------------------------------------------------------------------------- + +// Code block with line numbers +.highlighttable { + display: flow-root; + + // Set table elements to block layout, because otherwise the whole flexbox + // hacking won't work correctly + :is(tbody, td) { + display: block; + padding: 0; + } + + // We need to use flexbox layout, because otherwise it's not possible to + // make the code container scroll while keeping the line numbers static + tr { + display: flex; + } + + // The pre tags are nested inside a table, so we need to omit the margin + // because it collapses below all the overflows + pre { + margin: 0; + } + + // Code block title container + th.filename { + flex-grow: 1; + padding: 0; + text-align: left; + + // Adjust spacing + span.filename { + margin-top: 0; + } + } + + // Code block line numbers - disable user selection, so code can be easily + // copied without accidentally also copying the line numbers + .linenos { + padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); + padding-right: 0; + font-size: px2em(13.6px); + user-select: none; + background-color: var(--md-code-bg-color); + border-top-left-radius: px2rem(2px); + border-bottom-left-radius: px2rem(2px); + } + + // Code block line numbers container + .linenodiv { + padding-right: px2em(8px, 13.6px); + box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; + + // Adjust colors and alignment + pre { + color: var(--md-default-fg-color--light); + text-align: right; + } + } + + // Code block container - stretch to remaining space + .code { + flex: 1; + min-width: 0; + } +} + +// Code block line numbers container +.linenodiv a { + color: inherit; +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Code block with line numbers - unfortunately, these selectors need to be + // overly specific so they don't bleed into code blocks in annotations. + .highlighttable { + margin: 1em 0; + direction: ltr; + + // Remove rounded borders on code blocks + > tbody > tr > .code > div > pre > code { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + // Code block result container + .highlight + .result { + padding: 0 px2em(16px); + margin-top: calc(-1em + #{px2em(-2px)}); + overflow: visible; + border: px2rem(1px) solid var(--md-code-bg-color); + border-top-width: px2rem(2px); + border-bottom-right-radius: px2rem(2px); + border-bottom-left-radius: px2rem(2px); + + // Clearfix, because we can't use overflow: auto + &::after { + display: block; + clear: both; + content: ""; + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level code block + .md-content__inner > .highlight { + margin: 1em px2rem(-16px); + + // Remove rounded borders + > .filename, + > pre > code { + border-radius: 0; + } + + // Code block with line numbers - unfortunately, these selectors need to be + // overly specific so they don't bleed into code blocks in annotations. + > .highlighttable > tbody > tr > .filename span.filename, + > .highlighttable > tbody > tr > .linenos, + > .highlighttable > tbody > tr > .code > div > pre > code { + border-radius: 0; + } + + // Code block result container + + .result { + margin-inline: px2rem(-16px); + border-inline-width: 0; + border-radius: 0; + } + } +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss new file mode 100644 index 00000000..8749f08c --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss @@ -0,0 +1,115 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss new file mode 100644 index 00000000..9df91bfc --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss @@ -0,0 +1,400 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tabbed variables +:root { + --md-tabbed-icon--prev: svg-load("material/chevron-left.svg"); + --md-tabbed-icon--next: svg-load("material/chevron-right.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tabbed container + .tabbed-set { + position: relative; + display: flex; + flex-flow: column wrap; + margin: 1em 0; + border-radius: px2rem(2px); + + // Tab radio button - the Tabbed extension will generate radio buttons with + // labels, so tabs can be triggered without the necessity for JavaScript. + // This is pretty cool, as it has great accessibility out-of-the box, so + // we just hide the radio button and toggle the label color for indication. + > input { + position: absolute; + width: 0; + height: 0; + opacity: 0; + + // Adjust scroll margin + &:target { + --md-scroll-offset: #{px2em(10px, 16px)}; + } + + // Tab label states + @for $i from 20 through 1 { + &:nth-child(#{$i}) { + + // Tab is active + &:checked { + + // Tab label + ~ .tabbed-labels > :nth-child(#{$i}) { + @extend %tabbed-label; + } + + // Tab content + ~ .tabbed-content > :nth-child(#{$i}) { + @extend %tabbed-content; + } + } + + // Tab label on keyboard focus + &.focus-visible ~ .tabbed-labels > :nth-child(#{$i}) { + @extend %tabbed-label-focus-visible; + } + } + } + + // Tab indicator on keyboard focus + &.focus-visible ~ .tabbed-labels::before { + background-color: var(--md-accent-fg-color); + } + } + } + + // Tabbed labels + .tabbed-labels { + display: flex; + max-width: 100%; + overflow: auto; + box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest) inset; + -ms-overflow-style: none; // IE, Edge + scrollbar-width: none; // Firefox + + // [print]: Move one layer up for ordering + @media print { + display: contents; + } + + // [screen and no reduced motion]: Disable animation + @media screen { + + // [js]: Show animated tab indicator + .js & { + position: relative; + + // Tab indicator + &::before { + position: absolute; + bottom: 0; + left: 0; + display: block; + width: var(--md-indicator-width); + height: 2px; + content: ""; + background: var(--md-default-fg-color); + transition: + width 225ms, + background-color 250ms, + transform 250ms; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transform: translateX(var(--md-indicator-x)); + } + } + } + + // Webkit scrollbar + &::-webkit-scrollbar { + display: none; // Chrome, Safari + } + + // Tab label + > label { + flex-shrink: 0; + width: auto; + padding: px2em(10px, 12.8px) 1.25em px2em(8px, 12.8px); + font-size: px2rem(12.8px); + font-weight: 700; + color: var(--md-default-fg-color--light); + white-space: nowrap; + cursor: pointer; + border-bottom: px2rem(2px) solid transparent; + border-radius: px2rem(2px) px2rem(2px) 0 0; + transition: + background-color 250ms, + color 250ms; + scroll-margin-inline-start: px2rem(20px); + + // [print]: Intersperse labels with containers + @media print { + + // Ensure correct order of labels + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + order: $i; + } + } + } + + // Tab label on hover + &:hover { + color: var(--md-default-fg-color); + } + } + } + + // Tabbed content + .tabbed-content { + width: 100%; + + // [print]: Move one layer up for ordering + @media print { + display: contents; + } + } + + // Tabbed block + .tabbed-block { + display: none; + + // [print]: Intersperse labels with containers + @media print { + display: block; + + // Ensure correct order of containers + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + order: $i; + } + } + } + + // Code block is the first child of a tab - remove margin and mirror + // previous (now deprecated) SuperFences code block grouping behavior + > pre:first-child, + > .highlight:first-child > pre { + margin: 0; + + // Remove rounded borders on code block + > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + // Code block is the first child of a tab - remove margin and mirror + // previous (now deprecated) SuperFences code block grouping behavior + > .highlight:first-child { + + // Code block title - remove spacing and rounded borders + > .filename { + margin: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + // Code block with line numbers - unfortunately, these selectors need to + // be overly specific so they don't bleed into code blocks in annotations. + > .highlighttable { + margin: 0; + + // Remove rounded borders on line numbers and titles + > tbody > tr > .filename span.filename, + > tbody > tr > .linenos { + margin: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + // Remove rounded borders on code blocks + > tbody > tr > .code > div > pre > code { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + // Code block result container - adjust spacing + + .result { + margin-top: px2em(-2px); + } + } + + // Adjust spacing for nested tabbed container + > .tabbed-set { + margin: 0; + } + } + + // Tabbed button + .tabbed-button { + display: block; + align-self: center; + width: px2rem(18px); + height: px2rem(18px); + margin-top: px2rem(2px); + color: var(--md-default-fg-color--light); + pointer-events: initial; + cursor: pointer; + border-radius: 100%; + transition: background-color 250ms; + + // Tabbed button on hover + &:hover { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--transparent); + } + + // Tabbed button icon + &::after { + display: block; + width: 100%; + height: 100%; + content: ""; + background-color: currentcolor; + transition: + background-color 250ms, + transform 250ms; + mask-image: var(--md-tabbed-icon--prev); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + } + + // Tabbed control + .tabbed-control { + position: absolute; + display: flex; + justify-content: start; + width: px2rem(24px); + height: px2rem(38px); + pointer-events: none; + background: + linear-gradient( + to right, + var(--md-default-bg-color) 60%, + transparent + ); + transition: opacity 125ms; + + // Adjust for right-to-left languages + [dir="rtl"] & { + transform: rotate(180deg); + } + + // Tabbed control is hidden + &[hidden] { + opacity: 0; + } + + // Tabbed control next + &--next { + right: 0; + justify-content: end; + background: + linear-gradient( + to left, + var(--md-default-bg-color) 60%, + transparent + ); + + // Tabbed button icon content + .tabbed-button::after { + mask-image: var(--md-tabbed-icon--next); + } + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: top-level +// ---------------------------------------------------------------------------- + +// [mobile -]: Align with body copy +@include break-to-device(mobile) { + + // Top-level tabbed labels + .md-content__inner > .tabbed-set .tabbed-labels { + max-width: 100vw; + padding-inline-start: px2rem(16px); + margin: 0 px2rem(-16px); + scroll-padding-inline-start: px2rem(16px); + + // Hack: some browsers ignore the right padding on flex containers, + // see https://bit.ly/3lsPS3S + &::after { + padding-inline-end: px2rem(16px); + content: ""; + } + + // Tabbed control previous + ~ .tabbed-control--prev { + width: px2rem(40px); + padding-inline-start: px2rem(16px); + margin-inline-start: px2rem(-16px); + } + + // Tabbed control next + ~ .tabbed-control--next { + width: px2rem(40px); + padding-inline-end: px2rem(16px); + margin-inline-end: px2rem(-16px); + } + } +} + +// ---------------------------------------------------------------------------- +// Placeholders: improve colocation for better compression +// ---------------------------------------------------------------------------- + +// Tab label placeholder +%tabbed-label { + + // [screen]: Show active state + @media screen { + color: var(--md-default-fg-color); + + // [no-js]: Show border (indicator is animated with JavaScript) + .no-js & { + border-color: var(--md-default-fg-color); + } + } +} + +// Tab label on keyboard focus placeholder +%tabbed-label-focus-visible { + color: var(--md-accent-fg-color); +} + +// Tab content placeholder +%tabbed-content { + display: block; +} diff --git a/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss new file mode 100644 index 00000000..a1d1117c --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss @@ -0,0 +1,78 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Tasklist variables +:root { + --md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg"); + --md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg"); +} + +// ---------------------------------------------------------------------------- + +// Scoped in typesetted content to match specificity of regular content +.md-typeset { + + // Tasklist item + .task-list-item { + position: relative; + list-style-type: none; + + // Make checkbox items align with normal list items, but position + // everything in ems for correct layout at smaller font sizes + [type="checkbox"] { + position: absolute; + top: 0.45em; + inset-inline-start: -2em; + } + } + + // Hide native checkbox, when custom classes are enabled + .task-list-control [type="checkbox"] { + z-index: -1; + opacity: 0; + } + + // Tasklist indicator in unchecked state + .task-list-indicator::before { + position: absolute; + top: 0.15em; + width: px2em(20px); + height: px2em(20px); + content: ""; + background-color: var(--md-default-fg-color--lightest); + inset-inline-start: px2em(-24px); + mask-image: var(--md-tasklist-icon); + mask-position: center; + mask-repeat: no-repeat; + mask-size: contain; + } + + // Tasklist indicator in checked state + [type="checkbox"]:checked + .task-list-indicator::before { + background-color: $clr-green-a400; + mask-image: var(--md-tasklist-icon--checked); + } +} diff --git a/docs/src/templates/assets/stylesheets/main/integrations/_mermaid.scss b/docs/src/templates/assets/stylesheets/main/integrations/_mermaid.scss new file mode 100644 index 00000000..d0325f39 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/main/integrations/_mermaid.scss @@ -0,0 +1,67 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Mermaid variables +:root > * { + --md-mermaid-font-family: var(--md-text-font-family), sans-serif; + + // General colors + --md-mermaid-edge-color: var(--md-code-fg-color); + --md-mermaid-node-bg-color: var(--md-accent-fg-color--transparent); + --md-mermaid-node-fg-color: var(--md-accent-fg-color); + --md-mermaid-label-bg-color: var(--md-default-bg-color); + --md-mermaid-label-fg-color: var(--md-code-fg-color); + + // Sequence diagram colors + --md-mermaid-sequence-actor-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-actor-fg-color: var(--md-mermaid-label-fg-color); + --md-mermaid-sequence-actor-border-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-actor-line-color: var(--md-default-fg-color--lighter); + --md-mermaid-sequence-actorman-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-actorman-line-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-box-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-box-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-label-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-label-fg-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-loop-bg-color: var(--md-mermaid-node-bg-color); + --md-mermaid-sequence-loop-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-loop-border-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-message-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-message-line-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-note-bg-color: var(--md-mermaid-label-bg-color); + --md-mermaid-sequence-note-fg-color: var(--md-mermaid-edge-color); + --md-mermaid-sequence-note-border-color: var(--md-mermaid-label-fg-color); + --md-mermaid-sequence-number-bg-color: var(--md-mermaid-node-fg-color); + --md-mermaid-sequence-number-fg-color: var(--md-accent-bg-color); +} + +// ---------------------------------------------------------------------------- + +// Mermaid container +.mermaid { + margin: 1em 0; + line-height: normal; +} diff --git a/docs/src/templates/assets/stylesheets/palette.scss b/docs/src/templates/assets/stylesheets/palette.scss new file mode 100644 index 00000000..ff73a982 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/palette.scss @@ -0,0 +1,40 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/palette/_accent.scss b/docs/src/templates/assets/stylesheets/palette/_accent.scss new file mode 100644 index 00000000..9f69b596 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/palette/_accent.scss @@ -0,0 +1,61 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/palette/_primary.scss b/docs/src/templates/assets/stylesheets/palette/_primary.scss new file mode 100644 index 00000000..a8653f0f --- /dev/null +++ b/docs/src/templates/assets/stylesheets/palette/_primary.scss @@ -0,0 +1,203 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +@use "sass:list"; + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Define primary colors +@each $name, $colors in ( + "red": $clr-red-400 $clr-red-300 $clr-red-600, + "pink": $clr-pink-500 $clr-pink-400 $clr-pink-700, + "purple": $clr-purple-400 $clr-purple-300 $clr-purple-600, + "deep-purple": $clr-deep-purple-400 $clr-deep-purple-300 $clr-deep-purple-500, + "indigo": $clr-indigo-500 $clr-indigo-400 $clr-indigo-700, + "blue": $clr-blue-500 $clr-blue-400 $clr-blue-700, + "light-blue": $clr-light-blue-500 $clr-light-blue-400 $clr-light-blue-700, + "cyan": $clr-cyan-500 $clr-cyan-400 $clr-cyan-700, + "teal": $clr-teal-500 $clr-teal-400 $clr-teal-700, + "green": $clr-green-500 $clr-green-400 $clr-green-700, + "light-green": $clr-light-green-500 $clr-light-green-400 $clr-light-green-700, + "lime": $clr-lime-500 $clr-lime-400 $clr-lime-700, + "yellow": $clr-yellow-500 $clr-yellow-400 $clr-yellow-700, + "amber": $clr-amber-500 $clr-amber-400 $clr-amber-700, + "orange": $clr-orange-400 $clr-orange-400 $clr-orange-600, + "deep-orange": $clr-deep-orange-400 $clr-deep-orange-300 $clr-deep-orange-600, + "brown": $clr-brown-500 $clr-brown-400 $clr-brown-700, + "grey": $clr-grey-600 $clr-grey-500 $clr-grey-700, + "blue-grey": $clr-blue-grey-600 $clr-blue-grey-500 $clr-blue-grey-700 +) { + + // Color palette + [data-md-color-primary="#{$name}"] { + --md-primary-fg-color: hsl(#{hex2hsl(list.nth($colors, 1))}); + --md-primary-fg-color--light: hsl(#{hex2hsl(list.nth($colors, 2))}); + --md-primary-fg-color--dark: hsl(#{hex2hsl(list.nth($colors, 3))}); + + // Inverted text for lighter shades + @if index("lime" "yellow" "amber" "orange", $name) { + --md-primary-bg-color: hsla(0, 0%, 0%, 0.87); + --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54); + } @else { + --md-primary-bg-color: hsla(0, 0%, 100%, 1); + --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); + } + + // Typeset color shades + @if index("grey" "blue-grey", $name) { + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + } + } +} + +// ---------------------------------------------------------------------------- + +// Adjust link colors for light primary colors +@each $name, $color in ( + "light-green": hsl(88, 58%, 43%), + "lime": hsl(66, 88%, 32%), + "yellow": hsl(54, 100%, 36%), + "amber": hsl(45, 100%, 41%), + "orange": hsl(36, 100%, 45%) +) { + [data-md-color-primary="#{$name}"]:not([data-md-color-scheme="slate"]) { + --md-typeset-a-color: #{$color}; + } +} + +// ---------------------------------------------------------------------------- +// Rules: white +// ---------------------------------------------------------------------------- + +// Define primary colors for white +[data-md-color-primary="white"] { + --md-primary-fg-color: hsla(var(--md-hue), 0%, 100%, 1); + --md-primary-fg-color--light: hsla(var(--md-hue), 0%, 100%, 0.7); + --md-primary-fg-color--dark: hsla(var(--md-hue), 0%, 0%, 0.07); + --md-primary-bg-color: hsla(var(--md-hue), 0%, 0%, 0.87); + --md-primary-bg-color--light: hsla(var(--md-hue), 0%, 0%, 0.54); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // Form button + .md-button { + color: var(--md-typeset-a-color); + + // Primary button + &--primary { + color: hsla(var(--md-hue), 0%, 100%, 1); + background-color: var(--md-typeset-a-color); + border-color: var(--md-typeset-a-color); + } + } + + // [tablet portrait +]: Header-embedded search + @include break-from-device(tablet landscape) { + + // Search form + .md-search__form { + background-color: hsla(var(--md-hue), 0%, 0%, 0.07); + + // Search form on hover + &:hover { + background-color: hsla(var(--md-hue), 0%, 0%, 0.32); + } + } + + // Search icon + .md-search__input + .md-search__icon { + color: hsla(var(--md-hue), 0%, 0%, 0.87); + } + } + + // [screen +]: Add bottom border for tabs + @include break-from-device(screen) { + + // Navigation tabs + .md-tabs { + border-bottom: px2rem(1px) solid hsla(0, 0%, 0%, 0.07); + } + } +} + +// ---------------------------------------------------------------------------- +// Rules: black +// ---------------------------------------------------------------------------- + +// Define primary colors for black +[data-md-color-primary="black"] { + --md-primary-fg-color: hsla(var(--md-hue), 15%, 9%, 1); + --md-primary-fg-color--light: hsla(var(--md-hue), 15%, 9%, 0.54); + --md-primary-fg-color--dark: hsla(var(--md-hue), 15%, 9%, 1); + --md-primary-bg-color: hsla(var(--md-hue), 15%, 100%, 1); + --md-primary-bg-color--light: hsla(var(--md-hue), 15%, 100%, 0.7); + + // Typeset `a` color shades + --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); + + // Form button + .md-button { + color: var(--md-typeset-a-color); + + // Primary button + &--primary { + color: hsla(var(--md-hue), 0%, 100%, 1); + background-color: var(--md-typeset-a-color); + border-color: var(--md-typeset-a-color); + } + } + + // Header + .md-header { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + + // [tablet portrait -]: Layered navigation + @include break-to-device(tablet portrait) { + + // Repository information container + .md-nav__source { + background-color: hsla(var(--md-hue), 15%, 11%, 0.87); + } + } + + // [tablet -]: Layered navigation + @include break-to-device(tablet) { + + // Site title in main navigation + html & .md-nav--primary .md-nav__title[for="__drawer"] { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + } + + // [screen +]: Set background color for tabs + @include break-from-device(screen) { + + // Navigation tabs + .md-tabs { + background-color: hsla(var(--md-hue), 15%, 9%, 1); + } + } +} diff --git a/docs/src/templates/assets/stylesheets/palette/_scheme.scss b/docs/src/templates/assets/stylesheets/palette/_scheme.scss new file mode 100644 index 00000000..0a9f9823 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/palette/_scheme.scss @@ -0,0 +1,145 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +/// DEALINGS +//// + +// ---------------------------------------------------------------------------- +// Rules +// ---------------------------------------------------------------------------- + +// Only use dark mode on screens +@media screen { + + // Slate theme, i.e. dark mode + [data-md-color-scheme="slate"] { + + // Indicate that the site is rendered with a dark color scheme + color-scheme: dark; + + // Default color shades + --md-default-fg-color: hsla(var(--md-hue), 15%, 90%, 0.82); + --md-default-fg-color--light: hsla(var(--md-hue), 15%, 90%, 0.56); + --md-default-fg-color--lighter: hsla(var(--md-hue), 15%, 90%, 0.32); + --md-default-fg-color--lightest: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-default-bg-color: hsla(var(--md-hue), 15%, 14%, 1); + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 14%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 14%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 14%, 0.07); + + // Code color shades + --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82); + --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1); + + // Code highlighting color shades + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.15); + --md-code-hl-number-color: hsla(6, 74%, 63%, 1); + --md-code-hl-special-color: hsla(340, 83%, 66%, 1); + --md-code-hl-function-color: hsla(291, 57%, 65%, 1); + --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); + --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); + --md-code-hl-string-color: hsla(150, 58%, 44%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + // Typeset color shades + --md-typeset-color: var(--md-default-fg-color); + + // Typeset `a` color shades + --md-typeset-a-color: var(--md-primary-fg-color); + + // Typeset `kbd` color shades + --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 90%, 0.2); + --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1); + + // Typeset `mark` color shades + --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3); + + // Typeset `table` color shades + --md-typeset-table-color: hsla(var(--md-hue), 15%, 95%, 0.12); + --md-typeset-table-color--light: hsla(var(--md-hue), 15%, 95%, 0.035); + + // Admonition color shades + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + // Footer color shades + --md-footer-bg-color: hsla(var(--md-hue), 15%, 10%, 0.87); + --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 8%, 1); + + // Shadow depth 1 + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + // Shadow depth 2 + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.25), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + // Shadow depth 3 + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.4), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); + + // Hide images for light mode + img[src$="#only-light"], + img[src$="#gh-light-mode-only"] { + display: none; + } + } + + // -------------------------------------------------------------------------- + + // Adjust link colors for dark primary colors + @each $name, $color in ( + "pink": hsl(340, 81%, 63%), + "purple": hsl(291, 53%, 63%), + "deep-purple": hsl(262, 73%, 70%), + "indigo": hsl(219, 76%, 62%), + "teal": hsl(174, 100%, 40%), + "green": hsl(122, 39%, 60%), + "deep-orange": hsl(14, 100%, 65%), + "brown": hsl(16, 45%, 56%), + + // Set neutral colors to indigo + "grey": hsl(219, 66%, 62%), + "blue-grey": hsl(219, 66%, 62%), + "white": hsl(219, 66%, 62%), + "black": hsl(219, 66%, 62%) + ) { + [data-md-color-scheme="slate"][data-md-color-primary="#{$name}"] { + --md-typeset-a-color: #{$color}; + } + } + + // -------------------------------------------------------------------------- + + // Switching in progress - disable all transitions temporarily + [data-md-color-switching] *, + [data-md-color-switching] *::before, + [data-md-color-switching] *::after { + transition-duration: 0ms !important; // stylelint-disable-line + } +} diff --git a/docs/src/templates/assets/stylesheets/utilities/_break.scss b/docs/src/templates/assets/stylesheets/utilities/_break.scss new file mode 100644 index 00000000..7ccd8622 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/utilities/_break.scss @@ -0,0 +1,219 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/assets/stylesheets/utilities/_convert.scss b/docs/src/templates/assets/stylesheets/utilities/_convert.scss new file mode 100644 index 00000000..8199c9c8 --- /dev/null +++ b/docs/src/templates/assets/stylesheets/utilities/_convert.scss @@ -0,0 +1,79 @@ +//// +/// Copyright (c) 2016-2023 Martin Donath +/// +/// Permission is hereby granted, free of charge, to any person obtaining a +/// copy of this software and associated documentation files (the "Software"), +/// to deal in the Software without restriction, including without limitation +/// the rights to use, copy, modify, merge, publish, distribute, sublicense, +/// and/or sell copies of the Software, and to permit persons to whom the +/// Software is furnished to do so, subject to the following conditions: +/// +/// The above copyright notice and this permission notice shall be included in +/// all copies or substantial portions of the Software. +/// +/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL +/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +/// LIABILITY, WHETHER 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/docs/src/templates/base.html b/docs/src/templates/base.html new file mode 100644 index 00000000..8323e76e --- /dev/null +++ b/docs/src/templates/base.html @@ -0,0 +1,445 @@ + + +{% import "partials/language.html" as lang with context %} + + + + + + + {% block site_meta %} + + + + + {% if page.meta and page.meta.description %} + + {% elif config.site_description %} + + {% endif %} + + + {% if page.meta and page.meta.author %} + + {% elif config.site_author %} + + {% endif %} + + + {% if page.canonical_url %} + + {% endif %} + + + {% if page.previous_page %} + + {% endif %} + + + {% if page.next_page %} + + {% endif %} + + + {% if "rss" in config.plugins %} + + + {% endif %} + + + + + + + {% endblock %} + + + {% block htmltitle %} + {% if page.meta and page.meta.title %} + {{ page.meta.title }} - {{ config.site_name }} + {% elif page.title and not page.is_homepage %} + {{ page.title | striptags }} - {{ config.site_name }} + {% else %} + {{ config.site_name }} + {% endif %} + {% endblock %} + + + {% block styles %} + + + + {% if config.theme.palette %} + {% set palette = config.theme.palette %} + + {% endif %} + + + {% include "partials/icons.html" %} + {% endblock %} + + + {% block libs %} + {% for script in config.extra.polyfills %} + {{ script | script_tag }} + {% endfor %} + {% endblock %} + + + {% block fonts %} + + + {% if config.theme.font != false %} + {% set text = config.theme.font.get("text", "Roboto") %} + {% set code = config.theme.font.get("code", "Roboto Mono") %} + + + + {% endif %} + {% endblock %} + + + {% for path in config.extra_css %} + + {% endfor %} + + + {% include "partials/javascripts/base.html" %} + + + {% block analytics %} + {% include "partials/integrations/analytics.html" %} + {% endblock %} + + + {% if page.meta and page.meta.meta %} + {% for tag in page.meta.meta %} + + {% endfor %} + {% endif %} + + + {% block extrahead %}{% endblock %} + + + + {% set direction = config.theme.direction or lang.t("direction") %} + {% if config.theme.palette %} + {% set palette = config.theme.palette %} + {% if not palette is mapping %} + {% set palette = palette | first %} + {% endif %} + {% set scheme = palette.scheme | d("default", true) %} + {% set primary = palette.primary | d("indigo", true) %} + {% set accent = palette.accent | d("indigo", true) %} + + {% else %} + + {% endif %} + {% set features = config.theme.features or [] %} + + + {% if not config.theme.palette is mapping %} + {% include "partials/javascripts/palette.html" %} + {% endif %} + + + + + + + + + +
    + {% if page.toc | first is defined %} + {% set skip = page.toc | first %} + + {{ lang.t("action.skip") }} + + {% endif %} +
    + + +
    + {% if self.announce() %} + + {% endif %} +
    + + + {% if config.extra.version %} + + {% endif %} + + + {% block header %} + {% include "partials/header.html" %} + {% endblock %} + + +
    + + + {% block hero %}{% endblock %} + + + {% block tabs %} + {% if "navigation.tabs.sticky" not in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} + {% endblock %} + + +
    +
    + + + {% block site_nav %} + + + {% if nav %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "navigation" in page.meta.hide %} + {% endif %} + + {% endif %} + + + {% if "toc.integrate" not in features %} + {% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "toc" in page.meta.hide %} + {% endif %} + + {% endif %} + {% endblock %} + + + {% block container %} +
    +
    + {% block content %} + {% include "partials/content.html" %} + {% endblock %} +
    +
    + {% endblock %} + + + {% include "partials/javascripts/content.html" %} +
    + + + {% if "navigation.top" in features %} + {% include "partials/top.html" %} + {% endif %} +
    + + + {% block footer %} + {% include "partials/footer.html" %} + {% endblock %} +
    + + +
    +
    +
    + + + {% if "navigation.instant.progress" in features %} + {% include "partials/progress.html" %} + {% endif %} + + + {% if config.extra.consent %} + + + + {% include "partials/javascripts/consent.html" %} + {% endif %} + + + {% block config %} + {%- set app = { + "base": base_url, + "features": features, + "translations": {}, + "search": "assets/javascripts/workers/search.js" | url + } -%} + + + {%- if config.extra.version -%} + {%- set _ = app.update({ "version": config.extra.version }) -%} + {%- endif -%} + + + {%- if config.extra.tags -%} + {%- set _ = app.update({ "tags": config.extra.tags }) -%} + {%- endif -%} + + + {%- set translations = app.translations -%} + {%- for key in [ + "clipboard.copy", + "clipboard.copied", + "search.result.placeholder", + "search.result.none", + "search.result.one", + "search.result.other", + "search.result.more.one", + "search.result.more.other", + "search.result.term.missing", + "select.version" + ] -%} + {%- set _ = translations.update({ key: lang.t(key) }) -%} + {%- endfor -%} + + + + {% endblock %} + + + {% block scripts %} + + + + {% for script in config.extra_javascript %} + {{ script | script_tag }} + {% endfor %} + {% endblock %} + + diff --git a/docs/src/templates/blog-post.html b/docs/src/templates/blog-post.html new file mode 100644 index 00000000..73fb669f --- /dev/null +++ b/docs/src/templates/blog-post.html @@ -0,0 +1,164 @@ + + +{% extends "main.html" %} + +{% import "partials/nav-item.html" as item with context %} + + +{% block container %} +
    + + +
    +
    +
    + + + + {% if "toc.integrate" in features %} + {% include "partials/toc.html" %} + {% endif %} +
    +
    +
    + + +
    + {% block content %} + {% include "partials/content.html" %} + {% endblock %} +
    +
    +{% endblock %} diff --git a/docs/src/templates/blog.html b/docs/src/templates/blog.html new file mode 100644 index 00000000..eedc77d9 --- /dev/null +++ b/docs/src/templates/blog.html @@ -0,0 +1,48 @@ + + +{% extends "main.html" %} + + +{% block container %} +
    +
    + + +
    + {{ page.content }} +
    + + + {% for post in posts %} + {% include "partials/post.html" %} + {% endfor %} + + + {% if pagination %} + {% block pagination %} + {% include "partials/pagination.html" %} + {% endblock %} + {% endif %} +
    +
    +{% endblock %} diff --git a/docs/src/templates/main.html b/docs/src/templates/main.html new file mode 100644 index 00000000..3b77d200 --- /dev/null +++ b/docs/src/templates/main.html @@ -0,0 +1,23 @@ + + +{% extends "base.html" %} diff --git a/docs/src/templates/mkdocs_theme.yml b/docs/src/templates/mkdocs_theme.yml new file mode 100644 index 00000000..aaa47f5e --- /dev/null +++ b/docs/src/templates/mkdocs_theme.yml @@ -0,0 +1,50 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# Language for theme localization +language: en + +# Text direction (can be ltr or rtl), default: ltr +direction: + +# Feature flags for functionality that alters behavior significantly, and thus +# may be a matter of taste +features: [] + +# Fonts used by Material, automatically loaded from Google Fonts - see the site +# for a list of available fonts +font: + + # Default font for text + text: Roboto + + # Fixed-width font for code listings + code: Roboto Mono + +# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs. +# Icons that are part of the HTML can be configured and replaced +icon: + +# Favicon to be rendered +favicon: assets/images/favicon.png + +# Static pages to build +static_templates: + - 404.html diff --git a/docs/src/templates/partials/actions.html b/docs/src/templates/partials/actions.html new file mode 100644 index 00000000..75fcb8eb --- /dev/null +++ b/docs/src/templates/partials/actions.html @@ -0,0 +1,54 @@ + + + +{% if page.edit_url %} + + + {% if "content.action.edit" in features %} + + {% set icon = config.theme.icon.edit or "material/file-edit-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} + + + {% if "content.action.view" in features %} + {% if "/blob/" in page.edit_url %} + {% set part = "blob" %} + {% else %} + {% set part = "edit" %} + {% endif %} + + {% set icon = config.theme.icon.view or "material/file-eye-outline" %} + {% include ".icons/" ~ icon ~ ".svg" %} + + {% endif %} +{% endif %} diff --git a/docs/src/templates/partials/alternate.html b/docs/src/templates/partials/alternate.html new file mode 100644 index 00000000..7d7c925b --- /dev/null +++ b/docs/src/templates/partials/alternate.html @@ -0,0 +1,49 @@ + + + +
    +
    + {% set icon = config.theme.icon.alternate or "material/translate" %} + +
    + +
    +
    +
    diff --git a/docs/src/templates/partials/comments.html b/docs/src/templates/partials/comments.html new file mode 100644 index 00000000..6641d20e --- /dev/null +++ b/docs/src/templates/partials/comments.html @@ -0,0 +1,23 @@ + + + diff --git a/docs/src/templates/partials/consent.html b/docs/src/templates/partials/consent.html new file mode 100644 index 00000000..c84622bc --- /dev/null +++ b/docs/src/templates/partials/consent.html @@ -0,0 +1,107 @@ + + + +{% set cookies = config.extra.consent.cookies | d({}) %} +{% if config.extra.analytics %} + {% if "analytics" not in cookies %} + {% set _ = cookies.update({ "analytics": "Google Analytics" }) %} + {% endif %} +{% endif %} +{% if config.repo_url and "github.com" in config.repo_url %} + {% if "github" not in cookies %} + {% set _ = cookies.update({ "github": "GitHub" }) %} + {% endif %} +{% endif %} + + +{% set actions = config.extra.consent.actions %} +{% if not actions %} + {% set actions = ["accept", "manage"] %} +{% endif %} + + +{% if "manage" not in actions %} + {% set checked = "checked" %} +{% endif %} + + +

    {{ config.extra.consent.title }}

    +

    {{ config.extra.consent.description }}

    + + + + + + + diff --git a/docs/src/templates/partials/content.html b/docs/src/templates/partials/content.html new file mode 100644 index 00000000..2b78b09b --- /dev/null +++ b/docs/src/templates/partials/content.html @@ -0,0 +1,54 @@ + + + +{% if "material/tags" in config.plugins and tags %} + {% include "partials/tags.html" %} +{% endif %} + + +{% include "partials/actions.html" %} + + +{% if "\x3ch1" not in page.content %} +

    {{ page.title | d(config.site_name, true)}}

    +{% endif %} + + +{{ page.content }} + + +{% if page.meta and ( + page.meta.git_revision_date_localized or + page.meta.revision_date +) %} + {% include "partials/source-file.html" %} +{% endif %} + + +{% include "partials/feedback.html" %} + + +{% include "partials/comments.html" %} diff --git a/docs/src/templates/partials/copyright.html b/docs/src/templates/partials/copyright.html new file mode 100644 index 00000000..070948d2 --- /dev/null +++ b/docs/src/templates/partials/copyright.html @@ -0,0 +1,39 @@ + + + + diff --git a/docs/src/templates/partials/feedback.html b/docs/src/templates/partials/feedback.html new file mode 100644 index 00000000..bf27c640 --- /dev/null +++ b/docs/src/templates/partials/feedback.html @@ -0,0 +1,79 @@ + + + +{% if config.extra.analytics %} + {% set feedback = config.extra.analytics.feedback %} +{% endif %} + + +{% if page.meta and page.meta.hide %} + {% if "feedback" in page.meta.hide %} + {% set feedback = None %} + {% endif %} +{% endif %} + + +{% if feedback %} + +{% endif %} diff --git a/docs/src/templates/partials/footer.html b/docs/src/templates/partials/footer.html new file mode 100644 index 00000000..ebe9278f --- /dev/null +++ b/docs/src/templates/partials/footer.html @@ -0,0 +1,98 @@ + + + + diff --git a/docs/src/templates/partials/header.html b/docs/src/templates/partials/header.html new file mode 100644 index 00000000..9b6d2e2e --- /dev/null +++ b/docs/src/templates/partials/header.html @@ -0,0 +1,112 @@ + + + +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} + + +
    + + + + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
    diff --git a/docs/src/templates/partials/icons.html b/docs/src/templates/partials/icons.html new file mode 100644 index 00000000..17dd20d8 --- /dev/null +++ b/docs/src/templates/partials/icons.html @@ -0,0 +1,72 @@ + + + +{% 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 %} + + +{% if config.theme.icon.annotation %} + {% set style = ["\x3cstyle\x3e:root{"] %} + {% import ".icons/" ~ config.theme.icon.annotation ~ ".svg" as icon %} + {% set _ = style.append( + "--md-annotation-icon:" ~ + "url('data:image/svg+xml;charset=utf-8," ~ + icon | replace("\n", "") ~ + "');" + ) %} + {% set _ = style.append("}\x3c/style\x3e") %} + {{ style | join }} +{% endif %} + + +{% if config.theme.icon.tag %} + {% set style = ["\x3cstyle\x3e"] %} + {% for type, icon in config.theme.icon.tag.items() %} + {% import ".icons/" ~ icon ~ ".svg" as icon %} + {% if type != "default" %} + {% set modifier = "--" ~ type %} + {% endif %} + {% set _ = style.append( + ".md-tag" ~ modifier ~ "{" ~ + "--md-tag-icon:" ~ + "url('data:image/svg+xml;charset=utf-8," ~ + icon | replace("\n", "") ~ + "');" ~ + "}" + ) %} + {% endfor %} + {% set _ = style.append("\x3c/style\x3e") %} + {{ style | join }} +{% endif %} diff --git a/docs/src/templates/partials/integrations/analytics.html b/docs/src/templates/partials/integrations/analytics.html new file mode 100644 index 00000000..4b483046 --- /dev/null +++ b/docs/src/templates/partials/integrations/analytics.html @@ -0,0 +1,49 @@ + + + +{% if config.extra.analytics %} + {% set provider = config.extra.analytics.provider %} +{% endif %} + + +{% if provider %} + {% include "partials/integrations/analytics/" ~ provider ~ ".html" %} + + + {% if config.extra.consent %} + + + + {% else %} + + {% endif %} +{% endif %} diff --git a/docs/src/templates/partials/integrations/analytics/google.html b/docs/src/templates/partials/integrations/analytics/google.html new file mode 100644 index 00000000..a9fa37d9 --- /dev/null +++ b/docs/src/templates/partials/integrations/analytics/google.html @@ -0,0 +1,97 @@ + + + +{% if config.extra.analytics %} + {% set property = config.extra.analytics.property | d("", true) %} +{% endif %} + + + diff --git a/docs/src/templates/partials/javascripts/announce.html b/docs/src/templates/partials/javascripts/announce.html new file mode 100644 index 00000000..f13961b2 --- /dev/null +++ b/docs/src/templates/partials/javascripts/announce.html @@ -0,0 +1,31 @@ + + + + diff --git a/docs/src/templates/partials/javascripts/base.html b/docs/src/templates/partials/javascripts/base.html new file mode 100644 index 00000000..f0eeeb8a --- /dev/null +++ b/docs/src/templates/partials/javascripts/base.html @@ -0,0 +1,48 @@ + + + + diff --git a/docs/src/templates/partials/javascripts/consent.html b/docs/src/templates/partials/javascripts/consent.html new file mode 100644 index 00000000..13730da7 --- /dev/null +++ b/docs/src/templates/partials/javascripts/consent.html @@ -0,0 +1,61 @@ + + + + diff --git a/docs/src/templates/partials/javascripts/content.html b/docs/src/templates/partials/javascripts/content.html new file mode 100644 index 00000000..d361f18b --- /dev/null +++ b/docs/src/templates/partials/javascripts/content.html @@ -0,0 +1,39 @@ + + + +{% if "content.tabs.link" in features %} + +{% endif %} diff --git a/docs/src/templates/partials/javascripts/outdated.html b/docs/src/templates/partials/javascripts/outdated.html new file mode 100644 index 00000000..576f3c85 --- /dev/null +++ b/docs/src/templates/partials/javascripts/outdated.html @@ -0,0 +1,29 @@ + + + + diff --git a/docs/src/templates/partials/javascripts/palette.html b/docs/src/templates/partials/javascripts/palette.html new file mode 100644 index 00000000..a2daef1d --- /dev/null +++ b/docs/src/templates/partials/javascripts/palette.html @@ -0,0 +1,29 @@ + + + + diff --git a/docs/src/templates/partials/language.html b/docs/src/templates/partials/language.html new file mode 100644 index 00000000..e37b953b --- /dev/null +++ b/docs/src/templates/partials/language.html @@ -0,0 +1,28 @@ + + + +{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %} +{% import "partials/languages/en.html" as fallback %} + + +{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/af.html b/docs/src/templates/partials/languages/af.html new file mode 100644 index 00000000..b7f9f8fa --- /dev/null +++ b/docs/src/templates/partials/languages/af.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "af", + "action.edit": "Wysig hierdie bladsy", + "action.skip": "Slaan oor na inhoud", + "action.view": "Bekyk bron van hierdie bladsy", + "announce.dismiss": "Moenie dit weer wys nie", + "blog.archive": "Argief", + "blog.categories": "Kategorieë", + "blog.categories.in": "binne", + "blog.continue": "Lees verder", + "blog.draft": "Konsep", + "blog.index": "Terug na indeks", + "blog.meta": "Metadata", + "blog.references": "Verwante skakels", + "clipboard.copy": "Kopieer na knipbord", + "clipboard.copied": "gekopieer na knipbord", + "consent.accept": "Aanvaar", + "consent.manage": "Bestuur instellings", + "consent.reject": "Verwerp", + "footer": "Voetskrif", + "footer.next": "Volgende", + "footer.previous": "Vorige", + "header": "Kopskrif", + "meta.comments": "Kommentaar", + "meta.source": "Bron", + "nav": "Navigasie", + "readtime.one": "1 minuut se lees", + "readtime.other": "# minuut se lees", + "rss.created": "RSS-voer geskep", + "rss.updated": "RSS-voer van opgedateerde inhoud", + "search": "Soek", + "search.config.lang": "nl", + "search.placeholder": "Soek", + "search.share": "Deel", + "search.reset": "Terugstel", + "search.result.initializer": "Inisialisering van soektog", + "search.result.placeholder": "Tik om te begin soek", + "search.result.none": "Geen ooreenstemmende dokumente", + "search.result.one": "1 ooreenstemmende dokument", + "search.result.other": "# ooreenstemmende dokumente", + "search.result.more.one": "1 meer op hierdie bladsy", + "search.result.more.other": "# meer op hierdie bladsy", + "search.result.term.missing": "Vermis", + "select.language": "Kies taal", + "select.version": "Kies weergawe", + "source": "Slaan oor na inhoud", + "source.file.contributors": "Medewerkers", + "source.file.date.created": "Geskep", + "source.file.date.updated": "Laaste opdatering", + "tabs": "Duimgids", + "toc": "Inhoudsopgawe", + "top": "Terug na bo" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ar.html b/docs/src/templates/partials/languages/ar.html new file mode 100644 index 00000000..4d5da33a --- /dev/null +++ b/docs/src/templates/partials/languages/ar.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "ar", + "direction": "rtl", + "action.edit": "عدل الصفحة", + "action.skip": "انتقل إلى المحتوى", + "action.view": "عرض مصدر هذه الصفحة", + "announce.dismiss": "لا تظهر هذا مرة أخرى", + "blog.archive": "أرشيف", + "blog.categories": "فئات", + "blog.categories.in": "ضمن", + "blog.continue": "أكمل القراءة", + "blog.draft": "مسودة", + "blog.index": "رجوع إلى الفهرس", + "blog.meta": "بيانات وصفية", + "blog.references": "روابط ذات علاقة", + "clipboard.copy": "نسخ إلى الحافظة", + "clipboard.copied": "تم النسخ الى الحافظة", + "consent.accept": "قبول", + "consent.manage": "إدارة الإعدادات", + "consent.reject": "رفض", + "footer": "هامش سفلي", + "footer.next": "التالية", + "footer.previous": "السابقة", + "header": "عنوان العارضة", + "meta.comments": "التعليقات", + "meta.source": "المصدر", + "nav": "تصفح", + "readtime.one": "قراءة لمدة دقيقة", + "readtime.other": "دقائق قراءة #", + "rss.created": "ملقم بالخلاصات", + "rss.updated": "ملقم بالخلاصات المحدثة", + "search": "إبحث", + "search.config.pipeline": " ", + "search.placeholder": "بحث", + "search.share": "شارك", + "search.reset": "مسح كلي", + "search.result.initializer": "بدء البحث", + "search.result.placeholder": "اكتب لبدء البحث", + "search.result.none": "لا توجد نتائج", + "search.result.one": "نتائج البحث مستند واحد", + "search.result.other": "نتائج البحث # مستندات", + "search.result.more.one": "أكثر من 1 في هذه الصفحة", + "search.result.more.other": "أكثر من # في هذه الصفحة", + "search.result.term.missing": "مفقود", + "select.language": "إختر اللغة", + "select.version": "إختر الإصدار", + "source": "اذهب إلى المصدر", + "source.file.contributors": "المساهمون", + "source.file.date.created": "خلقت", + "source.file.date.updated": "اخر تحديث", + "tabs": "نوافذ", + "toc": "جدول المحتويات", + "top": "عد إلى الأعلى" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/be.html b/docs/src/templates/partials/languages/be.html new file mode 100644 index 00000000..c36c8402 --- /dev/null +++ b/docs/src/templates/partials/languages/be.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "be", + "direction": "ltr", + "action.edit": "Правіць старонку", + "action.skip": "Перайсці да зместа", + "action.view": "Паглядзець зыходны код старонкі", + "announce.dismiss": "Больш не паказваць", + "blog.archive": "Заархіваваць", + "blog.categories": "Катэгорыі", + "blog.categories.in": "у", + "blog.continue": "Працягнуць чытаць", + "blog.draft": "Чарнавік", + "blog.index": "Вярнуцца на хатнюю", + "blog.meta": "Метаданыя", + "blog.references": "Спасылкі па тэме", + "clipboard.copy": "Скапіраваць у буфер абмена", + "clipboard.copied": "Скапіравана ў буфер абмена", + "consent.accept": "Прыняць", + "consent.manage": "Кіраваць наладамі", + "consent.reject": "Адхіліць", + "footer": "Ніжні калантытул", + "footer.next": "Наступная", + "footer.previous": "Папярэдняя", + "header": "Верхні калантытул", + "meta.comments": "Каментарыі", + "meta.source": "Зыходны код", + "nav": "Навігацыя", + "readtime.one": "Прачытанне зойме 1 хв", + "readtime.other": "Прачытанне зойме # хв", + "rss.created": "RSS стужка", + "rss.updated": "RSS стужка з абноўленым зместам", + "search": "Пошук", + "search.config.lang": "ru", + "search.placeholder": "Пошук", + "search.share": "Падзяліцца", + "search.reset": "Ачысціць", + "search.result.initializer": "Пачынаем пошук", + "search.result.placeholder": "Пачніце друкаваць для пошуку", + "search.result.none": "Нічога ня знойдзена", + "search.result.one": "Адзін адпаведны дакумент", + "search.result.other": "Адпаведных дакументаў: #", + "search.result.more.one": "Яшчэ 1 на гэтай старонцы", + "search.result.more.other": "Яшчэ # на гэтай старонцы", + "search.result.term.missing": "Адсутнічае", + "select.language": "Выберыце мову", + "select.version": "Выберыце версію", + "source": "Перайсці ў рэпазітар", + "source.file.contributors": "Укладальнікі", + "source.file.date.created": "Створана", + "source.file.date.updated": "Апошняе абнаўленне", + "tabs": "Укладкі", + "toc": "Змест", + "top": "Вярнуцца да пачатку" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/bg.html b/docs/src/templates/partials/languages/bg.html new file mode 100644 index 00000000..f36fd437 --- /dev/null +++ b/docs/src/templates/partials/languages/bg.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "bg", + "action.edit": "Редактирай тази страница", + "action.skip": "Към съдържанието", + "action.view": "Виж съдържанието на тази страница", + "announce.dismiss": "Не показвай повече", + "blog.archive": "Архив", + "blog.categories": "Категории", + "blog.categories.in": "В", + "blog.continue": "Продължи четенето", + "blog.draft": "Чернова", + "blog.index": "Назад към индекса", + "blog.meta": "Метаданни", + "blog.references": "Свързани линкове", + "clipboard.copy": "Копирай", + "clipboard.copied": "Копирано", + "consent.accept": "Приеми", + "consent.manage": "Управление на настойките", + "consent.reject": "Откажи", + "footer": "Долен колонтитул", + "footer.next": "Следваща", + "footer.previous": "Предишна", + "header": "Горен колонтитул", + "meta.comments": "Коментари", + "meta.source": "Код", + "nav": "Навигация", + "readtime.one": "1 мин четено", + "readtime.other": "# мин четено", + "rss.created": "RSS публикации", + "rss.updated": "RSS публикации с актуализирано съдържание", + "search": "Търси", + "search.config.lang": "ru", + "search.placeholder": "Търси", + "search.share": "Сподели", + "search.reset": "Изчисти", + "search.result.initializer": "Инициализирано търсене", + "search.result.placeholder": "Започнете да пишете, за да търсите", + "search.result.none": "Няма резултати", + "search.result.one": "1 резултат", + "search.result.other": "# резултата", + "search.result.more.one": "още 1 на тази страница", + "search.result.more.other": "още # на тази страница", + "search.result.term.missing": "Липсващо", + "select.language": "Избери език", + "select.version": "Избери версия", + "source": "Към хранилището", + "source.file.contributors": "Участници", + "source.file.date.created": "Създаден", + "source.file.date.updated": "Последна промяна", + "tabs": "Табове", + "toc": "Съдържание", + "top": "Върни се в началото" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/bn.html b/docs/src/templates/partials/languages/bn.html new file mode 100644 index 00000000..0a3ee6d0 --- /dev/null +++ b/docs/src/templates/partials/languages/bn.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "bn", + "action.edit": "এই পেজ এডিট করুন", + "action.skip": "কনটেন্টে যান", + "action.view": "পেজের ভিউ", + "announce.dismiss": "আর কখনো দেখাবে না", + "blog.archive": "সংরক্ষণাগার", + "blog.categories": "বিভাগ", + "blog.categories.in": "বিভাগের মধ্যে", + "blog.continue": "পড়তে থাকুন", + "blog.draft": "খসড়া", + "blog.index": "ইনডেক্সে ফিরে যান", + "blog.meta": "মেটাডেটা", + "blog.references": "সম্পর্কিত লিংক", + "clipboard.copy": "ক্লিপবোর্ডে কপি করুন", + "clipboard.copied": "ক্লিপবোর্ডে কপি হয়েছে", + "consent.accept": "গ্রহণ", + "consent.manage": "সেটিংস ব্যবস্থাপনা", + "consent.reject": "প্রত্যাখ্যান", + "footer": "ফুটার", + "footer.next": "পরে", + "footer.previous": "পূর্ববর্তী", + "header": "হেডার", + "meta.comments": "মন্তব্য", + "meta.source": "উৎস", + "nav": "ন্যাভিগেশন", + "readtime.one": "১ মিনিট পড়া", + "readtime.other": "# মিনিট পড়া", + "rss.created": "আরএসএস ফিড", + "rss.updated": "আপডেট করা বিষয়বস্তুর আরএসএস ফিড", + "search": "অনুসন্ধান করুন", + "search.config.pipeline": " ", + "search.placeholder": "অনুসন্ধান করুন", + "search.share": "শেয়ার", + "search.reset": "রিসেট", + "search.result.initializer": "অনুসন্ধান শুরু করা হচ্ছে", + "search.result.placeholder": "সার্চ টাইপ করুন", + "search.result.none": "কিছু পাওয়া যায়নি", + "search.result.one": "১ টা ডকুমেন্ট", + "search.result.other": "# টা ডকুমেন্ট", + "search.result.more.one": "এই পৃষ্ঠায় আরও ১টি আছে", + "search.result.more.other": "এই পৃষ্ঠায় আরও #টি আছে", + "search.result.term.missing": "অনুপস্থিত", + "select.language": "ভাষা নির্বাচন করুণ", + "select.version": "সংস্করণ নির্বাচন করুণ", + "source": "রিপোজিটরিতে যান", + "source.file.contributors": "অবদানকারী", + "source.file.date.created": "তৈরি হয়েছে", + "source.file.date.updated": "শেষ আপডেট", + "tabs": "ট্যাব", + "toc": "সূচি তালিকা", + "top": "উপরে ফিরে যাও" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ca.html b/docs/src/templates/partials/languages/ca.html new file mode 100644 index 00000000..8fd2b03a --- /dev/null +++ b/docs/src/templates/partials/languages/ca.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "ca", + "action.edit": "Edita aquesta pàgina", + "action.skip": "Salta el contingut", + "action.view": "Visualitza el codi font", + "announce.dismiss": "No ho tornis a mostrar", + "blog.archive": "Arxiva", + "blog.categories": "Categories", + "blog.categories.in": "a", + "blog.continue": "Continua llegint", + "blog.draft": "Esborrany", + "blog.index": "Torna a l'índex", + "blog.meta": "Metadades", + "blog.references": "Enllaços relacionats", + "clipboard.copy": "Còpia al porta-retalls", + "clipboard.copied": "Copiat al porta-retalls", + "consent.accept": "Accepta", + "consent.manage": "Gestiona la configuració", + "consent.reject": "Rebutja", + "footer": "Peu de pàgina", + "footer.next": "Següent", + "footer.previous": "Anterior", + "header": "Capçalera", + "meta.comments": "Comentaris", + "meta.source": "Codi font", + "nav": "Navegació", + "readtime.one": "1 min de lectura", + "readtime.other": "# min de lectura", + "rss.created": "Canal RSS", + "rss.updated": "Canal RSS de contingut actualitzat", + "search": "Cerca", + "search.placeholder": "Cerca", + "search.share": "Comparteix", + "search.reset": "Neteja", + "search.result.initializer": "Inicialitzant cerca", + "search.result.placeholder": "Escriu per a començar a cercar", + "search.result.none": "Cap document coincideix", + "search.result.one": "1 document coincident", + "search.result.other": "# documents coincidents", + "search.result.more.one": "1 més en aquesta pàgina", + "search.result.more.other": "# més en aquesta pàgina", + "search.result.term.missing": "Desaparegut", + "select.language": "Selecciona la llengua", + "select.version": "Selecciona la versió", + "source": "Ves al repositori", + "source.file.contributors": "Col·laboradors", + "source.file.date.created": "Creada", + "source.file.date.updated": "Darrera actualització", + "tabs": "Pestanyes", + "toc": "Taula de continguts", + "top": "Torna a l'inici" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/cs.html b/docs/src/templates/partials/languages/cs.html new file mode 100644 index 00000000..fb955865 --- /dev/null +++ b/docs/src/templates/partials/languages/cs.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "cs", + "action.edit": "Upravit tuto stránku", + "action.skip": "Přeskočit obsah", + "action.view": "Zobrazit zdroj této stránky", + "announce.dismiss": "Již nezobrazovat", + "blog.archive": "Archiv", + "blog.categories": "Kategorie", + "blog.categories.in": "v", + "blog.continue": "Pokračovat ve čtení", + "blog.draft": "Návrh", + "blog.index": "Zpět na index", + "blog.meta": "Metadata", + "blog.references": "Související odkazy", + "clipboard.copy": "Kopírovat do schránky", + "clipboard.copied": "Zkopírováno do schránky", + "consent.accept": "Akceptovat", + "consent.manage": "Spravovat nastavení", + "consent.reject": "Odmítnout", + "footer": "Zápatí", + "footer.next": "Další", + "footer.previous": "Předchozí", + "header": "Záhlaví", + "meta.comments": "Komentáře", + "meta.source": "Zdroj", + "nav": "Navigace", + "readtime.one": "1 min čtení", + "readtime.other": "# min čtení", + "rss.created": "RSS kanál", + "rss.updated": "RSS zdroj aktualizovaného obsahu", + "search": "Vyhledávání", + "search.placeholder": "Hledat", + "search.share": "Sdílet", + "search.reset": "Vyčistit", + "search.result.initializer": "Inicializace vyhledávání", + "search.result.placeholder": "Pište co se má vyhledat", + "search.result.none": "Nenalezeny žádné dokumenty", + "search.result.one": "Nalezený dokument: 1", + "search.result.other": "Nalezené dokumenty: #", + "search.result.more.one": "1 další na této stránce", + "search.result.more.other": "# více na této stránce", + "search.result.term.missing": "Chybějící", + "select.language": "Zvolte jazyk", + "select.version": "Vyberte verzi", + "source": "Přejít do repozitáře", + "source.file.contributors": "Přispěvatelé", + "source.file.date.created": "Vytvořeno", + "source.file.date.updated": "Poslední aktualizace", + "tabs": "Karty", + "toc": "Obsah", + "top": "Zpět na začátek" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/da.html b/docs/src/templates/partials/languages/da.html new file mode 100644 index 00000000..2f9da2db --- /dev/null +++ b/docs/src/templates/partials/languages/da.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "da", + "action.edit": "Redigér denne side", + "action.skip": "Gå til indholdet", + "action.view": "Vis kildetekst på denne side", + "announce.dismiss": "Vis ikke dette igen", + "blog.archive": "Arkiv", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Læs mere", + "blog.draft": "Udkast", + "blog.index": "Gå tilbage", + "blog.meta": "Metadata", + "blog.references": "Relateret indhold", + "clipboard.copy": "Kopiér til udklipsholderen", + "clipboard.copied": "Kopieret til udklipsholderen", + "consent.accept": "Accepter", + "consent.manage": "Administrer indstillinger", + "consent.reject": "Afvis", + "footer": "Sidefod", + "footer.next": "Næste", + "footer.previous": "Forrige", + "header": "Sidehoved", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "nav": "Navigation", + "readtime.one": "1 minuts læsetid", + "readtime.other": "# minuts læstid", + "rss.created": "RSS feed", + "rss.updated": "RSS feed af opdateret indhold", + "search": "Søg", + "search.config.lang": "da", + "search.placeholder": "Søg", + "search.share": "Del", + "search.reset": "Nulstil søgning", + "search.result.initializer": "Start søgning", + "search.result.placeholder": "Indtast søgeord", + "search.result.none": "Ingen resultater fundet", + "search.result.one": "1 resultat", + "search.result.other": "# resultater", + "search.result.more.one": "1 resultat mere på denne side", + "search.result.more.other": "# resultater mere på denne side", + "search.result.term.missing": "Mangler", + "select.language": "Vælg sprog", + "select.version": "Vælg version", + "source": "Åbn arkiv", + "source.file.contributors": "Bidragydere", + "source.file.date.created": "Oprettet", + "source.file.date.updated": "Sidste ændring", + "tabs": "Faner", + "toc": "Indholdsfortegnelse", + "top": "Tilbage til start" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/de.html b/docs/src/templates/partials/languages/de.html new file mode 100644 index 00000000..bfd8b909 --- /dev/null +++ b/docs/src/templates/partials/languages/de.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "de", + "action.edit": "Seite editieren", + "action.skip": "Zum Inhalt", + "action.view": "Quellcode der Seite anzeigen", + "announce.dismiss": "Nicht mehr anzeigen", + "blog.archive": "Archiv", + "blog.categories": "Kategorien", + "blog.categories.in": "in", + "blog.continue": "Weiterlesen", + "blog.draft": "Entwurf", + "blog.index": "Zur Übersicht", + "blog.meta": "Metadaten", + "blog.references": "Weiterführende Links", + "clipboard.copy": "In Zwischenablage kopieren", + "clipboard.copied": "In Zwischenablage kopiert", + "consent.accept": "Akzeptieren", + "consent.manage": "Einstellungen", + "consent.reject": "Ablehnen", + "footer": "Fußzeile", + "footer.next": "Weiter", + "footer.previous": "Zurück", + "header": "Kopfzeile", + "meta.comments": "Kommentare", + "meta.source": "Quellcode", + "nav": "Navigation", + "readtime.one": "1 Min. Lesezeit", + "readtime.other": "# Min. Lesezeit", + "rss.created": "RSS Feed", + "rss.updated": "RSS Feed der aktualisierten Inhalte", + "search": "Suche", + "search.config.lang": "de", + "search.placeholder": "Suche", + "search.share": "Teilen", + "search.reset": "Zurücksetzen", + "search.result.initializer": "Suche wird initialisiert", + "search.result.placeholder": "Suchbegriff eingeben", + "search.result.none": "Keine Suchergebnisse", + "search.result.one": "1 Suchergebnis", + "search.result.other": "# Suchergebnisse", + "search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite", + "search.result.more.other": "# weitere Suchergebnisse auf dieser Seite", + "search.result.term.missing": "Es fehlt", + "select.language": "Sprache wechseln", + "select.version": "Version auswählen", + "source": "Zum Repository", + "source.file.contributors": "Mitwirkende", + "source.file.date.created": "Erstellt", + "source.file.date.updated": "Letztes Update", + "tabs": "Hauptnavigation", + "toc": "Inhaltsverzeichnis", + "top": "Zurück zum Seitenanfang" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/el.html b/docs/src/templates/partials/languages/el.html new file mode 100644 index 00000000..8dce1793 --- /dev/null +++ b/docs/src/templates/partials/languages/el.html @@ -0,0 +1,74 @@ + + + +{% macro t(key) %}{{ { + "language": "el", + "action.edit": "Επεξεργασία αυτής της σελίδας", + "action.skip": "Μετάβαση στο περιεχόμενο", + "action.view": "Προβολή πηγαίου κώδικα", + "announce.dismiss": "Μην το ξαναδείξετε αυτό", + "blog.archive": "Aρχείο", + "blog.categories": "Κατηγορίες", + "blog.categories.in": "Στο", + "blog.continue": "Περισσότερα", + "blog.draft": "Πρόχειρο", + "blog.index": "Eπιστροφή", + "blog.references": "Σχετικοί σύνδεσμοι", + "clipboard.copy": "Αντιγραφή στο πρόχειρο", + "clipboard.copied": "Αντιγράφηκε στο πρόχειρο", + "consent.accept": "Αποδοχή", + "consent.manage": "Περισσότερες επιλογές", + "consent.reject": "Απόρριψη", + "footer": "Υποσέλιδο", + "footer.next": "Επόμενο", + "footer.previous": "Προηγούμενο", + "header": "Κεφαλίδα", + "meta.comments": "Σχόλια", + "meta.source": "Πηγή", + "nav": "Πλοήγηση", + "readtime.one": "1 λεπτό διάβασμα", + "readtime.other": "# λεπτά διάβασμα", + "rss.created": "Ροές Δεδομένων RSS", + "rss.updated": "Ροές Δεδομένων RSS. Τελευταία νέα", + "search": "Αναζήτηση", + "search.placeholder": "Αναζήτηση", + "search.share": "Διαμοίραση", + "search.reset": "Καθαρισμός", + "search.result.initializer": "Αρχικοποίηση αναζήτησης", + "search.result.placeholder": "Πληκτρολογήστε για να αρχίσει η αναζήτηση", + "search.result.none": "δεν βρήκε κάποιο έγγραφο", + "search.result.one": "1 έγγραφο που ταιριάζει", + "search.result.other": "# έγγραφα που ταιριάζουν", + "search.result.more.one": "1 ακόμα σε αυτήν τη σελίδα", + "search.result.more.other": "# ακόμα σε αυτήν τη σελίδα", + "search.result.term.missing": "Λείπει", + "select.language": "Επιλογή γλώσσας", + "select.version": "Επιλογή έκδοσης", + "source": "Μετάβαση στο αποθετήριο", + "source.file.contributors": "Συνεισφέροντες", + "source.file.date.created": "Δημιουργήθηκε", + "source.file.date.updated": "τελευταία ενημέρωση", + "tabs": "Καρτέλες", + "toc": "Πίνακας περιεχομένων", + "top": "Επιστροφή στην αρχή" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/en.html b/docs/src/templates/partials/languages/en.html new file mode 100644 index 00000000..0e6a73ac --- /dev/null +++ b/docs/src/templates/partials/languages/en.html @@ -0,0 +1,79 @@ + + + +{% macro t(key) %}{{ { + "language": "en", + "direction": "ltr", + "action.edit": "Edit this page", + "action.skip": "Skip to content", + "action.view": "View source of this page", + "announce.dismiss": "Don't show this again", + "blog.archive": "Archive", + "blog.categories": "Categories", + "blog.categories.in": "in", + "blog.continue": "Continue reading", + "blog.draft": "Draft", + "blog.index": "Back to index", + "blog.meta": "Metadata", + "blog.references": "Related links", + "clipboard.copy": "Copy to clipboard", + "clipboard.copied": "Copied to clipboard", + "consent.accept": "Accept", + "consent.manage": "Manage settings", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Next", + "footer.previous": "Previous", + "header": "Header", + "meta.comments": "Comments", + "meta.source": "Source", + "nav": "Navigation", + "readtime.one": "1 min read", + "readtime.other": "# min read", + "rss.created": "RSS feed", + "rss.updated": "RSS feed of updated content", + "search": "Search", + "search.config.lang": "en", + "search.config.pipeline": "stopWordFilter", + "search.config.separator": "[\\s\\-]+", + "search.placeholder": "Search", + "search.share": "Share", + "search.reset": "Clear", + "search.result.initializer": "Initializing search", + "search.result.placeholder": "Type to start searching", + "search.result.none": "No matching documents", + "search.result.one": "1 matching document", + "search.result.other": "# matching documents", + "search.result.more.one": "1 more on this page", + "search.result.more.other": "# more on this page", + "search.result.term.missing": "Missing", + "select.language": "Select language", + "select.version": "Select version", + "source": "Go to repository", + "source.file.contributors": "Contributors", + "source.file.date.created": "Created", + "source.file.date.updated": "Last update", + "tabs": "Tabs", + "toc": "Table of contents", + "top": "Back to top" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/eo.html b/docs/src/templates/partials/languages/eo.html new file mode 100644 index 00000000..cd3829a8 --- /dev/null +++ b/docs/src/templates/partials/languages/eo.html @@ -0,0 +1,49 @@ + + + +{% macro t(key) %}{{ { + "language": "eo", + "action.edit": "Redakti ĉi tiun paĝon", + "action.skip": "Saltu al enhavo", + "clipboard.copy": "Kopii al tondujo", + "clipboard.copied": "Kopiado al klipo", + "footer": "Piedlinio", + "footer.next": "Sekva", + "footer.previous": "Antaŭa", + "header": "Kaplinio", + "meta.comments": "Komentoj", + "meta.source": "Fontkodo", + "nav": "Navigado", + "search.config.lang": "es", + "search.placeholder": "Serĉo", + "search.reset": "Klara", + "search.result.placeholder": "Tajpu por komenci serĉadon", + "search.result.none": "Neniuj kongruaj dokumentoj", + "search.result.one": "1 kongrua dokumento", + "search.result.other": "# kongruaj dokumentoj", + "source": "Iru al deponejo", + "source.file.date.created": "Kreita", + "source.file.date.updated": "Lasta ĝisdatigo", + "tabs": "Langetoj", + "toc": "Enhavtabelo" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/es.html b/docs/src/templates/partials/languages/es.html new file mode 100644 index 00000000..bbbd9dc1 --- /dev/null +++ b/docs/src/templates/partials/languages/es.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "es", + "action.edit": "Editar esta página", + "action.skip": "Saltar a contenido", + "action.view": "Ver código fuente de esta página", + "announce.dismiss": "No mostrar esto de nuevo", + "blog.archive": "Archivo", + "blog.categories": "Categorías", + "blog.categories.in": "en", + "blog.continue": "Seguir leyendo", + "blog.draft": "Borrador", + "blog.index": "Regresar al índice", + "blog.meta": "Metadata", + "blog.references": "Enlaces relacionados", + "clipboard.copy": "Copiar al portapapeles", + "clipboard.copied": "Copiado al portapapeles", + "consent.accept": "Aceptar", + "consent.manage": "Gestionar cookies", + "consent.reject": "Rechazar", + "footer": "Pie", + "footer.next": "Siguiente", + "footer.previous": "Anterior", + "header": "Cabecera", + "meta.comments": "Comentarios", + "meta.source": "Fuente", + "nav": "Navegación", + "readtime.one": "1 minuto de lectura", + "readtime.other": "# minutos de lectura", + "rss.created": "Fuente RSS", + "rss.updated": "Fuente RSS de contenido actualizado", + "search": "Buscar", + "search.config.lang": "es", + "search.placeholder": "Búsqueda", + "search.share": "Compartir", + "search.reset": "Limpiar", + "search.result.initializer": "Inicializando búsqueda", + "search.result.placeholder": "Teclee para comenzar búsqueda", + "search.result.none": "No se encontraron documentos", + "search.result.one": "1 documento encontrado", + "search.result.other": "# documentos encontrados", + "search.result.more.one": "1 más en esta página", + "search.result.more.other": "# más en esta página", + "search.result.term.missing": "Falta", + "select.language": "Seleccionar idioma", + "select.version": "Seleccionar versión", + "source": "Ir al repositorio", + "source.file.contributors": "Contribuidores", + "source.file.date.created": "Creado", + "source.file.date.updated": "Última actualización", + "tabs": "Pestañas", + "toc": "Tabla de contenidos", + "top": "Volver al principio" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/et.html b/docs/src/templates/partials/languages/et.html new file mode 100644 index 00000000..8add4225 --- /dev/null +++ b/docs/src/templates/partials/languages/et.html @@ -0,0 +1,43 @@ + + + +{% macro t(key) %}{{ { + "language": "et", + "action.edit": "Muuda seda lehte", + "action.skip": "Keri sisuni", + "clipboard.copy": "Kopeeri lõikelauale", + "clipboard.copied": "Kopeeritud", + "footer.next": "Järgmine", + "footer.previous": "Eelmine", + "meta.comments": "Kommentaarid", + "meta.source": "Lähtekood", + "search.placeholder": "Otsi", + "search.result.placeholder": "Otsimiseks kirjuta siia", + "search.result.none": "Otsingule ei leitud ühtegi vastet", + "search.result.one": "Leiti üks tulemus", + "search.result.other": "Leiti # tulemust", + "source": "Ava repositooriumis", + "source.file.date.created": "Loodud", + "source.file.date.updated": "Viimane uuendus", + "toc": "Sisukord" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/eu.html b/docs/src/templates/partials/languages/eu.html new file mode 100644 index 00000000..0e52f925 --- /dev/null +++ b/docs/src/templates/partials/languages/eu.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "eu", + "action.edit": "Editatu orri hau", + "action.skip": "Joan zuzenean edukira", + "action.view": "Ikusi orri honen iturburua", + "announce.dismiss": "Ez erakutsi hau berriro", + "blog.archive": "Artxiboa", + "blog.categories": "Kategoriak", + "blog.categories.in": "kategoria", + "blog.continue": "Jarraitu irakurtzen", + "blog.draft": "Zirriborroa", + "blog.index": "Itzuli aurkibidera", + "blog.meta": "Metadatuak", + "blog.references": "Erlazionatutako estekak", + "clipboard.copy": "Kopiatu arbelean", + "clipboard.copied": "Arbelean kopiatuta", + "consent.accept": "Onartu", + "consent.manage": "Kudeatu ezarpenak", + "consent.reject": "Ukatu", + "footer": "Orri-oina", + "footer.next": "Hurrengoa", + "footer.previous": "Aurrekoa", + "header": "Atalburua", + "meta.comments": "Iruzkinak", + "meta.source": "Iturburua", + "nav": "Nabigazioa", + "readtime.one": "Minutu batean irakurtzeko", + "readtime.other": "# minututan irakurtzeko", + "rss.created": "RSS jarioa", + "rss.updated": "Eduki eguneratuen RSS jarioa", + "search": "Bilatu", + "search.placeholder": "Bilatu", + "search.share": "Partekatu", + "search.reset": "Garbitu", + "search.result.initializer": "Bilaketa hasieratzen", + "search.result.placeholder": "Idatzi bilatzen hasteko", + "search.result.none": "Bat datorren dokumenturik ez", + "search.result.one": "Bat datorren dokumentu bat", + "search.result.other": "Bat datozen # dokumentu", + "search.result.more.one": "Bat gehiago orri honetan", + "search.result.more.other": "# gehiago orri honetan", + "search.result.term.missing": "Falta da", + "select.language": "Hautatu hizkuntza", + "select.version": "Hautatu bertsioa", + "source": "Joan biltegira", + "source.file.contributors": "Kolaboratzaileak", + "source.file.date.created": "Sortze data", + "source.file.date.updated": "Azken eguneratzea", + "tabs": "Fitxak", + "toc": "Edukiak", + "top": "Igo goraino" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/fa.html b/docs/src/templates/partials/languages/fa.html new file mode 100644 index 00000000..deaa8bca --- /dev/null +++ b/docs/src/templates/partials/languages/fa.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "fa", + "direction": "rtl", + "action.edit": "این صفحه را ویرایش کنید", + "action.skip": "پرش به محتویات", + "action.view": "محتویات این صفحه را نشان بده", + "announce.dismiss": "این را دیگر نشان نده", + "blog.archive": "بایگانی", + "blog.categories": "دسته‌بندی‌ها", + "blog.categories.in": "در", + "blog.continue": "ادامه به خواندن", + "blog.draft": "پیش‌نویس", + "blog.index": "برگشت به فهرست", + "blog.meta": "فراداده", + "blog.references": "پیوندهای مربوط", + "clipboard.copy": "کپی کردن", + "clipboard.copied": "کپی شد", + "consent.accept": "تایید", + "consent.manage": "مدیریت تنظیمات", + "consent.reject": "رد کردن", + "footer": "پاورقی", + "footer.next": "بعدی", + "footer.previous": "قبلی", + "header": "سرتیتر", + "meta.comments": "نظرات", + "meta.source": "منبع", + "nav": "هدایت", + "readtime.one": "1 دقیقه زمان خواندن", + "readtime.other": "# دقیقه زمان خواندن", + "rss.created": "خوراک آراس‌اس", + "rss.updated": "خوراک آراساس محتویات به‌روز شده", + "search": "جستجو", + "search.config.pipeline": " ", + "search.placeholder": "جستجو", + "search.share": "هم‌رسانی", + "search.reset": "بازنشانی", + "search.result.initializer": "راه‌اندازی جستجو", + "search.result.placeholder": "برای شروع جستجو تایپ کنید", + "search.result.none": "سندی یافت نشد", + "search.result.one": "1 سند یافت شد", + "search.result.other": "# سند یافت شد", + "search.result.more.one": "1 مورد دیگر در این صفحه", + "search.result.more.other": "# مورد دیگر در این صفحه", + "search.result.term.missing": "موجود نیست", + "select.language": "انتخاب زبان", + "select.version": "انتخاب ویرایش", + "source": "رفتن به مخزن", + "source.file.contributors": "مشارکت کنندگان", + "source.file.date.created": "ایجاد شده", + "source.file.date.updated": "اخرین بروزرسانی", + "tabs": "زبانه‌ها", + "toc": "فهرست موضوعات", + "top": "برگشت به بالا" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/fi.html b/docs/src/templates/partials/languages/fi.html new file mode 100644 index 00000000..8ee09122 --- /dev/null +++ b/docs/src/templates/partials/languages/fi.html @@ -0,0 +1,44 @@ + + + +{% macro t(key) %}{{ { + "language": "fi", + "action.edit": "Muokkaa tätä sivua", + "action.skip": "Hyppää sisältöön", + "clipboard.copy": "Kopioi leikepöydälle", + "clipboard.copied": "Kopioitu leikepöydälle", + "footer.next": "Seuraava", + "footer.previous": "Edellinen", + "meta.comments": "Kommentit", + "meta.source": "Lähdekodi", + "search.config.lang": "fi", + "search.placeholder": "Hae", + "search.result.placeholder": "Kirjoita aloittaaksesi haun", + "search.result.none": "Ei täsmääviä dokumentteja", + "search.result.one": "1 täsmäävä dokumentti", + "search.result.other": "# täsmäävää dokumenttia", + "source": "Mene repositoryyn", + "source.file.date.created": "Luotu", + "source.file.date.updated": "Viimeisin päivitys", + "toc": "Sisällysluettelo" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/fr.html b/docs/src/templates/partials/languages/fr.html new file mode 100644 index 00000000..9d49535a --- /dev/null +++ b/docs/src/templates/partials/languages/fr.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "fr", + "action.edit": "Editer cette page", + "action.skip": "Aller au contenu", + "action.view": "Consulter la source de cette page", + "announce.dismiss": "Ne plus montrer cela", + "blog.archive": "Archive", + "blog.categories": "Catégories", + "blog.categories.in": "dans", + "blog.continue": "Continuer à lire", + "blog.draft": "Brouillon", + "blog.index": "Retourner à l'index", + "blog.meta": "Metadonnées", + "blog.references": "Liens connexes", + "clipboard.copy": "Copier dans le presse-papier", + "clipboard.copied": "Copié dans le presse-papier", + "consent.accept": "Accepter", + "consent.manage": "Paramétrer vos choix", + "consent.reject": "Refuser", + "footer": "Pied de page", + "footer.next": "Suivant", + "footer.previous": "Précédent", + "header": "En-tête", + "meta.comments": "Commentaires", + "meta.source": "Source", + "nav": "Navigation", + "readtime.one": "1 min de lecture", + "readtime.other": "# min de lecture", + "rss.created": "Flux RSS", + "rss.updated": "Flux RSS du contenu mis à jour", + "search": "Recherche", + "search.config.lang": "fr", + "search.placeholder": "Rechercher", + "search.share": "Partager", + "search.reset": "Effacer", + "search.result.initializer": "Initialisation de la recherche", + "search.result.placeholder": "Taper pour démarrer la recherche", + "search.result.none": "Aucun document trouvé", + "search.result.one": "1 document trouvé", + "search.result.other": "# documents trouvés", + "search.result.more.one": "1 de plus sur cette page", + "search.result.more.other": "# de plus sur cette page", + "search.result.term.missing": "Non trouvé", + "select.language": "Sélectionner la langue", + "select.version": "Sélectionner la version", + "source": "Aller au dépôt", + "source.file.contributors": "Contributeurs", + "source.file.date.created": "Créé", + "source.file.date.updated": "Dernière mise à jour", + "tabs": "Onglets", + "toc": "Table des matières", + "top": "Retour en haut de la page" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/gl.html b/docs/src/templates/partials/languages/gl.html new file mode 100644 index 00000000..ecb54ffd --- /dev/null +++ b/docs/src/templates/partials/languages/gl.html @@ -0,0 +1,56 @@ + + + +{% macro t(key) %}{{ { + "language": "gl", + "action.edit": "Editar esta páxina", + "action.skip": "Ir ao contido", + "clipboard.copy": "Copiar no cortapapeis", + "clipboard.copied": "Copiado no cortapapeis", + "footer": "Pé", + "footer.next": "Seguinte", + "footer.previous": "Anterior", + "header": "Cabeceira", + "meta.comments": "Comentarios", + "meta.source": "Fonte", + "nav": "Navegación", + "search.config.lang": "es", + "search.placeholder": "Procura", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando procura", + "search.result.placeholder": "Insira un termo", + "search.result.none": "Sen resultados", + "search.result.one": "1 resultado atopado", + "search.result.other": "# resultados atopados", + "search.result.more.one": "1 máis nesta páxina", + "search.result.more.other": "# máis nesta páxina", + "search.result.term.missing": "Falta", + "select.language": "Seleccionar idioma", + "select.version": "Seleccionar version", + "source": "Ir ao repositorio", + "source.file.date.created": "Creada", + "source.file.date.updated": "Última actualización", + "tabs": "Pestanas", + "toc": "Táboa de contidos", + "top": "Volver ao principio" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/he.html b/docs/src/templates/partials/languages/he.html new file mode 100644 index 00000000..128edab5 --- /dev/null +++ b/docs/src/templates/partials/languages/he.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "he", + "direction": "rtl", + "action.edit": "עריכת הדף הזה", + "action.skip": "לדלג לתוכן", + "action.view": "צפה במקור של דף זה", + "announce.dismiss": "לא להציג את זה שוב", + "blog.archive": "ארכיון", + "blog.categories": "קטגוריות", + "blog.categories.in": "בתוך", + "blog.continue": "המשך לקרוא", + "blog.draft": "טיוטה", + "blog.index": "חזרה לאינדקס", + "blog.meta": "מטא-נתונים", + "blog.references": "קישורים קשורים", + "clipboard.copy": "העתקה ללוח", + "clipboard.copied": "הועתק ללוח", + "consent.accept": "לקבל", + "consent.manage": "לנהל הגדרות", + "consent.reject": "לדחות", + "footer": "כותרת תחתונה", + "footer.next": "הבא", + "footer.previous": "הקודם", + "header": "כותרת עליונה", + "meta.comments": "הערות", + "meta.source": "מקור", + "nav": "ניווט", + "readtime.one": "קריאה 1 דקות", + "readtime.other": "# דקות קריאה", + "rss.created": "RSS הזנת", + "rss.updated": "הזנת RSS של תוכן מעודכן", + "search": "חיפוש", + "search.config.pipeline": " ", + "search.placeholder": "חיפוש", + "search.share": "שיתוף", + "search.reset": "ניקוי", + "search.result.initializer": "אתחול חיפוש", + "search.result.placeholder": "יש להקליד כדי להתחיל לחפש", + "search.result.none": "אין מסמכים תואמים", + "search.result.one": "מסמך1 תואם", + "search.result.other": "# מסמך תואם", + "search.result.more.one": "עוד אחד בדף הזה", + "search.result.more.other": "עוד # בדף הזה", + "search.result.term.missing": "חסר", + "select.language": "בחירת שפה", + "select.version": "בחירת גרסה", + "source": "לעבור אל המאגר", + "source.file.contributors": "תורמים", + "source.file.date.created": "נוצר", + "source.file.date.updated": "עדכון אחרון", + "tabs": "לשוניות", + "toc": "תוכן העניינים", + "top": "חזרה למעלה" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/hi.html b/docs/src/templates/partials/languages/hi.html new file mode 100644 index 00000000..8c27c259 --- /dev/null +++ b/docs/src/templates/partials/languages/hi.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "hi", + "action.edit": "इस पृष्ठ को संपादित करें", + "action.skip": "विषय पर बढ़ें", + "action.view": "इस पृष्ठ का सूत्र देखें", + "announce.dismiss": "इसे पुनः न दिखायें", + "blog.archive": "पुरालेख", + "blog.categories": "वर्ग", + "blog.categories.in": "में", + "blog.continue": "पढ़ते रहिये", + "blog.draft": "मसौदा", + "blog.index": "सूचि को लौटें", + "blog.meta": "मेटाडेटा", + "blog.references": "सम्बंधित लिंक", + "clipboard.copy": "क्लिपबोर्ड पर कॉपी करें", + "clipboard.copied": "क्लिपबोर्ड पर कॉपी कर दिया गया", + "consent.accept": "स्वीकार करें", + "consent.manage": "सेटिंग्स मैनेज करें", + "consent.reject": "अस्वीकार करें", + "footer": "फुटर", + "footer.next": "आगामी", + "footer.previous": "पिछला", + "header": "शीर्षक", + "meta.comments": "टिप्पणियाँ", + "meta.source": "स्रोत", + "nav": "नैविगेशन", + "readtime.one": "1 मिनट पढ़ने को", + "readtime.other": "# मिनट पढ़ने को", + "rss.created": "RSS फीड", + "rss.updated": "नवीनतम विषयवस्तु का RSS feed", + "search": "खोजें", + "search.config.lang": "hi", + "search.placeholder": "खोज", + "search.share": "शेयर करें", + "search.reset": "हटा दें", + "search.result.initializer": "खोज शुरू करें", + "search.result.placeholder": "खोज शुरू करने के लिए टाइप करें", + "search.result.none": "कोई मिलान डॉक्यूमेंट नहीं", + "search.result.one": "1 मिलान डॉक्यूमेंट", + "search.result.other": "# मिलान डाक्यूमेंट्स", + "search.result.more.one": "1 और इस पृष्ठ पर", + "search.result.more.other": "# और इस पृष्ठ पर", + "search.result.term.missing": "ग़ायब", + "select.language": "भाषा चुनें", + "select.version": "वर्शन चुनें", + "source": "रिपॉजिटरी पर जाएं", + "source.file.contributors": "योगदाता", + "source.file.date.created": "बनाया था", + "source.file.date.updated": "आखिरी अपडेट", + "tabs": "टैब", + "toc": "विषय - सूची", + "top": "शीर्षभाग पर लौटें" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/hr.html b/docs/src/templates/partials/languages/hr.html new file mode 100644 index 00000000..30afe6a9 --- /dev/null +++ b/docs/src/templates/partials/languages/hr.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "hr", + "action.edit": "Uredi stranicu", + "action.skip": "Preskoči na sadržaj", + "action.view": "Pregledaj izvorni kod ove stranice", + "announce.dismiss": "Ne prikazuj ovo opet", + "blog.archive": "Arhiva", + "blog.categories": "Kategorije", + "blog.categories.in": "u", + "blog.continue": "Nastavi čitati", + "blog.draft": "Nacrt", + "blog.index": "Natrag na indeks", + "blog.meta": "Metapodaci", + "blog.references": "Srodne poveznice", + "clipboard.copy": "Kopiraj u međuspremnik", + "clipboard.copied": "Kopirano u međuspremnik", + "consent.accept": "Prihvati", + "consent.manage": "Upravljaj postavkama", + "consent.reject": "Odbij", + "footer": "Podnožje", + "footer.next": "Sljedeće", + "footer.previous": "Prethodno", + "header": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav": "Navigacija", + "readtime.one": "1 minuta čitanja", + "readtime.other": "# minut(a/e) čitanja", + "rss.created": "RSS izvor", + "rss.updated": "RSS izvor osvježenog sadržaja", + "search": "Pretraživanje", + "search.placeholder": "Pretraži", + "search.share": "Podijeli", + "search.reset": "Očisti", + "search.result.initializer": "Inicijaliziranje pretraživanja", + "search.result.placeholder": "Unesite pojam pretraživanja", + "search.result.none": "Ništa nije pronađeno", + "search.result.one": "1 rezultat pretraživanja", + "search.result.other": "# rezultat(a) pretraživanja", + "search.result.more.one": "1 rezultat na ovoj stranici", + "search.result.more.other": "# rezultat(a) na ovoj stranici", + "search.result.term.missing": "Nedostaje", + "select.language": "Odabir jezika", + "select.version": "Odabir verzije", + "source": "Idi u repozitorij", + "source.file.contributors": "Suradnici", + "source.file.date.created": "Stvaranje", + "source.file.date.updated": "Posljednje ažuriranje", + "tabs": "Kartice", + "toc": "Sadržaj", + "top": "Vrati se na vrh" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/hu.html b/docs/src/templates/partials/languages/hu.html new file mode 100644 index 00000000..44798dd8 --- /dev/null +++ b/docs/src/templates/partials/languages/hu.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "hu", + "action.edit": "Oldal szerkesztése", + "action.skip": "Kihagyás", + "action.view": "Oldal forrásának megtekintése", + "announce.dismiss": "Ne mutasd többet", + "blog.archive": "Archívum", + "blog.categories": "Kategóriák", + "blog.categories.in": "Kategória:", + "blog.continue": "Folytatás", + "blog.draft": "Piszkozat", + "blog.index": "Vissza a főoldalra", + "blog.meta": "Metaadat", + "blog.references": "Kapcsolódó linkek", + "clipboard.copy": "Másolás vágólapra", + "clipboard.copied": "Vágólapra másolva", + "consent.accept": "Elfogadás", + "consent.manage": "Beállítások", + "consent.reject": "Visszautasítás", + "footer": "Élőláb", + "footer.next": "Következő", + "footer.previous": "Előző", + "header": "Élőfej", + "meta.comments": "Hozzászólások", + "meta.source": "Forrás", + "nav": "Navigáció", + "readtime.one": "1 percnyi", + "readtime.other": "# percnyi", + "rss.created": "RSS feed", + "rss.updated": "Frissített tartalom RSS feedje", + "search": "Keresés", + "search.config.lang": "hu", + "search.placeholder": "Keresés", + "search.share": "Megosztás", + "search.reset": "Törlés", + "search.result.initializer": "Keresés inicializálása", + "search.result.placeholder": "Kereséshez írj ide valamit", + "search.result.none": "Nincs találat", + "search.result.one": "1 egyező dokumentum", + "search.result.other": "# egyező dokumentum", + "search.result.more.one": "1 további találat az oldalon", + "search.result.more.other": "# további találat az oldalon", + "search.result.term.missing": "Üres", + "select.language": "Nyelvváltás", + "select.version": "Verzióváltás", + "source": "Főoldalra ugrás", + "source.file.contributors": "Szerzők", + "source.file.date.created": "Létrehozva", + "source.file.date.updated": "Utolsó frissítés", + "tabs": "Lapok", + "toc": "Tartalomjegyzék", + "top": "Vissza a tetejére" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/hy.html b/docs/src/templates/partials/languages/hy.html new file mode 100644 index 00000000..e3418341 --- /dev/null +++ b/docs/src/templates/partials/languages/hy.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "hy", + "action.edit": "Խմբագրել այս էջը", + "action.skip": "Անցնել պարունակությանը", + "action.view": "Դիտել այս էջի սկզբնաղբյուրը", + "announce.dismiss": "Այլևս չցուցադրել", + "blog.archive": "Արխիվ", + "blog.categories": "Կատեգորիաներ", + "blog.categories.in": "in", + "blog.continue": "Շարունակել կարդալ", + "blog.draft": "Սևագիր", + "blog.index": "Հետ դեպի ինդեքս", + "blog.meta": "Մետատվյալներ", + "blog.references": "Առնչվող հղումներ", + "clipboard.copy": "Պատճենել", + "clipboard.copied": "Պատճենված է", + "consent.accept": "Ընդունել", + "consent.manage": "Կառավարել կարգավորումները", + "consent.reject": "Մերժել", + "footer": "Էջատակ", + "footer.next": "Հաջորդը", + "footer.previous": "Նախորդը", + "header": "Գլխագիր", + "meta.comments": "Մեկնաբանությունները", + "meta.source": "Աղբյուր", + "nav": "Տեղորոշում", + "readtime.one": "Ընթերցում՝ 1 րոպե", + "readtime.other": "Ընթերցում՝ # րոպե", + "rss.created": "RSS հոսք", + "rss.updated": "Արդիացված բովանդակության RSS հոսք", + "search": "Որոնում", + "search.config.pipeline": " ", + "search.placeholder": "Որոնում", + "search.share": "Կիսվել", + "search.reset": "Ջնջել", + "search.result.initializer": "Որոնում", + "search.result.placeholder": "Մուտքագրեք որոնելու համար", + "search.result.none": "Արդյունքներ չկան", + "search.result.one": "1 արդյունք", + "search.result.other": "# արդյունք", + "search.result.more.one": "ևս 1-ը այս էջում", + "search.result.more.other": "ևս #-ը այս էջում", + "search.result.term.missing": "Բացակայում է", + "select.language": "Ընտրել լեզուն", + "select.version": "Ընտրել տարբերակը", + "source": "Դեպի պահոց", + "source.file.contributors": "Հեղինակողներ", + "source.file.date.created": "Ստեղծված է", + "source.file.date.updated": "Վերջին թարմացումը", + "tabs": "Ներդիրներ", + "toc": "Բովանդակություն", + "top": "Վերադառնալ սկիզբ" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/id.html b/docs/src/templates/partials/languages/id.html new file mode 100644 index 00000000..c54229f0 --- /dev/null +++ b/docs/src/templates/partials/languages/id.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "id", + "action.edit": "Ubah halaman ini", + "action.skip": "Lewati ke isi", + "action.view": "Lihat sumber halaman ini", + "announce.dismiss": "Jangan lihat ini lagi", + "blog.archive": "Arsip", + "blog.categories": "Kategori", + "blog.categories.in": "dalam", + "blog.continue": "Lanjut membaca", + "blog.draft": "Draf", + "blog.index": "Kembali ke indeks", + "blog.meta": "Metadata", + "blog.references": "Tautan yang berhubungan", + "clipboard.copy": "Salin ke clipboard", + "clipboard.copied": "Tersalin ke clipboard", + "consent.accept": "Terima", + "consent.manage": "Kelola pengaturan", + "consent.reject": "Tolak", + "footer": "Footer", + "footer.next": "Selanjutnya", + "footer.previous": "Sebelumnya", + "header": "Header", + "meta.comments": "Komentar", + "meta.source": "Sumber", + "nav": "Navigasi", + "readtime.one": "1 menit baca", + "readtime.other": "# menit baca", + "rss.created": "Umpan RSS", + "rss.updated": "Umpan RSS dari konten yang diperbarui", + "search": "Cari", + "search.config.pipeline": " ", + "search.placeholder": "Cari", + "search.share": "Bagikan", + "search.reset": "Kosongkan", + "search.result.initializer": "Mempersiapkan pencarian", + "search.result.placeholder": "Ketik untuk mulai pencarian", + "search.result.none": "Tidak ada dokumen yang sesuai", + "search.result.one": "1 dokumen ditemukan", + "search.result.other": "# dokumen ditemukan", + "search.result.more.one": "1 lagi di halaman ini", + "search.result.more.other": "# lagi di halaman ini", + "search.result.term.missing": "Tidak ada", + "select.language": "Pilih bahasa", + "select.version": "Pilih versi", + "source": "Ke repositori", + "source.file.contributors": "Kontributor", + "source.file.date.created": "Dibuat", + "source.file.date.updated": "Pembaruan terakhir", + "tabs": "Tab", + "toc": "Daftar isi", + "top": "Kembali ke atas" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/is.html b/docs/src/templates/partials/languages/is.html new file mode 100644 index 00000000..5b9a47e8 --- /dev/null +++ b/docs/src/templates/partials/languages/is.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "is", + "action.edit": "Breyta þessari síðu", + "action.skip": "Hoppa yfir í efnið", + "action.view": "Skoða frumgögn þessarar síðu", + "announce.dismiss": "Ekki sýna þetta aftur", + "blog.archive": "Safn", + "blog.categories": "Flokkar", + "blog.categories.in": "í", + "blog.continue": "Lesa meira", + "blog.draft": "Uppkast", + "blog.index": "Til baka í yfirlit", + "blog.meta": "Lýsigögn", + "blog.references": "Þessu tengt", + "clipboard.copy": "Afrita á klemmuspjald", + "clipboard.copied": "Afritað á klemmuspjald", + "consent.accept": "Samþykkja", + "consent.manage": "Breyta stillingum", + "consent.reject": "Hafna", + "footer": "Síðufótur", + "footer.next": "Næsta", + "footer.previous": "Fyrri", + "header": "Haus", + "meta.comments": "Umræður", + "meta.source": "Frumgögn", + "nav": "Valmynd", + "readtime.one": "1 mín lestur", + "readtime.other": "# mín lestur", + "rss.created": "RSS veita", + "rss.updated": "RSS veita fyrir uppfært innihald", + "search": "Leita", + "search.placeholder": "Leita", + "search.share": "Deila", + "search.reset": "Hreinsa", + "search.result.initializer": "Ræsi leitarvél", + "search.result.placeholder": "Byrjaðu að skrifa til að hefja leit", + "search.result.none": "Engar síður fundust", + "search.result.one": "1 síða fannst", + "search.result.other": "# síður fundust", + "search.result.more.one": "1 til viðbótar á þessari síðu", + "search.result.more.other": "# til viðbótar á þessari síðu", + "search.result.term.missing": "Vantar", + "select.language": "Veldu tungumál", + "select.version": "Veldu útgáfu", + "source": "Fara í gagnageymslu", + "source.file.contributors": "Meðhöfundar", + "source.file.date.created": "Búið til", + "source.file.date.updated": "Síðast uppfært", + "tabs": "Flipar", + "toc": "Efnisyfirlit", + "top": "Fara aftur efst" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/it.html b/docs/src/templates/partials/languages/it.html new file mode 100644 index 00000000..77956ee7 --- /dev/null +++ b/docs/src/templates/partials/languages/it.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "it", + "action.edit": "Modifica", + "action.skip": "Vai al contenuto", + "action.view": "Vedi il sorgente di questa pagina", + "announce.dismiss": "Non mostrare più", + "blog.archive": "Archivio", + "blog.categories": "Categorie", + "blog.categories.in": "in", + "blog.continue": "Continua a leggere", + "blog.draft": "Bozza", + "blog.index": "Torna all'indice", + "blog.meta": "Metadati", + "blog.references": "Collegamenti", + "clipboard.copy": "Copia", + "clipboard.copied": "Copiato", + "consent.accept": "Accetta", + "consent.manage": "Gestisci le opzioni", + "consent.reject": "Rifiuta", + "footer": "Piede", + "footer.next": "Successivo", + "footer.previous": "Precedente", + "header": "Intestazione", + "meta.comments": "Commenti", + "meta.source": "Sorgente", + "nav": "Navigazione", + "readtime.one": "1 minuto di lettura", + "readtime.other": "# minuti di lettura", + "rss.created": "Feed RSS", + "rss.updated": "Contenuto aggiornato del feed RSS", + "search": "Cerca", + "search.config.lang": "it", + "search.placeholder": "Cerca", + "search.share": "Condividi", + "search.reset": "Cancella", + "search.result.initializer": "Inizializza la ricerca", + "search.result.placeholder": "Scrivi per iniziare a cercare", + "search.result.none": "Nessun documento trovato", + "search.result.one": "1 documento trovato", + "search.result.other": "# documenti trovati", + "search.result.more.one": "1 altro in questa pagina", + "search.result.more.other": "# altri in questa pagina", + "search.result.term.missing": "Non presente", + "select.language": "Seleziona la lingua", + "select.version": "Seleziona la versione", + "source": "Apri repository", + "source.file.contributors": "Contributori", + "source.file.date.created": "Creata", + "source.file.date.updated": "Ultimo aggiornamento", + "tabs": "Tabs", + "toc": "Indice", + "top": "Torna su" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ja.html b/docs/src/templates/partials/languages/ja.html new file mode 100644 index 00000000..b4b4279d --- /dev/null +++ b/docs/src/templates/partials/languages/ja.html @@ -0,0 +1,78 @@ + + + +{% macro t(key) %}{{ { + "language": "ja", + "action.edit": "編集", + "action.skip": "コンテンツにスキップ", + "action.view": "このページの原文を表示", + "announce.dismiss": "非表示にします", + "blog.archive": "過去の投稿", + "blog.categories": "カテゴリー", + "blog.categories.in": "", + "blog.continue": "続きを読む", + "blog.draft": "下書き", + "blog.index": "ブログトップへ戻る", + "blog.meta": "メタデータ", + "blog.references": "関連リンク", + "clipboard.copy": "クリップボードへコピー", + "clipboard.copied": "コピーしました", + "consent.accept": "同意", + "consent.manage": "サイトの設定", + "consent.reject": "拒否", + "footer": "フッター", + "footer.next": "次", + "footer.previous": "前", + "header": "ヘッダー", + "meta.comments": "コメント", + "meta.source": "ソース", + "nav": "ナビゲーション", + "readtime.one": "このページは約1分で読めます", + "readtime.other": "このページは約#分で読めます", + "rss.created": "新しいページのRSSフィード", + "rss.updated": "更新されたページのRSSフィード", + "search": "検索", + "search.config.lang": "ja", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\- 、。,.]+", + "search.placeholder": "検索", + "search.share": "共有", + "search.reset": "クリア", + "search.result.initializer": "検索を初期化", + "search.result.placeholder": "検索キーワードを入力してください", + "search.result.none": "何も見つかりませんでした", + "search.result.one": "1件見つかりました", + "search.result.other": "#件見つかりました", + "search.result.more.one": "このページ内にもう1件見つかりました", + "search.result.more.other": "このページ内にあと#件見つかりました", + "search.result.term.missing": "検索に含まれない", + "select.language": "言語切り替え", + "select.version": "バージョン切り替え", + "source": "リポジトリへ", + "source.file.contributors": "投稿者", + "source.file.date.created": "作成日", + "source.file.date.updated": "最終更新日", + "tabs": "タブ", + "toc": "目次", + "top": "ページトップへ戻る" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ka.html b/docs/src/templates/partials/languages/ka.html new file mode 100644 index 00000000..edfd2e02 --- /dev/null +++ b/docs/src/templates/partials/languages/ka.html @@ -0,0 +1,49 @@ + + + +{% macro t(key) %}{{ { + "language": "ka", + "action.edit": "გვერდის რედარქირება", + "action.skip": "კონტენტზე გადასვლა", + "clipboard.copy": "კოპირება", + "clipboard.copied": "კოპირებულია", + "footer.next": "შემდეგი", + "footer.previous": "წინა", + "meta.comments": "კომენტარები", + "meta.source": "წყარო", + "nav": "ნავიგაცია", + "search.config.pipeline": " ", + "search.placeholder": "ძებნა", + "search.reset": "გასუფთავება", + "search.result.placeholder": "ჩაწერე ძებნის დასაწყებად", + "search.result.none": "დოკუმენტი ვერ მოიძებნა", + "search.result.one": "მოიძებნა 1 დოკუმენტი", + "search.result.other": "მოიძებნა # დოკუმენტი", + "search.result.more.one": "კიდევ 1 ამ გვერდზე", + "search.result.more.other": "კიდევ # ამ გვერდზე", + "source": "საცავში გადასვლა", + "source.file.date.created": "შეიქმნა", + "source.file.date.updated": "ბოლო განახლება", + "tabs": "ტაბები", + "toc": "სარჩევი" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/kn.html b/docs/src/templates/partials/languages/kn.html new file mode 100644 index 00000000..bd0ff722 --- /dev/null +++ b/docs/src/templates/partials/languages/kn.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "kn", + "action.edit": "ಈ ಪುಟವನ್ನು ತಿದ್ದುಪಡಿ ಮಾಡಿ", + "action.skip": "ವಿಷಯಕ್ಕೆ ತೆರಳಿ", + "action.view": "ಈ ಪುಟದ ಮೂಲವನ್ನು ವೀಕ್ಷಿಸಿ", + "announce.dismiss": "ಇದನ್ನು ಮತ್ತೊಮ್ಮೆ ತೋರಿಸಬೇಡಿ", + "blog.archive": "ಹಳೆಯ ಲೇಖನ", + "blog.categories": "ವರ್ಗಗಳು", + "blog.categories.in": "ರಲ್ಲಿ", + "blog.continue": "ಓದು ಮುಂದುವರೆಸಿ", + "blog.draft": "ಆರಂಭಿಕ ಬರವಣಿಗೆ", + "blog.index": "ಸೂಚ್ಯಂಕಕ್ಕೆ ಹಿಂತಿರುಗಿ", + "blog.meta": "ಮಾಹಿತಿಯ ಬಗ್ಗೆ ಮಾಹಿತಿ", + "blog.references": "ಸಂಬಂಧಿತ ಉಲ್ಲೇಖಗಳು", + "clipboard.copy": "ಇದನ್ನು ನಕಲಿಸಿ", + "clipboard.copied": "ಇದನ್ನು ನಕಲು ಮಾಡಿದೆ", + "consent.accept": "ನಾನು ಇದನ್ನು ಒಪ್ಪಿಕೊಳ್ಳುತ್ತೇನೆ", + "consent.manage": "ಸಂರಚನೆಯನ್ನು ನಿರ್ವಹಿಸಿ", + "consent.reject": "ನಾನು ಇದನ್ನು ತಿರಸ್ಕರಿಸುತ್ತೇನೆ", + "footer": "ಅಡಿಟಿಪ್ಪಣಿ", + "footer.next": "ಮುಂದಿನ ಸಂಚಿಕೆ", + "footer.previous": "ಹಿಂದಿನ ಸಂಚಿಕೆ", + "header": "ಮೇಲ್ಟಿಪ್ಪಣಿ", + "meta.comments": "ಪ್ರತಿಕ್ರಿಯೆಗಳು", + "meta.source": "ಮೂಲ", + "nav": "ಸಂಚರಣೆ", + "readtime.one": "ಓದಲು ೧ ನಿಮಿಷ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", + "readtime.other": "ಓದಲು # ನಿಮಿಷಗಳನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", + "rss.created": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆ", + "rss.updated": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆಯಿಂದ ಇತ್ತೀಚಿನ ನವೀಕರಣ", + "search": "ಹುಡುಕಿ", + "search.placeholder": "ಹುಡುಕಿ", + "search.share": "ಹಂಚಿಕೊಳ್ಳಿ", + "search.reset": "ಅಳಿಸು", + "search.result.initializer": "ಹುಡುಕಾಟವನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ", + "search.result.placeholder": "ಬರೆಯುವ ಮೂಲಕ ಹುಡುಕಲು ಪ್ರಾರಂಭಿಸಿ", + "search.result.none": "ಹೊಂದಾಣಿಕೆಯಾಗುವ ದಾಖಲೆಗಳಿಲ್ಲ", + "search.result.one": "೧ ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಯಿದೆ", + "search.result.other": "# ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಗಳಿವೆ", + "search.result.more.one": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ ಒಂದು ಕಂಡುಬಂದಿದೆ", + "search.result.more.other": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ # ಇವೆ", + "search.result.term.missing": "ಕಾಣೆಯಾಗಿದೆ", + "select.language": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", + "select.version": "ಆವೃತ್ತಿಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", + "source": "ಭಂಡಾರಕ್ಕೆ ಹೋಗಿ", + "source.file.contributors": "ಕೊಡುಗೆದಾರರು", + "source.file.date.created": "ರಚಿಸಿದ ದಿನಾಂಕ", + "source.file.date.updated": "ಕೊನೆಯ ನವೀಕರಣ ದಿನಾಂಕ", + "tabs": "ವಿವಿಧ ಕಿಟಕಿಗಳು", + "toc": "ವಿಷಯಗಳ ಪಟ್ಟಿ", + "top": "ಮೇಲಕ್ಕೆ ಹಿಂತಿರುಗಿ" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ko.html b/docs/src/templates/partials/languages/ko.html new file mode 100644 index 00000000..adadccb7 --- /dev/null +++ b/docs/src/templates/partials/languages/ko.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "ko", + "action.edit": "이 페이지를 편집", + "action.skip": "콘텐츠로 이동", + "action.view": "페이지소스 보기", + "announce.dismiss": "다시 안보기", + "blog.archive": "아카이브", + "blog.categories": "카테고리", + "blog.categories.in": "카테고리", + "blog.continue": "계속 읽기", + "blog.draft": "임시 저장", + "blog.index": "Index로 돌아가기", + "blog.meta": "메타데이터", + "blog.references": "관련 링크", + "clipboard.copy": "클립보드로 복사", + "clipboard.copied": "클립보드에 복사됨", + "consent.accept": "동의 허락", + "consent.manage": "동의 허락 관리", + "consent.reject": "동의 거부", + "footer": "하단/푸터", + "footer.next": "다음", + "footer.previous": "이전", + "header": "상단/헤더", + "meta.comments": "댓글", + "meta.source": "출처", + "nav": "네비게이션", + "readtime.one": "읽는시간 1분", + "readtime.other": "읽는시간 #분", + "rss.created": "RSS 피드 생성완료", + "rss.updated": "RSS 피드 업데이트완료", + "search": "검색", + "search.config.pipeline": " ", + "search.placeholder": "검색", + "search.share": "공유", + "search.reset": "지우기", + "search.result.initializer": "검색 초기화", + "search.result.placeholder": "검색어를 입력하세요", + "search.result.none": "검색어와 일치하는 문서가 없습니다", + "search.result.one": "1개의 일치하는 문서", + "search.result.other": "#개의 일치하는 문서", + "search.result.more.one": "이 문서에서 1개의 검색 결과 더 보기", + "search.result.more.other": "이 문서에서 #개의 검색 결과 더 보기", + "search.result.term.missing": "포함되지 않은 검색어", + "select.language": "언어설정", + "select.version": "버전 선택", + "source": "저장소로 이동", + "source.file.contributors": "참여자들", + "source.file.date.created": "작성일", + "source.file.date.updated": "마지막 업데이트", + "tabs": "탭", + "toc": "목차", + "top": "맨위로" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ku-IQ.html b/docs/src/templates/partials/languages/ku-IQ.html new file mode 100644 index 00000000..fe9dd1e7 --- /dev/null +++ b/docs/src/templates/partials/languages/ku-IQ.html @@ -0,0 +1,64 @@ + + + +{% macro t(key) %}{{ { + "language": "ku", + "direction": "rtl", + "action.edit": "دەستکاری ئەم پەڕە بکە", + "action.skip": "ئەم ناوەڕۆکە بپەڕێنە", + "action.view": "سەرچاوەی ئەم لاپەڕەیە نیشان بدە", + "announce.dismiss": "دووبارە ئەمە پیشان مەدە", + "clipboard.copy": "لەبەرگتنەوە بۆ کلیپبۆرد", + "clipboard.copied": "لەبەرگیرایەوە بۆ کلیپ بۆرد", + "consent.accept": "ڕازیبوون", + "consent.manage": "بەڕیوەبردنی ڕیکخستنەکان", + "consent.reject": "ڕەتکردنەوە", + "footer": "ژێرپەڕە", + "footer.next": "دواتر", + "footer.previous": "پێشتر", + "header": "ناونیشانی بەڕه", + "meta.comments": "لێدوانەکان", + "meta.source": "سەرجاوە", + "nav": "ڕێنیشاندەر", + "search": "گەڕان", + "search.config.pipeline": " ", + "search.placeholder": "گەڕان", + "search.share": "گەڕان", + "search.reset": "سڕینەوە", + "search.result.initializer": "ئامادەکردنی گەڕان", + "search.result.placeholder": "بنووسە بۆ دەستپێکردن بە گەڕان", + "search.result.none": "هیچ بەڵگەنامەیەکی هاوتا نیە", + "search.result.one": "١ بەڵگەنامەی هاوتا", + "search.result.other": "بەڵگەنامەی هاوتا #", + "search.result.more.one": "١ دانەی تر لەسەر ئەم پەڕەیە", + "search.result.more.other": "دانەی تر لەسەر ئەم پەڕەیە #", + "search.result.term.missing": "ونبوو", + "select.language": "زمان دیاریبکە", + "select.version": "وەشان دیاریبکە", + "source": "بڕۆ بۆ کۆگا", + "source.file.date.created": "دروسکت کرا", + "source.file.date.updated": "دوایین نوێکردنەوە", + "tabs": "تابەکان", + "toc": "خشتەی ناوەڕۆکەکان", + "top": "گەڕانەوە بۆ سەرەوە" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/lb.html b/docs/src/templates/partials/languages/lb.html new file mode 100644 index 00000000..f38c6568 --- /dev/null +++ b/docs/src/templates/partials/languages/lb.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "lb", + "direction": "ltr", + "action.edit": "D'Säit beaarbechten", + "action.skip": "Zum Inhalt iwwersprangen", + "action.view": "Quellcode uweisen", + "announce.dismiss": "Net erëm uweisen", + "blog.archive": "Archiv", + "blog.categories": "Kategorien", + "blog.categories.in": "an", + "blog.continue": "Weider liesen", + "blog.draft": "Skizz", + "blog.index": "Zeréck zum Index", + "blog.meta": "Metadaten", + "blog.references": "Änlech Links", + "clipboard.copy": "Kopéieren", + "clipboard.copied": "Kopéiert", + "consent.accept": "Accept", + "consent.manage": "Astellungen beaarbechten", + "consent.reject": "Ofleenen", + "footer": "Footer", + "footer.next": "Weider", + "footer.previous": "Zeréck", + "header": "Header", + "meta.comments": "Kommentaren", + "meta.source": "Quell", + "nav": "Navigatioun", + "readtime.one": "1 min Liesedauer", + "readtime.other": "# min Liesedauer", + "rss.created": "RSS feed", + "rss.updated": "RSS feed vun aktualiséiertem Inhalt", + "search": "Sichen", + "search.placeholder": "Sichen", + "search.share": "Deelen", + "search.reset": "Läschen", + "search.result.initializer": "D'Sich gëtt initialiséiert", + "search.result.placeholder": "Schreif fir eppes ze sichen", + "search.result.none": "Keng zoutreffend Dokumenter", + "search.result.one": "1 zoutreffend Dokument", + "search.result.other": "# zoutreffend Dokumenter", + "search.result.more.one": "1 méi op dëser Säit", + "search.result.more.other": "# méi op dëser Säit", + "search.result.term.missing": "Feelend", + "select.language": "Sprooch auswielen", + "select.version": "Versioun auswielen", + "source": "Op den Repository goen", + "source.file.contributors": "Matwirkender", + "source.file.date.created": "Erstallt", + "source.file.date.updated": "Läscht update", + "tabs": "Tabs", + "toc": "Inhaltsverzeichnis", + "top": "Zeréck zum Ufank" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/lt.html b/docs/src/templates/partials/languages/lt.html new file mode 100644 index 00000000..129505f5 --- /dev/null +++ b/docs/src/templates/partials/languages/lt.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "lt", + "action.edit": "Redaguoti šį puslapį", + "action.skip": "Pereiti prie turinio", + "action.view": "Žiūrėti puslapio šaltinius", + "announce.dismiss": "Daugiau neberodyti", + "blog.archive": "Archyvas", + "blog.categories": "Kategorijos", + "blog.categories.in": "į", + "blog.continue": "Skaityti toliau", + "blog.draft": "Ruošinys", + "blog.index": "Grįžti į indeksą", + "blog.meta": "Meta duomenys", + "blog.references": "Susieja saitai", + "clipboard.copy": "Kopijuoti į iškarpinę", + "clipboard.copied": "Nukopijuota į iškarpinę", + "consent.accept": "Sutikti", + "consent.manage": "Redaguoti nustatymus", + "consent.reject": "Atmesti", + "footer": "Poraštė", + "footer.next": "Sekantis", + "footer.previous": "Ankstesnis", + "header": "Antraštė", + "meta.comments": "Komentarai", + "meta.source": "Išeitinis kodas", + "nav": "Navigacija", + "readtime.one": "1 min skaitymo", + "readtime.other": "# min skaitymo", + "rss.created": "RSS šaltinis", + "rss.updated": "RSS šaltinis atnaujinimams", + "search": "Paieška", + "search.config.pipeline": " ", + "search.placeholder": "Paieška", + "search.share": "Dalintis", + "search.reset": "Išvalyti", + "search.result.initializer": "Paieškos inicijavimas", + "search.result.placeholder": "Įveskite norėdami pradėti paiešką", + "search.result.none": "Atitinkančių dokumentų nerasta", + "search.result.one": "1 atitinkantis dokumentas", + "search.result.other": "# atitinkantys dokumentai", + "search.result.more.one": "Dar 1 šiame puslapyje", + "search.result.more.other": "Dar # šiame puslapyje", + "search.result.term.missing": "Nerasta", + "select.language": "Pasirinkti kalbą", + "select.version": "Pasrinkti versiją", + "source": "Eiti į saugyklą", + "source.file.contributors": "Dalininkai", + "source.file.date.created": "Sukurta", + "source.file.date.updated": "Paskutinis atnaujinimas", + "tabs": "Skirtukai", + "toc": "Turinys", + "top": "Grįžti į viršų" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/lv.html b/docs/src/templates/partials/languages/lv.html new file mode 100644 index 00000000..7bcd9ace --- /dev/null +++ b/docs/src/templates/partials/languages/lv.html @@ -0,0 +1,55 @@ + + + +{% macro t(key) %}{{ { + "language": "lv", + "action.edit": "Rediģēt šo lapu", + "action.skip": "Pāriet uz saturu", + "clipboard.copy": "Kopēt starpliktuvē", + "clipboard.copied": "Kopēts starpliktuvē", + "footer": "Kājene", + "footer.next": "Nākamais", + "footer.previous": "Iepriekšējais", + "header": "Galvene", + "meta.comments": "Komentārs", + "meta.source": "Avots", + "nav": "Navigācija", + "search.placeholder": "Meklēt", + "search.reset": "Notīrīt", + "search.result.initializer": "Notiek meklēšanas inicializācija", + "search.result.placeholder": "Ierakstiet, lai sāktu meklēšanu", + "search.result.none": "Nav atbilstošu dokumentu", + "search.result.one": "1 atbilstošs dokuments", + "search.result.other": "# atbilstoši dokumenti ", + "search.result.more.one": "1 šajā lapā", + "search.result.more.other": "# un vairāk šajā lapā", + "search.result.term.missing": "Trūkstošs", + "select.language": "Izvēlies valodu", + "select.version": "Izvēlies versiju", + "source": "Doties uz repozitoriju", + "source.file.date.created": "Izveidots", + "source.file.date.updated": "Pēdējoreiz atjaunots", + "tabs": "Cilnes", + "toc": "Satura rādītājs", + "top": "Atpakaļ uz augšu" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/mk.html b/docs/src/templates/partials/languages/mk.html new file mode 100644 index 00000000..e3dc114c --- /dev/null +++ b/docs/src/templates/partials/languages/mk.html @@ -0,0 +1,56 @@ + + + +{% macro t(key) %}{{ { + "language": "mk", + "action.edit": "Уредете ја оваа страница", + "action.skip": "Прескокнете до содржината", + "clipboard.copy": "Копирај во таблата", + "clipboard.copied": "Копирано", + "footer": "Подножје", + "footer.next": "Следно", + "footer.previous": "Претходно", + "header": "Заглавје", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav": "Наслов за навигација", + "search.config.lang": "ru", + "search.placeholder": "Пребарување", + "search.reset": "Чисти", + "search.result.initializer": "Иницијализирање на пребарувањето", + "search.result.placeholder": "Напишете за да започнете со пребарување", + "search.result.none": "Нема соодветни документи", + "search.result.one": "1 документ што се совпаѓа", + "search.result.other": "# соодветни документи", + "search.result.more.one": "Уште 1 на оваа страница", + "search.result.more.other": "Уште # на оваа страница", + "search.result.term.missing": "Недостасува", + "select.language": "Изберете јазик", + "select.version": "Изберете верзија", + "source": "Одете до складиштето", + "source.file.date.created": "Создаден", + "source.file.date.updated": "Последно ажурирање", + "tabs": "Јазичиња", + "toc": "Содржина", + "top": "Вратете се на почетокот" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/mn.html b/docs/src/templates/partials/languages/mn.html new file mode 100644 index 00000000..de9002ab --- /dev/null +++ b/docs/src/templates/partials/languages/mn.html @@ -0,0 +1,51 @@ + + + +{% macro t(key) %}{{ { + "language": "mn", + "action.edit": "Хуудас засварлах", + "action.skip": "Агуулгыг алгасах", + "clipboard.copy": "Хуулах", + "clipboard.copied": "Санах ойд хуулах", + "footer": "Хөл", + "footer.next": "Дараах", + "footer.previous": "Өмнөх", + "header": "Толгой", + "meta.comments": "Сэтгэгдэл", + "meta.source": "Эх үүсвэр", + "nav": "Чиглүүлэгч", + "search.config.lang": "ru", + "search.placeholder": "Хайлт", + "search.reset": "Цэвэрлэх", + "search.result.placeholder": "Хайлтын үгээ бичнэ үү", + "search.result.none": "Таарц илэрсэнгүй", + "search.result.one": "1 таарц илэрлээ", + "search.result.other": "# Тохирох баримт бичиг", + "search.result.more.one": "1 илүү хуудас байна", + "search.result.more.other": "# илүү хуудас байна", + "source": "Хадгалах сан руу очих", + "source.file.date.created": "Үүсгэсэн", + "source.file.date.updated": "Сүүлийн шинэчлэлт", + "tabs": "Табууд", + "toc": "Агуулга" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ms.html b/docs/src/templates/partials/languages/ms.html new file mode 100644 index 00000000..57b70fc7 --- /dev/null +++ b/docs/src/templates/partials/languages/ms.html @@ -0,0 +1,55 @@ + + + +{% macro t(key) %}{{ { + "language": "ms", + "action.edit": "Edit halaman ini", + "action.skip": "Langkau tajuk talian", + "clipboard.copy": "Salin ke papan keratan", + "clipboard.copied": "Disalin ke papan keratan", + "footer": "Pengaki", + "footer.next" : "Seterusnya", + "footer.previous": "Sebelumnya", + "header": "Pengepala", + "meta.comments": "Komen", + "meta.source": "Sumber", + "nav": "Navigasi", + "search.placeholder": "Cari", + "search.reset": "Padam", + "search.result.initializer": "Siap carian", + "search.result.placeholder": "Taip untuk mula mencari", + "search.result.none": "Tiada dokumen yang sepadan", + "search.result.one": "1 dokumen yang sepadan", + "search.result.other": "# dokumen yang sepadan", + "search.result.more.one": "1 lagi di halaman ini", + "search.result.more.other": "# lagi di halaman ini", + "search.result.term.missing": "Hilang", + "select.language": "Pilih bahasa", + "select.version": "Pilih versi", + "source": "tajuk talian asal", + "source.file.date.created": "tarikh fil asal dicipta", + "source.file.date.updated": "Tarikh fil dikemas kini", + "tabs": "Tab", + "toc": "Jadual kandungan", + "top": "Kembali ke atas" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/my.html b/docs/src/templates/partials/languages/my.html new file mode 100644 index 00000000..27ca3ad9 --- /dev/null +++ b/docs/src/templates/partials/languages/my.html @@ -0,0 +1,49 @@ + + + +{% macro t(key) %}{{ { + "language": "my", + "action.edit": "ဤ စာမျက်နှာကို ပြင်ရန်", + "action.skip": "မာတိကာ သို့ သွားရန်", + "clipboard.copy": "ကလစ်ဘုတ် သို့ ကူးယူရန်", + "clipboard.copied": "ကလစ်ဘုတ် သို့ ကူယူပြီး", + "footer": "အောက်ခြေ", + "footer.next": "ရှေ့သို့", + "footer.previous": "နောက်သို့", + "header": "ခေါင်းပိုင်း", + "meta.comments": "မှတ်ချက်များ", + "meta.source": "ရင်းမြစ်", + "nav": "လမ်းညွှန်", + "search.config.pipeline": " ", + "search.placeholder": "ရှာရန်", + "search.reset": "ရှင်းလင်း", + "search.result.placeholder": "ရှာဖွေခြင်းစရန် စာရိုက်ပါ", + "search.result.none": "တူညီသော စာရွက်စာတမ်းများ မရှိပါ", + "search.result.one": "စာရွက်စာတမ်း ၁ ခု တူညီသည်", + "search.result.other": "စာရွက်စာတမ်း # ခု တူညီသည်", + "source": "repository သို့ သွားရန်", + "source.file.date.created": "နေပြည်တော်", + "source.file.date.updated": "နောက်ဆုံး ထုတ်ပြန်ချက်", + "tabs": "တက်များ", + "toc": "ပါဝင်အကြောင်းအရာများ" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/nb.html b/docs/src/templates/partials/languages/nb.html new file mode 100644 index 00000000..6be63531 --- /dev/null +++ b/docs/src/templates/partials/languages/nb.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "nb", + "action.edit": "Rediger denne siden", + "action.skip": "Gå til innhold", + "action.view": "Vis kildekoden til denne siden", + "announce.dismiss": "Ikke vis dette igjen", + "blog.archive": "Arkiv", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Fortsett å lese", + "blog.draft": "Kladd", + "blog.index": "Tilbake til oversikt", + "blog.meta": "Metadata", + "blog.references": "Relaterte lenker", + "clipboard.copy": "Kopier til utklippstavlen", + "clipboard.copied": "Kopiert til utklippstavlen", + "consent.accept": "Akseptert", + "consent.manage": "Innstillinger", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Neste", + "footer.previous": "Forrige", + "header": "Header", + "meta.comments": "Kommentarer", + "meta.source": "Kilde", + "nav": "Navigasjon", + "readtime.one": "lesteid: 1 min", + "readtime.other": "lesetid: # min", + "rss.created": "RSS feed", + "rss.updated": "Oppdatert RSS feed", + "search": "Søk", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.share": "Del", + "search.reset": "Nullstill", + "search.result.initializer": "Starter søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "search.result.more.one": "1 til på denne siden", + "search.result.more.other": "# flere på denne siden", + "search.result.term.missing": "Mangler", + "select.language": "Velg språk", + "select.version": "Velg versjon", + "source": "Gå til kilde", + "source.file.contributors": "Bidragsytere", + "source.file.date.created": "Opprettet", + "source.file.date.updated": "Sist oppdatert", + "tabs": "Faner", + "toc": "Innholdsliste", + "top": "Tilbake til toppen" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/nl.html b/docs/src/templates/partials/languages/nl.html new file mode 100644 index 00000000..0000fe60 --- /dev/null +++ b/docs/src/templates/partials/languages/nl.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "nl", + "action.edit": "Wijzig deze pagina", + "action.skip": "Ga naar inhoud", + "action.view": "Bron van deze pagina bekijken", + "announce.dismiss": "Niet meer laten zien", + "blog.archive": "Archief", + "blog.categories": "Categorieën", + "blog.categories.in": "in", + "blog.continue": "Doorgaan met lezen", + "blog.draft": "Concept", + "blog.index": "Terug naar de inhoudsopgave", + "blog.meta": "Metadata", + "blog.references": "Gerelateerde links", + "clipboard.copy": "Kopiëren naar klembord", + "clipboard.copied": "Gekopieerd naar klembord", + "consent.accept": "Accepteren", + "consent.manage": "Instellingen", + "consent.reject": "Afwijzen", + "footer": "Footer", + "footer.next": "Volgende", + "footer.previous": "Vorige", + "header": "Header", + "meta.comments": "Reacties", + "meta.source": "Bron", + "nav": "Navigatie", + "readtime.one": "1 min leestijd", + "readtime.other": "# min leestijd", + "rss.created": "RSS feed", + "rss.updated": "RSS feed met geüpdatet inhoud", + "search": "Zoeken", + "search.config.lang": "nl", + "search.placeholder": "Zoeken", + "search.share": "Delen", + "search.reset": "Leegmaken", + "search.result.initializer": "Zoeken initialiseren", + "search.result.placeholder": "Typ om te beginnen met zoeken", + "search.result.none": "Geen overeenkomende documenten", + "search.result.one": "1 overeenkomende document", + "search.result.other": "# overeenkomende documenten", + "search.result.more.one": "1 extra overeenkomst op deze pagina", + "search.result.more.other": "# extra overeenkomsten op deze pagina", + "search.result.term.missing": "Ontbreekt", + "select.language": "Selecteer taal", + "select.version": "Selecteer versie", + "source": "Ga naar repository", + "source.file.contributors": "Bijdragers", + "source.file.date.created": "Gecreëerd", + "source.file.date.updated": "Laatst geüpdatet", + "tabs": "Tabs", + "toc": "Inhoudsopgave", + "top": "Terug naar boven" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/nn.html b/docs/src/templates/partials/languages/nn.html new file mode 100644 index 00000000..9478bdbe --- /dev/null +++ b/docs/src/templates/partials/languages/nn.html @@ -0,0 +1,62 @@ + + + +{% macro t(key) %}{{ { + "language": "nn", + "action.edit": "Rediger denne sida", + "action.skip": "Gå til innhald", + "announce.dismiss": "Ikkje vis dette att", + "clipboard.copy": "Kopier til utklippstavla", + "clipboard.copied": "Kopiert til utklippstavla", + "consent.accept": "Akseptert", + "consent.manage": "Innstillinger", + "consent.reject": "Reject", + "footer": "Footer", + "footer.next": "Neste", + "footer.previous": "Førre", + "header": "Header", + "meta.comments": "Kommentarar", + "meta.source": "Kjelde", + "nav": "Navigasjon", + "search": "Søk", + "search.config.lang": "no", + "search.placeholder": "Søk", + "search.share": "Del", + "search.reset": "Nullstill", + "search.result.initializer": "Startar søk", + "search.result.placeholder": "Skriv søkeord", + "search.result.none": "Ingen treff", + "search.result.one": "1 treff", + "search.result.other": "# treff", + "search.result.more.one": "1 til på denne sida", + "search.result.more.other": "# fleire på denne sida", + "search.result.term.missing": "Manglar", + "select.language": "Vel språk", + "select.version": "Vel versjon", + "source": "Gå til kjelde", + "source.file.date.created": "Oppretta", + "source.file.date.updated": "Sist oppdatert", + "tabs": "Faner", + "toc": "Innhaldsliste", + "top": "Tilbake til toppen" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/pl.html b/docs/src/templates/partials/languages/pl.html new file mode 100644 index 00000000..7817633a --- /dev/null +++ b/docs/src/templates/partials/languages/pl.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "pl", + "action.edit": "Edytuj tę stronę", + "action.skip": "Przejdź do treści", + "action.view": "Zobacz kod źródłowy tej strony", + "announce.dismiss": "Nie pokazuj tego ponownie", + "blog.archive": "Archiwum", + "blog.categories": "Kategorie", + "blog.categories.in": "", + "blog.continue": "Czytaj dalej", + "blog.draft": "Wersja robocza", + "blog.index": "Powrót do indeksu", + "blog.meta": "Metadane", + "blog.references": "Powiązane łącza", + "clipboard.copy": "Kopiuj do schowka", + "clipboard.copied": "Skopiowano do schowka", + "consent.accept": "Akceptuj", + "consent.manage": "Zarządzaj ustawieniami", + "consent.reject": "Odrzuć", + "footer": "Stopka", + "footer.next": "Następna strona", + "footer.previous": "Poprzednia strona", + "header": "Nagłówek", + "meta.comments": "Komentarze", + "meta.source": "Kod źródłowy", + "nav": "Nawigacja", + "readtime.one": "Czas czytania: 1 min", + "readtime.other": "Czas czytania: # min", + "rss.created": "Kanał RSS", + "rss.updated": "Kanał RSS zaktualizowanych treści", + "search": "Szukaj", + "search.config.pipeline": " ", + "search.placeholder": "Szukaj", + "search.share": "Udostępnij", + "search.reset": "Wyczyść", + "search.result.initializer": "Inicjowanie wyszukiwania", + "search.result.placeholder": "Zacznij pisać, aby szukać", + "search.result.none": "Brak wyników wyszukiwania", + "search.result.one": "Wyniki wyszukiwania: 1", + "search.result.other": "Wyniki wyszukiwania: #", + "search.result.more.one": "1 więcej na tej stronie", + "search.result.more.other": "# więcej na tej stronie", + "search.result.term.missing": "Brak", + "select.language": "Wybierz język", + "select.version": "Wybierz wersję", + "source": "Przejdź do repozytorium", + "source.file.contributors": "Kontrybutorzy", + "source.file.date.created": "Utworzony", + "source.file.date.updated": "Ostatnia aktualizacja", + "tabs": "Zakładki", + "toc": "Spis treści", + "top": "Powrót do góry" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/pt-BR.html b/docs/src/templates/partials/languages/pt-BR.html new file mode 100644 index 00000000..d934a9ac --- /dev/null +++ b/docs/src/templates/partials/languages/pt-BR.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "pt", + "action.edit": "Editar esta página", + "action.skip": "Pular para conteúdo", + "action.view": "Exibir fonte desta página", + "announce.dismiss": "Não mostrar isso novamente", + "blog.archive": "Arquivo", + "blog.categories": "Categorias", + "blog.categories.in": "em", + "blog.continue": "Continuar leitura", + "blog.draft": "Rascunho", + "blog.index": "Voltar ao índice", + "blog.meta": "Metadados", + "blog.references": "Links relacionados", + "clipboard.copy": "Copiar para área de transferência", + "clipboard.copied": "Copiado para área de transferência", + "consent.accept": "Aceitar", + "consent.manage": "Gerenciar configurações", + "consent.reject": "Rejeitar", + "footer": "Rodapé", + "footer.next": "Próximo", + "footer.previous": "Anterior", + "header": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Origem", + "nav": "Navegação", + "readtime.one": "1 min de leitura", + "readtime.other": "# min de leitura", + "rss.created": "RSS feed", + "rss.updated": "RSS feed de conteúdo atualizado", + "search": "Pesquisar", + "search.config.lang": "pt", + "search.placeholder": "Buscar", + "search.share": "Compartilhar", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando busca", + "search.result.placeholder": "Digite para iniciar a busca", + "search.result.none": "Nenhum documento encontrado", + "search.result.one": "1 documento encontrado", + "search.result.other": "# documentos encontrados", + "search.result.more.one": "mais 1 nesta página", + "search.result.more.other": "# mais nesta página", + "search.result.term.missing": "Ausente", + "select.language": "Selecione o idioma", + "select.version": "Selecione a versão", + "source": "Ir para repositório", + "source.file.contributors": "Contribuidores", + "source.file.date.created": "Criado em", + "source.file.date.updated": "Última atualização", + "tabs": "Abas", + "toc": "Índice", + "top": "Voltar para o topo" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/pt.html b/docs/src/templates/partials/languages/pt.html new file mode 100644 index 00000000..e5dee1cb --- /dev/null +++ b/docs/src/templates/partials/languages/pt.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "pt", + "action.edit": "Editar esta página", + "action.skip": "Ir para o conteúdo", + "action.view": "Ver fonte desta página", + "announce.dismiss": "Não mostrar novamente", + "blog.archive": "Arquivo", + "blog.categories": "Categorias", + "blog.categories.in": "em", + "blog.continue": "Continuar leitura", + "blog.draft": "Rascunho", + "blog.index": "Voltar ao índice", + "blog.meta": "Metadados", + "blog.references": "Ligações relacionadas", + "clipboard.copy": "Copiar para área de transferência", + "clipboard.copied": "Copiado para área de transferência", + "consent.accept": "Aceitar", + "consent.manage": "Gerir configurações", + "consent.reject": "Rejeitar", + "footer": "Rodapé", + "footer.next": "Próximo", + "footer.previous": "Anterior", + "header": "Cabeçalho", + "meta.comments": "Comentários", + "meta.source": "Fonte", + "nav": "Navegação", + "readtime.one": "1 min de leitura", + "readtime.other": "# min de leitura", + "rss.created": "canal RSS", + "rss.updated": "canal RSS com conteúdo atualizado", + "search": "Pesquisar", + "search.config.lang": "pt", + "search.placeholder": "Buscar", + "search.share": "Compartilhar", + "search.reset": "Limpar", + "search.result.initializer": "Inicializando a pesquisa", + "search.result.placeholder": "Digite para iniciar a busca", + "search.result.none": "Nenhum resultado encontrado", + "search.result.one": "1 resultado encontrado", + "search.result.other": "# resultados encontrados", + "search.result.more.one": "Mais 1 nesta página", + "search.result.more.other": "Mais # nesta página", + "search.result.term.missing": "Ausente", + "select.language": "Selecione o idioma", + "select.version": "Selecione a versão", + "source": "Ir ao repositório", + "source.file.contributors": "Colaboradores", + "source.file.date.created": "Criada", + "source.file.date.updated": "Última atualização", + "tabs": "Abas", + "toc": "Índice", + "top": "Voltar ao topo" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ro.html b/docs/src/templates/partials/languages/ro.html new file mode 100644 index 00000000..7bea9afb --- /dev/null +++ b/docs/src/templates/partials/languages/ro.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "ro", + "action.edit": "Editeaza această pagină", + "action.skip": "Sari la conținut", + "action.view": "Vezi sursa acestei pagini", + "announce.dismiss": "Nu mai arăta asta", + "blog.archive": "Arhivează", + "blog.categories": "Categorii", + "blog.categories.in": "în", + "blog.continue": "Continuă să citești", + "blog.draft": "Ciornă", + "blog.index": "Înapoi la index", + "blog.meta": "Metadata", + "blog.references": "Link-uri relevante", + "clipboard.copy": "Copiază în clipboard", + "clipboard.copied": "Copiat în clipboard", + "consent.accept": "Accept", + "consent.manage": "Gestionați setările", + "consent.reject": "Refuz", + "footer": "Subsol", + "footer.next": "Următor", + "footer.previous": "Anterior", + "header": "Antet", + "meta.comments": "Comentarii", + "meta.source": "Sursă", + "nav": "Navigație", + "readtime.one": "1 minut de citit", + "readtime.other": "# minut de citit", + "rss.created": "Flux RSS", + "rss.updated": "Flux RSS cu conținut actualizat", + "search": "Caută", + "search.config.lang": "ro", + "search.placeholder": "Căutare", + "search.share": "Distribuie", + "search.reset": "Resetează", + "search.result.initializer": "Inițializare căutare", + "search.result.placeholder": "Tastează pentru a începe căutarea", + "search.result.none": "Nu a fost găsit niciun document", + "search.result.one": "1 document găsit", + "search.result.other": "# documente găsite", + "search.result.more.one": "Încă 1 pe această pagină", + "search.result.more.other": "Încă # pe această pagină", + "search.result.term.missing": "Lipsă", + "select.language": "Selectează limba", + "select.version": "Selectează versuine", + "source": "Accesează repository-ul", + "source.file.contributors": "Contribuitori", + "source.file.date.created": "Creată", + "source.file.date.updated": "Ultima actualizare", + "tabs": "File", + "toc": "Cuprins", + "top": "Înapoi sus" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ru.html b/docs/src/templates/partials/languages/ru.html new file mode 100644 index 00000000..ddbd7b95 --- /dev/null +++ b/docs/src/templates/partials/languages/ru.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "ru", + "action.edit": "Редактировать страницу", + "action.skip": "Перейти к содержанию", + "action.view": "Посмотреть исходный код страницы", + "announce.dismiss": "Больше не показывать", + "blog.archive": "Архив", + "blog.categories": "Категории", + "blog.categories.in": "В", + "blog.continue": "Читать", + "blog.draft": "Черновик", + "blog.index": "На главную", + "blog.meta": "Метаданные", + "blog.references": "Ссылки", + "clipboard.copy": "Копировать в буфер", + "clipboard.copied": "Скопировано в буфер", + "consent.accept": "Принять", + "consent.manage": "Управлять настройками", + "consent.reject": "Отклонить", + "footer": "Нижний колонтитул", + "footer.next": "Вперед", + "footer.previous": "Назад", + "header": "Верхний колонтитул", + "meta.comments": "Комментарии", + "meta.source": "Исходный код", + "nav": "Навигация", + "readtime.one": "Читать 1 минуту", + "readtime.other": "Читать # минут", + "rss.created": "RSS канал", + "rss.updated": "RSS канал с новым контентом", + "search": "Поиск", + "search.config.lang": "ru", + "search.placeholder": "Поиск", + "search.share": "Поделиться", + "search.reset": "Очистить", + "search.result.initializer": "Инициализация поиска", + "search.result.placeholder": "Начните печатать для поиска", + "search.result.none": "Совпадений не найдено", + "search.result.one": "Найдено 1 совпадение", + "search.result.other": "Найдено совпадений: #", + "search.result.more.one": "Ещё 1 на этой странице", + "search.result.more.other": "Ещё # на этой странице", + "search.result.term.missing": "Отсутствует", + "select.language": "Выберите язык", + "select.version": "Выберите версию", + "source": "Перейти к репозиторию", + "source.file.contributors": "Участники", + "source.file.date.created": "Дата создания", + "source.file.date.updated": "Последнее обновление", + "tabs": "Вкладки", + "toc": "Содержание", + "top": "К началу" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sa.html b/docs/src/templates/partials/languages/sa.html new file mode 100644 index 00000000..338e2b61 --- /dev/null +++ b/docs/src/templates/partials/languages/sa.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "sa", + "action.edit": "एतत् पृष्ठं सम्पादयतु", + "action.skip": "सामग्रीं त्यजन्तु", + "action.view": "अस्य पृष्ठस्य स्रोतः पश्यन्तु", + "announce.dismiss": "एतत् पुनः न दर्शयतु", + "blog.archive": "लेखागार", + "blog.categories": "श्रेणियाँ", + "blog.categories.in": "इत्यस्मिन्‌", + "blog.continue": "पठनं निरन्तरं कुर्वन्तु", + "blog.draft": "प्रारूप", + "blog.index": "अनुक्रमणिकां प्रति पुनः आगच्छन्तु", + "blog.meta": "परिदत्तांश", + "blog.references": "सन्दर्भाः", + "clipboard.copy": "एतत् प्रतिलिख्यताम्", + "clipboard.copied": "प्रतिलिपितः भवति", + "consent.accept": "अहं तत् स्वीकुर्वन् अस्मि", + "consent.manage": "वविन्यासं प्रबन्धयन्तु", + "consent.reject": "अहं तत् निराकरोमि", + "footer": "पादलेखः", + "footer.next": "अग्रिमः", + "footer.previous": "पूर्वकृत", + "header": "शीर्षकम्", + "meta.comments": "विचाराः", + "meta.source": "स्रोतः", + "nav": "मार्गदर्शनम्", + "readtime.one": "१ निमेषं पठितुं", + "readtime.other": "# निमेषं पठितुं", + "rss.created": "आरएसएस सेवा", + "rss.updated": "आरएसएस सेवातः नवीनतमं अद्यतनम्", + "search": "अन्वेषण", + "search.placeholder": "अन्वेषण", + "search.share": "भजतु", + "search.reset": "तत् स्वच्छं कुर्वन्तु", + "search.result.initializer": "अन्वेषणस्य आरम्भः", + "search.result.placeholder": "अन्वेषणं आरभ्य लिखन्तु", + "search.result.none": "अभिलेखाः नास्ति", + "search.result.one": "१ अभिलेखः अस्ति", + "search.result.other": "# अभिलेखाः सन्ति", + "search.result.more.one": "अस्मिन् पृष्ठे १ अन्यः अस्ति", + "search.result.more.other": "अस्मिन् पृष्ठे # अन्ये सन्ति", + "search.result.term.missing": "शून्य", + "select.language": "भाषां चिनोतु", + "select.version": "संस्करणं चिनोतु", + "source": "भण्डारं गच्छन्तु", + "source.file.contributors": "अंशदाता", + "source.file.date.created": "ननिर्माणस्य तिथिः", + "source.file.date.updated": "परिवर्तनस्य तिथिः", + "tabs": "पट्टाः", + "toc": "सामग्रीसारणी", + "top": "पुनः उपरिभागं प्रति गच्छन्तु" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sh.html b/docs/src/templates/partials/languages/sh.html new file mode 100644 index 00000000..42a0d902 --- /dev/null +++ b/docs/src/templates/partials/languages/sh.html @@ -0,0 +1,70 @@ + + + +{% macro t(key) %}{{ { + "language": "sh", + "action.edit": "Ažuriraj stranicu", + "action.skip": "Idi na tekst", + "action.view": "Pogledaj izvorni kod ove stranice", + "announce.dismiss": "Nemoj mi ponovo pokazati ovo", + "blog.archive": "Arhiva", + "blog.categories": "Kategorije", + "blog.categories.in": "u", + "blog.continue": "Nastavi sa čitanjem", + "blog.meta": "Metapodaci", + "blog.references": "Povezani linkovi", + "clipboard.copy": "Kopiraj u klipbord", + "clipboard.copied": "Iskopirano u klipbord", + "consent.accept": "Prihvati", + "consent.manage": "Promeni podešavanja", + "consent.reject": "Odbij", + "footer": "Podnožje", + "footer.next": "Sledeće", + "footer.previous": "Prethodno", + "header": "Zaglavlje", + "meta.comments": "Komentari", + "meta.source": "Izvor", + "nav": "Navigacija", + "readtime.one": "1 minut čitanja", + "readtime.other": "# minuta čitanja", + "search": "Pretraga", + "search.placeholder": "Pretraga", + "search.share": "Deljenje", + "search.reset": "Očisti", + "search.result.initializer": "Inicijalizujem pretragu", + "search.result.placeholder": "Unesite pojam pretrage", + "search.result.none": "Ništa nije pronađeno", + "search.result.one": "1 rezultat pretrage", + "search.result.other": "# rezultata pretrage", + "search.result.more.one": "još 1 na ovoj strani", + "search.result.more.other": "još # na ovoj strani", + "search.result.term.missing": "Nedostaje", + "select.language": "Izaberi jezik", + "select.version": "Izaberi verziju", + "source": "Idi u repozitorijum", + "source.file.date.created": "Kreiran", + "source.file.date.updated": "Ažuriran", + "tabs": "Tabovi", + "toc": "Sadržaj", + "top": "Nazad na vrh" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/si.html b/docs/src/templates/partials/languages/si.html new file mode 100644 index 00000000..eb41309e --- /dev/null +++ b/docs/src/templates/partials/languages/si.html @@ -0,0 +1,51 @@ + + + +{% macro t(key) %}{{ { + "language": "si", + "action.edit": "පිටුව සංස්කරණය", + "action.skip": "අන්තර්ගතය වෙත යන්න", + "clipboard.copy": "කොපි කරන්න", + "clipboard.copied": "කොපි කළා", + "footer": "පාදම", + "footer.next": "මීළඟ", + "footer.previous": "පසුගිය", + "header": "ශීර්ෂය", + "meta.comments": "ප්‍රතිචාර", + "meta.source": "මූලාශ්‍රය", + "nav": "යාත්‍රණය", + "search.config.pipeline": " ", + "search.placeholder": "සොයන්න", + "search.reset": "මකන්න", + "search.result.placeholder": "සෙවීමට ටයිප් කරන්න", + "search.result.none": "කිසිවක් හමු නොවුණි", + "search.result.one": "1 ගැලපෙන ගොනුවක්", + "search.result.other": "ගැලපෙන ගොනු # ක්", + "search.result.more.one": "තව 1 ප්‍රතිඵලයක්", + "search.result.more.other": "තව ප්‍රතිඵල # ක්", + "source": "රිපොසිටරියට යන්න", + "source.file.date.created": "ٺاھيو ويو", + "source.file.date.updated": "අවසන් යාවත්කාලීන වීම", + "tabs": "ටැබ්ස්", + "toc": "පටුන" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sk.html b/docs/src/templates/partials/languages/sk.html new file mode 100644 index 00000000..701a5a53 --- /dev/null +++ b/docs/src/templates/partials/languages/sk.html @@ -0,0 +1,43 @@ + + + +{% macro t(key) %}{{ { + "language": "sk", + "action.edit": "Upraviť túto stránku", + "action.skip": "Preskočiť na obsah", + "clipboard.copy": "Kopírovať do schránky", + "clipboard.copied": "Skopírované do schránky", + "footer.next": "Ďalej", + "footer.previous": "Späť", + "meta.comments": "Komentáre", + "meta.source": "Zdroj", + "search.placeholder": "Hľadať", + "search.result.placeholder": "Pre vyhľadávanie začni písať", + "search.result.none": "Žiadne vyhovujúce dokumenty", + "search.result.one": "Vyhovujúci dokument: 1", + "search.result.other": "Vyhovujúce dokumenty: #", + "source": "Zobraziť repozitár", + "source.file.date.created": "Vytvorené", + "source.file.date.updated": "Posledná aktualizácia", + "toc": "Obsah" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sl.html b/docs/src/templates/partials/languages/sl.html new file mode 100644 index 00000000..a01f31d9 --- /dev/null +++ b/docs/src/templates/partials/languages/sl.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "sl", + "action.edit": "Uredi stran", + "action.skip": "Skoči na vsebino", + "action.view": "Prikaži izvorno stran", + "announce.dismiss": "Ne prikaži več", + "blog.archive": "Arhiv", + "blog.categories": "Kategorije", + "blog.categories.in": "v", + "blog.continue": "Nadaljuj z branjem", + "blog.draft": "Osnutek", + "blog.index": "Nazaj na kazalo", + "blog.meta": "Metapodatki", + "blog.references": "Sorodne povezave", + "clipboard.copy": "Kopiraj v odložišče", + "clipboard.copied": "Kopirano v odložišče", + "consent.accept": "Sprejmi", + "consent.manage": "Uredi nastavitve", + "consent.reject": "Zavrni", + "footer": "Glava", + "footer.next": "Naslednja stran", + "footer.previous": "Prejšnja stran", + "header": "Noga", + "meta.comments": "Komentarji", + "meta.source": "Izvorna koda", + "nav": "Navigacija", + "readtime.one": "Čas branja: 1 min", + "readtime.other": "Čas branja: # min", + "rss.created": "RSS vir", + "rss.updated": "RSS vir posodobljene vsebine", + "search": "Iskanje", + "search.config.lang": "sl", + "search.placeholder": "Išči", + "search.share": "Deli", + "search.reset": "Počisti", + "search.result.initializer": "Inicializacija iskanja", + "search.result.placeholder": "Vpiši iskalni niz", + "search.result.none": "Ni zadetkov", + "search.result.one": "1 zadetek", + "search.result.other": "# zadetkov", + "search.result.more.one": "Še 1 na tej strani", + "search.result.more.other": "Še # na tej strani", + "search.result.term.missing": "Manjka", + "select.language": "Izberi jezik", + "select.version": "Izberi različico", + "source": "Pojdi na repozitorij", + "source.file.contributors": "Soavtorji", + "source.file.date.created": "Ustvarjeno", + "source.file.date.updated": "Zadnja posodobitev", + "tabs": "Zavihki", + "toc": "Kazalo", + "top": "Nazaj na vrh" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sr.html b/docs/src/templates/partials/languages/sr.html new file mode 100644 index 00000000..275ea126 --- /dev/null +++ b/docs/src/templates/partials/languages/sr.html @@ -0,0 +1,57 @@ + + + +{% macro t(key) %}{{ { + "language": "sr", + "action.edit": "Ажурирај страницу", + "action.skip": "Иди на текст", + "clipboard.copy": "Копирај у клипборд", + "clipboard.copied": "Ископирано у клипборд", + "footer": "Подножје", + "footer.next": "Следеће", + "footer.previous": "Претходно", + "header": "Заглавље", + "meta.comments": "Коментари", + "meta.source": "Извор", + "nav": "Навигација", + "search": "Претрага", + "search.placeholder": "Претрага", + "search.share": "Дељење", + "search.reset": "Очисти", + "search.result.initializer": "Иницијализујем претрагу", + "search.result.placeholder": "Унесите појам претраге", + "search.result.none": "Ништа није пронађено", + "search.result.one": "1 резултат претраге", + "search.result.other": "# резултата претраге", + "search.result.more.one": "још 1 на овој страни", + "search.result.more.other": "још # на овој страни", + "search.result.term.missing": "Недостаје", + "select.language": "Изабери језик", + "select.version": "Изабери верзију", + "source": "Иди у репозиторијум", + "source.file.date.created": "Креиран", + "source.file.date.updated": "Ажуриран", + "tabs": "Табови", + "toc": "Садржај", + "top": "Назад на врх" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/sv.html b/docs/src/templates/partials/languages/sv.html new file mode 100644 index 00000000..52f151d2 --- /dev/null +++ b/docs/src/templates/partials/languages/sv.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "sv", + "action.edit": "Redigera sidan", + "action.skip": "Gå till innehållet", + "action.view": "Visa källkoden för denna sida", + "announce.dismiss": "Visa inte igen", + "blog.archive": "Arkivera", + "blog.categories": "Kategorier", + "blog.categories.in": "i", + "blog.continue": "Fortsätt läsa", + "blog.draft": "Utkast", + "blog.index": "Tillbaka till index", + "blog.meta": "Metadata", + "blog.references": "Relaterade länkar", + "clipboard.copy": "Kopiera till urklipp", + "clipboard.copied": "Kopierat till urklipp", + "consent.accept": "Acceptera", + "consent.manage": "Hantera inställningar", + "consent.reject": "Acceptera inte", + "footer": "Sidfot", + "footer.next": "Nästa", + "footer.previous": "Föregående", + "header": "Sidhuvud", + "meta.comments": "Kommentarer", + "meta.source": "Källa", + "nav": "Navigation", + "readtime.one": "1 min lästid", + "readtime.other": "# min lästid", + "rss.created": "RSS-flöde", + "rss.updated": "RSS-flöde av uppdaterat innehåll", + "search": "Sök", + "search.config.lang": "sv", + "search.placeholder": "Sök", + "search.share": "Dela", + "search.reset": "Rensa", + "search.result.initializer": "Initialiserar sök", + "search.result.placeholder": "Skriv sökord", + "search.result.none": "Inga sökresultat", + "search.result.one": "1 sökresultat", + "search.result.other": "# sökresultat", + "search.result.more.one": "1 till på denna sida", + "search.result.more.other": "# till på denna sida", + "search.result.term.missing": "Saknas", + "select.language": "Välj språk", + "select.version": "Välj version", + "source": "Gå till datakatalog", + "source.file.contributors": "Författare", + "source.file.date.created": "Skapad", + "source.file.date.updated": "Senast uppdaterad", + "tabs": "Flikar", + "toc": "Innehållsförteckning", + "top": "Tillbaka till toppen" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/te.html b/docs/src/templates/partials/languages/te.html new file mode 100644 index 00000000..7529a47c --- /dev/null +++ b/docs/src/templates/partials/languages/te.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "te", + "action.edit": "ఈ పేజీలో దిద్దుబాట్లు చేయండి", + "action.skip": "సమాచారానికి వెళ్లండి", + "action.view": "నేను ఈ పేజీ యొక్క మూలాన్ని చూడాలనుకుంటున్నాను", + "announce.dismiss": "దీన్ని మళ్లీ చూపవద్దు", + "blog.archive": "పాత వ్యాసం", + "blog.categories": "వర్గాలు", + "blog.categories.in": "లో", + "blog.continue": "చదవడం కొనసాగించండి", + "blog.draft": "ప్రారంభ రచన", + "blog.index": "సూచికకు తిరిగి వెళ్ళు", + "blog.meta": "సమాచారం గురించి సమాచారం", + "blog.references": "సంబంధిత సూచనలు", + "clipboard.copy": "దీనిని అనుకరించు", + "clipboard.copied": "దీనిని అతికించు", + "consent.accept": "నేను దీనిని అంగీకరిస్తున్నాను", + "consent.manage": "ఆకృతీకరణను నిర్వహించండి", + "consent.reject": "నేను దీనిని తిరస్కరిస్తున్నాను", + "footer": "అడిటిప్పణి", + "footer.next": "తదుపరి భాగం", + "footer.previous": "మునుపటి భాగం", + "header": "శీర్షిక విభాగం", + "meta.comments": "అభిప్రాయాలు", + "meta.source": "మూలం", + "nav": "మార్గదర్శక పట్టీ", + "readtime.one": "చదవడానికి ఒక నిమిషం పడుతుంది", + "readtime.other": "చదవడానికి # నిమిషాలు పడుతుంది", + "rss.created": "ఆర్ఎస్ఎస్ సేవ", + "rss.updated": "ఆర్ఎస్ఎస్ సేవ నుండి తాజా నవీకరణ", + "search": "వెతకండి", + "search.placeholder": "వెతకండి", + "search.share": "పంచుకోండి", + "search.reset": "తుడిచివేయు", + "search.result.initializer": "శోధనను ప్రారంభిస్తోంది", + "search.result.placeholder": "రాయడం ద్వారా వెతకడం ప్రారంభించండి", + "search.result.none": "సరిపోలే పత్రాలు లేవు", + "search.result.one": "ఒక సరిపోలే పత్రం", + "search.result.other": "# సరిపోలే పత్రాలు", + "search.result.more.one": "ఈ పేజీలో మరొకటి", + "search.result.more.other": "ఈ పేజీలో ఇంకా # ఉన్నాయి", + "search.result.term.missing": "తప్పిపోయింది", + "select.language": "భాషను ఎంచుకోండి", + "select.version": "సంస్కరణను ఎంచుకోండి", + "source": "భండారానికి వెళ్ళండి", + "source.file.contributors": "సహకారులు", + "source.file.date.created": "సృష్టించబడింది", + "source.file.date.updated": "చివరి నవీకరణ", + "tabs": "వివిధ కిటికీలు", + "toc": "విషయ సూచిక", + "top": "పైకి తిరిగి వెళ్ళు" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/th.html b/docs/src/templates/partials/languages/th.html new file mode 100644 index 00000000..c8104fc1 --- /dev/null +++ b/docs/src/templates/partials/languages/th.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "th", + "action.edit": "แก้ไขหน้านี้", + "action.skip": "ข้ามไปที่เนื้อหา", + "action.view": "ดูแหล่งที่มาของหน้านี้", + "announce.dismiss": "อย่าแสดงสิ่งนี้อีก", + "blog.archive": "คลังเก็บเอกสาร", + "blog.categories": "หมวดหมู่", + "blog.categories.in": "ใย", + "blog.continue": "อ่านต่อไป", + "blog.draft": "ฉบับร่าง", + "blog.index": "กลับไปยังหน้าแรก", + "blog.meta": "คำอธิบายข้อมูล", + "blog.references": "ลิงก์ที่เกี่ยวข้อง", + "clipboard.copy": "คัดลอก", + "clipboard.copied": "คัดลอกแล้ว", + "consent.accept": "ยอมรับ", + "consent.manage": "จัดการการตั้งค่า", + "consent.reject": "ปฏิเสธ", + "footer": "ส่วนท้าย", + "footer.next": "ต่อไป", + "footer.previous": "ก่อนหน้า", + "header": "หัวข้อ", + "meta.comments": "ความคิดเห็น", + "meta.source": "แหล่งที่มา", + "nav": "ตัวนำทาง", + "readtime.one": "อ่าน 1 นาที", + "readtime.other": "อ่าน # นาที", + "rss.created": "ฟีด RSS", + "rss.updated": "ฟีด RSS ของเนื้อหาที่อัปเดต", + "search": "ค้นหา", + "search.config.lang": "th", + "search.placeholder": "ค้นหา", + "search.share": "แบ่งปัน", + "search.reset": "ล้าง", + "search.result.initializer": "กำลังเริ่มต้นการค้นหา", + "search.result.placeholder": "พิมพ์เพื่อเริ่มค้นหา", + "search.result.none": "ไม่พบเอกสารที่ตรงกัน", + "search.result.one": "พบเอกสารที่ตรงกัน", + "search.result.other": "พบ # เอกสารที่ตรงกัน", + "search.result.more.one": "อีกหนึ่งในหน้านี้", + "search.result.more.other": "# เพิ่มเติมในหน้านี้", + "search.result.term.missing": "ไม่พบ", + "select.language": "เลือกภาษา", + "select.version": "เลือกเวอร์ชัน", + "source": "ไปที่พื้นที่เก็บข้อมูล", + "source.file.contributors": "ผู้มีส่วนร่วม", + "source.file.date.created": "สร้าง", + "source.file.date.updated": "สร้าง", + "tabs": "แท็บ", + "toc": "สารบัญ", + "top": "กลับไปด้านบนสุด" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/tl.html b/docs/src/templates/partials/languages/tl.html new file mode 100644 index 00000000..00c22c99 --- /dev/null +++ b/docs/src/templates/partials/languages/tl.html @@ -0,0 +1,57 @@ + + + +{% macro t(key) %}{{ { + "language": "tl", + "action.edit": "I-edit ang pahinang ito", + "action.skip": "I-skip tungo sa nilalaman", + "clipboard.copy": "Kopyahin sa clipboard", + "clipboard.copied": "Nakopya mula sa clipboard", + "footer": "Lagdang Pangwakas", + "footer.next": "Susunod", + "footer.previous": "Nakaraan", + "header": "Pamuhatan", + "meta.comments": "Mga Komento", + "meta.source": "Pinagmulan", + "nav": "Nabigasyon", + "search": "Hanapin", + "search.placeholder": "Hanapin", + "search.share": "Ibahagi", + "search.reset": "Tanggalin", + "search.result.initializer": "Sinisimulan ang paghahanap", + "search.result.placeholder": "Mag-type upang simulan ang paghahanap", + "search.result.none": "Walang nahanap na dokumento", + "search.result.one": "1 magkatugmang dokumento", + "search.result.other": "# magkatugmang mga dokumento", + "search.result.more.one": "1 meron sa pahina na ito", + "search.result.more.other": "# meron sa pahina na ito", + "search.result.term.missing": "Nawawala", + "select.language": "Pumili ng lenguwahe", + "select.version": "Pumili ng bersyon", + "source": "Pumunta sa repository", + "source.file.date.created": "Nagawa", + "source.file.date.updated": "Huling update", + "tabs": "Mga tala", + "toc": "Talaan ng nilalaman", + "top": "Bumalik sa taas" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/tr.html b/docs/src/templates/partials/languages/tr.html new file mode 100644 index 00000000..860f8ed7 --- /dev/null +++ b/docs/src/templates/partials/languages/tr.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "tr", + "action.edit": "Düzenle", + "action.skip": "Ana içeriğe geç", + "action.view": "Sayfanın kaynağını görüntüle", + "announce.dismiss": "Bir daha gösterme", + "blog.archive": "Arşiv", + "blog.categories": "Kategoriler", + "blog.categories.in": "in", + "blog.continue": "Okumaya devam et", + "blog.draft": "Taslak", + "blog.index": "Dizine geri dön", + "blog.meta": "Metadata", + "blog.references": "İlgili bağlantılar", + "clipboard.copy": "Kopyala", + "clipboard.copied": "Kopyalandı", + "consent.accept": "Kabul et", + "consent.manage": "Ayarları yönet", + "consent.reject": "Reddet", + "footer": "Altbilgi", + "footer.next": "Sonraki", + "footer.previous": "Önceki", + "header": "Başlık", + "meta.comments": "Yorumlar", + "meta.source": "Kaynak", + "nav": "Navigasyon", + "readtime.one": "1 dakika okuma", + "readtime.other": "# dakika okuma", + "rss.created": "RSS beslemesi", + "rss.updated": "Güncellenmiş içeriğin RSS beslemesi", + "search": "Ara", + "search.config.lang": "tr", + "search.placeholder": "Ara", + "search.share": "Paylaş", + "search.reset": "Temizle", + "search.result.initializer": "Arama başlatılıyor", + "search.result.placeholder": "Aramaya başlamak için yazın", + "search.result.none": "Eşleşen doküman bulunamadı", + "search.result.one": "1 doküman bulundu", + "search.result.other": "# doküman bulundu", + "search.result.more.one": "Bu sayfada 1 tane daha", + "search.result.more.other": "Bu sayfada # tane daha", + "search.result.term.missing": "Eksik", + "select.language": "Dil seç", + "select.version": "Versiyon seç", + "source": "Depoya git", + "source.file.contributors": "Katkıda bulunanlar", + "source.file.date.created": "Oluşturuldu", + "source.file.date.updated": "Son Güncelleme", + "tabs": "Sekmeler", + "toc": "İçindekiler", + "top": "Başa dön" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/uk.html b/docs/src/templates/partials/languages/uk.html new file mode 100644 index 00000000..ca5c709c --- /dev/null +++ b/docs/src/templates/partials/languages/uk.html @@ -0,0 +1,75 @@ + + + +{% macro t(key) %}{{ { + "language": "uk", + "action.edit": "Редагувати сторінку", + "action.skip": "Перейти до змісту", + "action.view": "Переглянути вихідний код сторінки", + "announce.dismiss": "Більше не показувати", + "blog.archive": "Архівувати", + "blog.categories": "Категорії", + "blog.categories.in": "в", + "blog.continue": "Читати далі", + "blog.draft": "Чернетка", + "blog.index": "Повернутись на головну", + "blog.meta": "Метадані", + "blog.references": "Пов'язані посилання", + "clipboard.copy": "Скопіювати в буфер", + "clipboard.copied": "Скопійовано в буфер", + "consent.accept": "Прийняти", + "consent.manage": "Керувати налаштуваннями", + "consent.reject": "Відхилити", + "footer": "Футер", + "footer.next": "Вперед", + "footer.previous": "Назад", + "header": "Хедер", + "meta.comments": "Коментарі", + "meta.source": "Вихідний код", + "nav": "Навігація", + "readtime.one": "Час на прочитання: 1 хвилина", + "readtime.other": "Час на прочитання: # хвилин", + "rss.created": "RSS стрічка", + "rss.updated": "RSS стрічка оновленого контенту", + "search": "Шукати", + "search.placeholder": "Пошук", + "search.share": "Поділитись", + "search.reset": "Очистити", + "search.result.initializer": "Пошук розпочато", + "search.result.placeholder": "Розпочніть писати для пошуку", + "search.result.none": "Збігів не знайдено", + "search.result.one": "Знайдено 1 збіг", + "search.result.other": "Знайдено # збігів", + "search.result.more.one": "Ще 1 збіг на цій сторінці", + "search.result.more.other": "Ще # збігів на цій сторінці", + "search.result.term.missing": "Не знайдено запиту", + "select.language": "Обрати мову", + "select.version": "Обрати версію", + "source": "Перейти до вихідного коду", + "source.file.contributors": "Контриб'ютори", + "source.file.date.created": "Створено", + "source.file.date.updated": "Востаннє оновлено", + "tabs": "Вкладки", + "toc": "Зміст", + "top": "Повернутись нагору" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/ur.html b/docs/src/templates/partials/languages/ur.html new file mode 100644 index 00000000..14a50588 --- /dev/null +++ b/docs/src/templates/partials/languages/ur.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "ur", + "direction": "rtl", + "action.edit": "اس صفحے میں ترمیم کریں", + "action.skip": "براہِ راست مواد پر جائیں", + "action.view": "اس صفحہ کا ماخذ دیکھیں", + "announce.dismiss": "اسے دوبارہ مت دکھائیں", + "blog.archive": "محفوظ شدہ", + "blog.categories": "اقسام", + "blog.categories.in": "میں", + "blog.continue": "پڑھنا جاری رکھیے", + "blog.draft": "ڈرافٹ", + "blog.index": "واپس انڈیکس پر جائیں", + "blog.meta": "میٹا ڈیٹا", + "blog.references": "متعلقہ لنکس", + "clipboard.copy": "کلِپ بورڈ میں نقل کریں", + "clipboard.copied": "کلِپ بورڈ میں نقل کر دیا گیا", + "consent.accept": "قبول کریں", + "consent.manage": "سیٹینگ بدلیں", + "consent.reject": "رد کرنا", + "footer": "ذیلی تحریر", + "footer.next": "اگلا", + "footer.previous": "پچھلا", + "header": "سر تحریر", + "meta.comments": "تبصرے", + "meta.source": "ذریعہ", + "nav": "رہنمائی", + "readtime.one": "1 منٹ لگے گا", + "readtime.other": "# منٹ لگیں گے", + "rss.created": "RSS فیڈ", + "rss.updated": "تازہ ترین مواد کی RSS فیڈ", + "search": "تلاش", + "search.config.pipeline": " ", + "search.placeholder": "تلاش کریں", + "search.share": "اشتراک کریں", + "search.reset": "صاف کریں", + "search.result.initializer": "تلاش کا آغاز ہو رہا ہے", + "search.result.placeholder": "تلاش شروع کرنے کے لئے ٹائپ کریں", + "search.result.none": "کوئی ملتی جلتی دستاویزات نہیں", + "search.result.one": "۱ ملتی جلتی دستاویز", + "search.result.other": "# ملتی جلتی دستاویزات", + "search.result.more.one": "اِس صفحے پر مزید ۱", + "search.result.more.other": "اِس صفحے پر مزید #", + "search.result.term.missing": "گمشدہ", + "select.language": "زبان کا انتخاب کریں", + "select.version": "ورژن کا انتخاب کریں", + "source": "ریپازٹری پر جائیں", + "source.file.contributors": "تعاون کار", + "source.file.date.created": "تخلیق", + "source.file.date.updated": "آخری بار تجدید", + "tabs": "ٹیبز", + "toc": "فہرست", + "top": "واپس اوپر جائیں" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/uz.html b/docs/src/templates/partials/languages/uz.html new file mode 100644 index 00000000..d86f4db2 --- /dev/null +++ b/docs/src/templates/partials/languages/uz.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "uz", + "action.edit": "Ushbu sahifani tahrirlash", + "action.skip": "Tarkibga o'tish", + "action.view": "Ushbu sahifaning manbasini ko'rish", + "announce.dismiss": "Buni boshqa ko'rsatma", + "blog.archive": "Arxiv", + "blog.categories": "Kategoriyalar", + "blog.categories.in": "ichida", + "blog.continue": "O'qishni davom ettiring", + "blog.draft": "Qoralama", + "blog.index": "Indeks sahifasiga qaytish", + "blog.meta": "Metama'lumot", + "blog.references": "Bog'liq havolalar", + "clipboard.copy": "Buferga nusxalash", + "clipboard.copied": "Buferga nusxalandi", + "consent.accept": "Qabul qilish", + "consent.manage": "Sozlamalarni boshqarish", + "consent.reject": "Rad etish", + "footer": "Pastgi qism", + "footer.next": "Keyingi sahifa", + "footer.previous": "Oldingi sahifa", + "header": "Sarlavha", + "meta.comments": "Izohlar", + "meta.source": "Manba", + "nav": "Navigatsiya", + "readtime.one": "1 daqiqa o'qish", + "readtime.other": "# daqiqa o'qish", + "rss.created": "RSS tasmasi", + "rss.updated": "Yangilangan kontentning RSS tasmasi", + "search": "Qidirish", + "search.config.lang": "tr", + "search.placeholder": "Qidirish", + "search.share": "Ulashish", + "search.reset": "Tozalash", + "search.result.initializer": "Qidiruv ishga tushirilmoqda", + "search.result.placeholder": "Qidiruvni boshlash uchun kiriting", + "search.result.none": "Mos natijalar yo'q", + "search.result.one": "1 ta mos natija", + "search.result.other": "# ta mos keladigan natijalar", + "search.result.more.one": "Ushbu sahifada yana 1 ta natija", + "search.result.more.other": "Bu sahifada yana # ta natija", + "search.result.term.missing": "To'ldirilmagan", + "select.language": "Tilni tanlang", + "select.version": "Versiyani tanlang", + "source": "Repozitoriyga o'tish", + "source.file.contributors": "Hissa qo'shuvchilar", + "source.file.date.created": "Yaratildi", + "source.file.date.updated": "Oxirgi yangilanish", + "tabs": "Yorliqlar", + "toc": "Mundarija", + "top": "Yuqoriga qaytish" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/vi.html b/docs/src/templates/partials/languages/vi.html new file mode 100644 index 00000000..b63a8d82 --- /dev/null +++ b/docs/src/templates/partials/languages/vi.html @@ -0,0 +1,76 @@ + + + +{% macro t(key) %}{{ { + "language": "vi", + "action.edit": "Chỉnh sửa", + "action.skip": "Bỏ qua", + "action.view": "Xem mã nguồn của trang", + "announce.dismiss": "Không hiển thị lại", + "blog.archive": "Lưu trữ", + "blog.categories": "Mục", + "blog.categories.in": "Trong", + "blog.continue": "Tiếp tục đọc", + "blog.draft": "Bản nháp", + "blog.index": "Quay lại", + "blog.meta": "Metadata", + "blog.references": "Các liên kết liên quan", + "clipboard.copy": "Sao chép vào bộ nhớ tạm", + "clipboard.copied": "Đã sao chép", + "consent.accept": "Đồng ý", + "consent.manage": "Cài đặt", + "consent.reject": "Từ chối", + "footer": "Chân trang", + "footer.next": "Sau", + "footer.previous": "Trước", + "header": "Đầu trang", + "meta.comments": "Bình luận", + "meta.source": "Mã nguồn", + "nav": "Thanh điều hướng", + "readtime.one": "1 phút đọc", + "readtime.other": "# phút đọc", + "rss.created": "RSS feed", + "rss.updated": "RSS feed of updated content", + "search": "Tìm kiếm", + "search.config.lang": "vi", + "search.placeholder": "Tìm kiếm", + "search.share": "Chia sẻ", + "search.reset": "Xoá", + "search.result.initializer": "Initializing search", + "search.result.placeholder": "Nhập để bắt đầu tìm kiếm", + "search.result.none": "Không tìm thấy tài liệu liên quan", + "search.result.one": "1 tài liệu liên quan", + "search.result.other": "# tài liệu liên quan", + "search.result.more.one": "1 từ khác trong trang", + "search.result.more.other": "# từ khác trong trang", + "search.result.term.missing": "Không", + "select.language": "Chọn ngôn ngữ", + "select.version": "Chọn phiên bản", + "source": "Xem mã nguồn", + "source.file.contributors": "Contributors", + "source.file.date.created": "Tạo", + "source.file.date.updated": "Cập nhật lần cuối", + "tabs": "Tabs", + "toc": "Mục lục", + "top": "Trở lại mục lục" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/zh-Hant.html b/docs/src/templates/partials/languages/zh-Hant.html new file mode 100644 index 00000000..578fc82a --- /dev/null +++ b/docs/src/templates/partials/languages/zh-Hant.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "zh-Hant", + "action.edit": "編輯此頁", + "action.skip": "跳轉至", + "action.view": "查看源代碼", + "announce.dismiss": "不再顯示此訊息", + "blog.archive": "存檔", + "blog.categories": "分類", + "blog.categories.in": "分類在", + "blog.continue": "繼續閲讀", + "blog.draft": "草稿", + "blog.index": "回到首頁", + "blog.meta": "元數據", + "blog.references": "相關鏈接", + "clipboard.copy": "拷貝", + "clipboard.copied": "已拷貝", + "consent.accept": "接受", + "consent.manage": "管理設置", + "consent.reject": "拒絕", + "footer": "頁脚", + "footer.next": "下一頁", + "footer.previous": "上一頁", + "header": "頁首", + "meta.comments": "評論", + "meta.source": "來源", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "nav": "導航", + "readtime.one": "需要 1 分鐘閲讀", + "readtime.other": "需要 # 分鐘閲讀", + "rss.created": "簡易資訊聚合", + "rss.updated": "更新之部分的簡易資訊聚合", + "search": "搜尋", + "search.placeholder": "搜尋", + "search.share": "分享", + "search.reset": "清空", + "search.result.initializer": "正在初始化搜尋引擎", + "search.result.placeholder": "鍵入以開始檢索", + "search.result.none": "沒有找到符合條件的結果", + "search.result.one": "找到 1 个符合條件的結果", + "search.result.other": "找到 # 個符合條件的結果", + "search.result.more.one": "此頁尚有 1 個符合的項目", + "search.result.more.other": "此頁尚有 # 個符合的項目", + "search.result.term.missing": "缺失", + "select.language": "選擇語言", + "select.version": "選擇版本", + "source": "前往倉庫", + "source.file.contributors": "貢獻者", + "source.file.date.created": "建立日期", + "source.file.date.updated": "最後更新", + "tabs": "標籤頁", + "toc": "目錄", + "top": "回到頂部" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/zh-TW.html b/docs/src/templates/partials/languages/zh-TW.html new file mode 100644 index 00000000..405538f8 --- /dev/null +++ b/docs/src/templates/partials/languages/zh-TW.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "zh-TW", + "action.edit": "編輯此頁", + "action.skip": "跳轉到", + "action.view": "查看此頁原始碼", + "announce.dismiss": "不再顯示此訊息", + "blog.archive": "封存", + "blog.categories": "分類", + "blog.categories.in": "於", + "blog.continue": "繼續閱讀", + "blog.draft": "草稿", + "blog.index": "回到主頁", + "blog.meta": "元數據", + "blog.references": "相關連結", + "clipboard.copy": "複製", + "clipboard.copied": "已複製", + "consent.accept": "同意", + "consent.manage": "管理設定", + "consent.reject": "拒絕", + "footer": "頁腳", + "footer.next": "下一頁", + "footer.previous": "上一頁", + "header": "頁首", + "meta.comments": "留言", + "meta.source": "來源", + "nav": "導覽列", + "readtime.one": "需要 1 分鐘閱讀時間", + "readtime.other": "需要 # 分鐘閱讀時間", + "rss.created": "RSS 訂閱", + "rss.updated": "RSS 訂閱內容已更新", + "search": "搜尋", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "search.placeholder": "搜尋", + "search.share": "分享", + "search.reset": "清除", + "search.result.initializer": "正在初始化搜尋引擎", + "search.result.placeholder": "打字進行搜尋", + "search.result.none": "沒有符合的項目", + "search.result.one": "找到 1 個符合的項目", + "search.result.other": "找到 # 個符合的項目", + "search.result.more.one": "此頁尚有 1 個符合的項目", + "search.result.more.other": "此頁尚有 # 個符合的項目", + "search.result.term.missing": "缺少字詞", + "select.language": "選擇語言", + "select.version": "選擇版本", + "source": "前往倉庫", + "source.file.contributors": "貢獻者", + "source.file.date.created": "建立日期", + "source.file.date.updated": "最後更新", + "tabs": "標籤", + "toc": "目錄", + "top": "回到頂端" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/languages/zh.html b/docs/src/templates/partials/languages/zh.html new file mode 100644 index 00000000..49f233a4 --- /dev/null +++ b/docs/src/templates/partials/languages/zh.html @@ -0,0 +1,77 @@ + + + +{% macro t(key) %}{{ { + "language": "zh", + "action.edit": "编辑此页", + "action.skip": "跳转至", + "action.view": "查看本页的源代码", + "announce.dismiss": "不再显示此消息", + "blog.archive": "归档", + "blog.categories": "分类", + "blog.categories.in": "分类于", + "blog.continue": "继续阅读", + "blog.draft": "草稿", + "blog.index": "回到主页", + "blog.meta": "元数据", + "blog.references": "相关链接", + "clipboard.copy": "复制", + "clipboard.copied": "已复制", + "consent.accept": "同意", + "consent.manage": "管理设定", + "consent.reject": "拒绝", + "footer": "页脚", + "footer.next": "下一页", + "footer.previous": "上一页", + "header": "页眉", + "meta.comments": "评论", + "meta.source": "来源", + "nav": "导航栏", + "readtime.one": "需要 1 分钟阅读时间", + "readtime.other": "需要 # 分钟阅读时间", + "rss.created": "RSS 订阅", + "rss.updated": "已更新内容的 RSS 订阅", + "search": "查找", + "search.config.pipeline": "stemmer", + "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", + "search.placeholder": "搜索", + "search.share": "分享", + "search.reset": "清空当前内容", + "search.result.initializer": "正在初始化搜索引擎", + "search.result.placeholder": "键入以开始搜索", + "search.result.none": "没有找到符合条件的结果", + "search.result.one": "找到 1 个符合条件的结果", + "search.result.other": "# 个符合条件的结果", + "search.result.more.one": "在该页上还有 1 个符合条件的结果", + "search.result.more.other": "在该页上还有 # 个符合条件的结果", + "search.result.term.missing": "缺少", + "select.language": "选择当前语言", + "select.version": "选择当前版本", + "source": "前往仓库", + "source.file.contributors": "贡献者", + "source.file.date.created": "创建日期", + "source.file.date.updated": "最后更新", + "tabs": "标签", + "toc": "目录", + "top": "回到页面顶部" +}[key] }}{% endmacro %} diff --git a/docs/src/templates/partials/logo.html b/docs/src/templates/partials/logo.html new file mode 100644 index 00000000..05832c71 --- /dev/null +++ b/docs/src/templates/partials/logo.html @@ -0,0 +1,29 @@ + + + +{% if config.theme.logo %} + logo +{% else %} + {% set icon = config.theme.icon.logo or "material/library" %} + {% include ".icons/" ~ icon ~ ".svg" %} +{% endif %} diff --git a/docs/src/templates/partials/nav-item.html b/docs/src/templates/partials/nav-item.html new file mode 100644 index 00000000..24d74a1a --- /dev/null +++ b/docs/src/templates/partials/nav-item.html @@ -0,0 +1,249 @@ + + + +{% macro render_status(nav_item, type) %} + {% set class = "md-status md-status--" ~ type %} + + + {% if config.extra.status and config.extra.status[type] %} + + + + + {% else %} + + {% endif %} +{% endmacro %} + + +{% macro render_content(nav_item, ref = nav_item) %} + + + {% if nav_item.is_page and nav_item.meta.icon %} + {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} + {% endif %} + + + + {{ ref.title }} + + + + {% if nav_item.is_page and nav_item.meta.status %} + {{ render_status(nav_item, nav_item.meta.status) }} + {% endif %} +{% endmacro %} + + +{% macro render_pruned(nav_item, ref = nav_item) %} + {% set first = nav_item.children | first %} + + + {% if first and first.children %} + {{ render_pruned(first, ref) }} + + + {% else %} + + {{ render_content(ref) }} + + + {% if nav_item.children | length > 0 %} + + {% endif %} + + {% endif %} +{% endmacro %} + + +{% macro render(nav_item, path, level) %} + + + {% set class = "md-nav__item" %} + {% if nav_item.active %} + {% set class = class ~ " md-nav__item--active" %} + {% endif %} + + + {% if nav_item.children %} + + + {% 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 %} + + + {% set tabs = "navigation.tabs" in features %} + {% set sections = "navigation.sections" in features %} + {% if tabs and level == 1 or sections and tabs >= level - 1 %} + {% set class = class ~ " md-nav__item--section" %} + {% set is_section = true %} + + + {% elif not nav_item.active and "navigation.prune" in features %} + {% set class = class ~ " md-nav__item--pruned" %} + {% set is_pruned = true %} + {% endif %} + + +
  • + {% if not is_pruned %} + {% set checked = "checked" if nav_item.active %} + + + {% set is_expanded = "navigation.expand" in features %} + {% if is_expanded and not checked %} + {% set indeterminate = "md-toggle--indeterminate" %} + {% endif %} + + + + + + {% if not indexes %} + {% set tabindex = "0" if not is_section %} + + + + {% else %} + {% set index = indexes | first %} + {% set class = "md-nav__link--active" if index == page %} + + {% endif %} + + + + + + {% else %} + {{ render_pruned(nav_item) }} + {% endif %} +
  • + + + {% elif nav_item == page %} +
  • + {% set toc = page.toc %} + + + + + + {% set first = toc | first %} + {% if first and first.level == 1 %} + {% set toc = first.children %} + {% endif %} + + + {% if toc %} + + {% endif %} + + {{ render_content(nav_item) }} + + + + {% if toc %} + {% include "partials/toc.html" %} + {% endif %} +
  • + + + {% else %} +
  • + + {{ render_content(nav_item) }} + +
  • + {% endif %} +{% endmacro %} diff --git a/docs/src/templates/partials/nav.html b/docs/src/templates/partials/nav.html new file mode 100644 index 00000000..c41fe694 --- /dev/null +++ b/docs/src/templates/partials/nav.html @@ -0,0 +1,69 @@ + + +{% import "partials/nav-item.html" as item with context %} + + +{% 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 %} + + + diff --git a/docs/src/templates/partials/pagination.html b/docs/src/templates/partials/pagination.html new file mode 100644 index 00000000..046ecbe9 --- /dev/null +++ b/docs/src/templates/partials/pagination.html @@ -0,0 +1,42 @@ + + + +{% import ".icons/material/chevron-double-left.svg" as icon_first %} +{% import ".icons/material/chevron-left.svg" as icon_previous %} +{% import ".icons/material/chevron-right.svg" as icon_next %} +{% import ".icons/material/chevron-double-right.svg" as icon_last %} + + + diff --git a/docs/src/templates/partials/palette.html b/docs/src/templates/partials/palette.html new file mode 100644 index 00000000..ccb8db0a --- /dev/null +++ b/docs/src/templates/partials/palette.html @@ -0,0 +1,55 @@ + + + +
    + {% for option in config.theme.palette %} + {% set scheme = option.scheme | d("default", true) %} + {% set primary = option.primary | d("indigo", true) %} + {% set accent = option.accent | d("indigo", true) %} + + {% if option.toggle %} + + {% endif %} + {% endfor %} +
    diff --git a/docs/src/templates/partials/post.html b/docs/src/templates/partials/post.html new file mode 100644 index 00000000..c7233051 --- /dev/null +++ b/docs/src/templates/partials/post.html @@ -0,0 +1,99 @@ + + + +
    +
    + + + {% if post.authors %} + + {% endif %} + + + +
    + + +
    + {{ post.content }} + + + +
    +
    diff --git a/docs/src/templates/partials/progress.html b/docs/src/templates/partials/progress.html new file mode 100644 index 00000000..f5d13d10 --- /dev/null +++ b/docs/src/templates/partials/progress.html @@ -0,0 +1,24 @@ + + + +
    diff --git a/docs/src/templates/partials/search.html b/docs/src/templates/partials/search.html new file mode 100644 index 00000000..1854a7d3 --- /dev/null +++ b/docs/src/templates/partials/search.html @@ -0,0 +1,109 @@ + + + + diff --git a/docs/src/templates/partials/social.html b/docs/src/templates/partials/social.html new file mode 100644 index 00000000..5d2c4017 --- /dev/null +++ b/docs/src/templates/partials/social.html @@ -0,0 +1,48 @@ + + + +
    + {% for social in config.extra.social %} + + + {% set rel = "noopener" %} + {% if "mastodon" in social.icon %} + {% set rel = rel ~ " me" %} + {% endif %} + + + {% set title = social.name %} + {% if not title and "//" in social.link %} + {% set _, url = social.link.split("//") %} + {% set title = url.split("/")[0] %} + {% endif %} + + {% include ".icons/" ~ social.icon ~ ".svg" %} + + {% endfor %} +
    diff --git a/docs/src/templates/partials/source-file.html b/docs/src/templates/partials/source-file.html new file mode 100644 index 00000000..928e35de --- /dev/null +++ b/docs/src/templates/partials/source-file.html @@ -0,0 +1,44 @@ + + + +
    +
    + + + + {% 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 %} +
    + {{ lang.t("source.file.date.created") }}: + {{ page.meta.git_creation_date_localized }} + {% endif %} + + + {% elif page.meta.revision_date %} + {{ lang.t("source.file.date.updated") }}: + {{ page.meta.revision_date }} + {% endif %} +
    +
    diff --git a/docs/src/templates/partials/source.html b/docs/src/templates/partials/source.html new file mode 100644 index 00000000..f4aac3e6 --- /dev/null +++ b/docs/src/templates/partials/source.html @@ -0,0 +1,37 @@ + + + + +
    + {% set icon = config.theme.icon.repo or "fontawesome/brands/git-alt" %} + {% include ".icons/" ~ icon ~ ".svg" %} +
    +
    + {{ config.repo_name }} +
    +
    diff --git a/docs/src/templates/partials/tabs-item.html b/docs/src/templates/partials/tabs-item.html new file mode 100644 index 00000000..7a12a742 --- /dev/null +++ b/docs/src/templates/partials/tabs-item.html @@ -0,0 +1,71 @@ + + + +{% macro render_content(nav_item, ref = nav_item) %} + + + {% if nav_item == ref or "navigation.indexes" in features %} + {% if nav_item.is_index and nav_item.meta.icon %} + {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} + {% endif %} + {% endif %} + + + {{ ref.title }} +{% endmacro %} + + +{% macro render(nav_item, ref = nav_item) %} + + + {% set class = "md-tabs__item" %} + {% if ref.active %} + {% set class = class ~ " md-tabs__item--active" %} + {% endif %} + + + {% if nav_item.children %} + {% set first = nav_item.children | first %} + + + {% if first.children %} + {{ render(first, ref) }} + + + {% else %} +
  • + + {{ render_content(first, ref) }} + +
  • + {% endif %} + + + {% else %} +
  • + + {{ render_content(nav_item) }} + +
  • + {% endif %} +{% endmacro %} diff --git a/docs/src/templates/partials/tabs.html b/docs/src/templates/partials/tabs.html new file mode 100644 index 00000000..0ea590cf --- /dev/null +++ b/docs/src/templates/partials/tabs.html @@ -0,0 +1,38 @@ + + +{% import "partials/tabs-item.html" as item with context %} + + + diff --git a/docs/src/templates/partials/tags.html b/docs/src/templates/partials/tags.html new file mode 100644 index 00000000..b3dea295 --- /dev/null +++ b/docs/src/templates/partials/tags.html @@ -0,0 +1,52 @@ + + + +{% if page.meta and page.meta.hide %} + {% set hidden = "hidden" if "tags" in page.meta.hide %} +{% endif %} + + + diff --git a/docs/src/templates/partials/toc-item.html b/docs/src/templates/partials/toc-item.html new file mode 100644 index 00000000..1af82c56 --- /dev/null +++ b/docs/src/templates/partials/toc-item.html @@ -0,0 +1,39 @@ + + + +
  • + + {{ toc_item.title }} + + + + {% if toc_item.children %} + + {% endif %} +
  • diff --git a/docs/src/templates/partials/toc.html b/docs/src/templates/partials/toc.html new file mode 100644 index 00000000..cb50b257 --- /dev/null +++ b/docs/src/templates/partials/toc.html @@ -0,0 +1,56 @@ + + + +{% set title = lang.t("toc") %} +{% if config.mdx_configs.toc and config.mdx_configs.toc.title %} + {% set title = config.mdx_configs.toc.title %} +{% endif %} + + + diff --git a/docs/src/templates/partials/top.html b/docs/src/templates/partials/top.html new file mode 100644 index 00000000..737e6248 --- /dev/null +++ b/docs/src/templates/partials/top.html @@ -0,0 +1,28 @@ + + + + diff --git a/docs/src/templates/redirect.html b/docs/src/templates/redirect.html new file mode 100644 index 00000000..80869c4f --- /dev/null +++ b/docs/src/templates/redirect.html @@ -0,0 +1,41 @@ + + + + + + + + {{ config.site_name }} + + + + + diff --git a/docs/src/test.py b/docs/src/test.py new file mode 100644 index 00000000..349b1bfd --- /dev/null +++ b/docs/src/test.py @@ -0,0 +1,4 @@ +import infini + +client = infini.Cli() +client.parse_args() diff --git a/examples/rule-pypackage/__init__.py b/examples/rule-pypackage/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/rule-pypackage/config.py b/examples/rule-pypackage/config.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/rule-singlefile.py b/examples/rule-singlefile.py deleted file mode 100644 index 04df25e1..00000000 --- a/examples/rule-singlefile.py +++ /dev/null @@ -1,10 +0,0 @@ -from infini import Rule, Core - -core = Core() - -class MyRule(Rule): - ... - - -if __name__ == "__main__": - core.run() \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 361f7f8c..8fe65436 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ theme: logo: material/book-sync repo: fontawesome/brands/git-alt annotation: material/arrow-right-circle - custom_dir: material/overrides + # custom_dir: docs/material/overrides palette: - scheme: default primary: indigo @@ -64,7 +64,7 @@ extra: deprecated: 已被废弃 version: provider: mike - homepage: https://grps.retrofor.space # hydroroll-team.github.io + homepage: https://grps.hydroroll.team # hydroroll-team.github.io consent: title: Cookie 偏好设置 description: >- diff --git a/pdm.lock b/pdm.lock index 2cceaba7..ff519c32 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,10 +3,9 @@ [metadata] groups = ["default"] -cross_platform = true -static_urls = false -lock_version = "4.3" -content_hash = "sha256:860461057289e7a545cf024ca51cc9776a6e9b4588aadf9893bbdb9814d53d73" +strategy = ["cross_platform"] +lock_version = "4.4" +content_hash = "sha256:9307c6c68876198929d269c58d5e5ebb39f6563bb8f3e24abaf8cb8491ef9a6e" [[package]] name = "annotated-types" @@ -21,248 +20,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "anyio" -version = "4.0.0" -requires_python = ">=3.8" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", -] -files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, -] - -[[package]] -name = "babel" -version = "2.13.0" -requires_python = ">=3.7" -summary = "Internationalization utilities" -dependencies = [ - "pytz>=2015.7; python_version < \"3.9\"", -] -files = [ - {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, - {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -requires_python = ">=3.6.0" -summary = "Screen-scraping library" -dependencies = [ - "soupsieve>1.2", -] -files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, -] - -[[package]] -name = "cairocffi" -version = "1.6.1" -requires_python = ">=3.7" -summary = "cffi-based cairo bindings for Python" -dependencies = [ - "cffi>=1.1.0", -] -files = [ - {file = "cairocffi-1.6.1-py3-none-any.whl", hash = "sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e"}, - {file = "cairocffi-1.6.1.tar.gz", hash = "sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7"}, -] - -[[package]] -name = "cairosvg" -version = "2.7.1" -requires_python = ">=3.5" -summary = "A Simple SVG Converter based on Cairo" -dependencies = [ - "cairocffi", - "cssselect2", - "defusedxml", - "pillow", - "tinycss2", -] -files = [ - {file = "CairoSVG-2.7.1-py3-none-any.whl", hash = "sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b"}, - {file = "CairoSVG-2.7.1.tar.gz", hash = "sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0"}, -] - -[[package]] -name = "certifi" -version = "2023.7.22" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -requires_python = ">=3.8" -summary = "Foreign Function Interface for Python calling C code." -dependencies = [ - "pycparser", -] -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.0" -requires_python = ">=3.7.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -files = [ - {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, - {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, -] - -[[package]] -name = "click" -version = "8.1.7" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - [[package]] name = "colorama" version = "0.4.6" @@ -273,82 +30,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "csscompressor" -version = "0.9.5" -summary = "A python port of YUI CSS Compressor" -files = [ - {file = "csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05"}, -] - -[[package]] -name = "cssselect2" -version = "0.7.0" -requires_python = ">=3.7" -summary = "CSS selectors for Python ElementTree" -dependencies = [ - "tinycss2", - "webencodings", -] -files = [ - {file = "cssselect2-0.7.0-py3-none-any.whl", hash = "sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969"}, - {file = "cssselect2-0.7.0.tar.gz", hash = "sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -summary = "XML bomb protection for Python stdlib modules" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - -[[package]] -name = "distribute" -version = "0.7.3" -summary = "" -dependencies = [ - "setuptools>=0.7", -] -files = [ - {file = "distribute-0.7.3.zip", hash = "sha256:3dc7a8d059dcf72f0ead2fa2144a24ee0ef07dce816e8c3545d7345767138c5e"}, -] - -[[package]] -name = "essentials" -version = "1.1.5" -summary = "General purpose classes and functions, reusable in any kind of Python application" -files = [ - {file = "essentials-1.1.5-py3-none-any.whl", hash = "sha256:905fa4a69fcd2b2cf41ecc6cc65827e30c87ef91f3f5c71540bcc5e984fa8360"}, - {file = "essentials-1.1.5.tar.gz", hash = "sha256:8736f738bb2c51d5069b2de2cf9146f7d402f25f9f95636781e59a422c908c46"}, -] - -[[package]] -name = "essentials-openapi" -version = "1.0.8" -requires_python = ">=3.7" -summary = "Classes to generate OpenAPI Documentation v3 and v2, in JSON and YAML." -dependencies = [ - "essentials>=1.1.5", - "pyyaml>=6", -] -files = [ - {file = "essentials_openapi-1.0.8-py3-none-any.whl", hash = "sha256:1d3afc67cefc952aade7d406c2ffac81634e54ce7e597ddd8ceb1bfe1fe62e46"}, - {file = "essentials_openapi-1.0.8.tar.gz", hash = "sha256:37832b7b422939bdcf9cc6f52d2317e0ee19907ac8aba2c77a8cff49a2c0b26a"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.1.3" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, -] - [[package]] name = "freetype-py" version = "2.3.0" @@ -364,138 +45,6 @@ files = [ {file = "freetype_py-2.3.0-py3-none-win_amd64.whl", hash = "sha256:3a552265b06c2cb3fa54f86ed6fcbf045d8dc8176f9475bedddf9a1b31f5402f"}, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -summary = "Copy your docs directly to the gh-pages branch." -dependencies = [ - "python-dateutil>=2.8.1", -] -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] - -[[package]] -name = "gitdb" -version = "4.0.10" -requires_python = ">=3.7" -summary = "Git Object Database" -dependencies = [ - "smmap<6,>=3.0.1", -] -files = [ - {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, - {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, -] - -[[package]] -name = "gitpython" -version = "3.1.37" -requires_python = ">=3.7" -summary = "GitPython is a Python library used to interact with Git repositories" -dependencies = [ - "gitdb<5,>=4.0.1", -] -files = [ - {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, - {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, -] - -[[package]] -name = "h11" -version = "0.14.0" -requires_python = ">=3.7" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "htmlmin2" -version = "0.1.13" -summary = "An HTML Minifier" -files = [ - {file = "htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2"}, -] - -[[package]] -name = "httpcore" -version = "0.18.0" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -dependencies = [ - "anyio<5.0,>=3.0", - "certifi", - "h11<0.15,>=0.13", - "sniffio==1.*", -] -files = [ - {file = "httpcore-0.18.0-py3-none-any.whl", hash = "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced"}, - {file = "httpcore-0.18.0.tar.gz", hash = "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9"}, -] - -[[package]] -name = "httpx" -version = "0.25.0" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -dependencies = [ - "certifi", - "httpcore<0.19.0,>=0.18.0", - "idna", - "sniffio", -] -files = [ - {file = "httpx-0.25.0-py3-none-any.whl", hash = "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100"}, - {file = "httpx-0.25.0.tar.gz", hash = "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875"}, -] - -[[package]] -name = "idna" -version = "3.4" -requires_python = ">=3.5" -summary = "Internationalized Domain Names in Applications (IDNA)" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] - -[[package]] -name = "importlib-metadata" -version = "6.8.0" -requires_python = ">=3.8" -summary = "Read metadata from Python packages" -dependencies = [ - "zipp>=0.5", -] -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[[package]] -name = "jinja2" -version = "3.1.2" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] - -[[package]] -name = "jsmin" -version = "3.0.1" -summary = "JavaScript minifier." -files = [ - {file = "jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc"}, -] - [[package]] name = "loguru" version = "0.7.2" @@ -510,447 +59,6 @@ files = [ {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, ] -[[package]] -name = "lxml" -version = "4.9.3" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -files = [ - {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, - {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, - {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, - {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, - {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, - {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, - {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, - {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, - {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, - {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, - {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, - {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, - {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, - {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, - {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, -] - -[[package]] -name = "markdown" -version = "3.5" -requires_python = ">=3.8" -summary = "Python implementation of John Gruber's Markdown." -dependencies = [ - "importlib-metadata>=4.4; python_version < \"3.10\"", -] -files = [ - {file = "Markdown-3.5-py3-none-any.whl", hash = "sha256:4afb124395ce5fc34e6d9886dab977fd9ae987fc6e85689f08278cf0c69d4bf3"}, - {file = "Markdown-3.5.tar.gz", hash = "sha256:a807eb2e4778d9156c8f07876c6e4d50b5494c5665c4834f67b06459dfd877b3"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -dependencies = [ - "mdurl~=0.1", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "2.1.3" -requires_python = ">=3.7" -summary = "Safely add untrusted strings to HTML/XML markup." -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -requires_python = ">=3.6" -summary = "A deep merge function for 🐍." -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] - -[[package]] -name = "mike" -version = "1.1.2" -summary = "Manage multiple versions of your MkDocs-powered documentation" -dependencies = [ - "jinja2", - "mkdocs>=1.0", - "pyyaml>=5.1", - "verspec", -] -files = [ - {file = "mike-1.1.2-py3-none-any.whl", hash = "sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca"}, - {file = "mike-1.1.2.tar.gz", hash = "sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b"}, -] - -[[package]] -name = "mkdocs" -version = "1.5.3" -requires_python = ">=3.7" -summary = "Project documentation with Markdown." -dependencies = [ - "click>=7.0", - "colorama>=0.4; platform_system == \"Windows\"", - "ghp-import>=1.0", - "importlib-metadata>=4.3; python_version < \"3.10\"", - "jinja2>=2.11.1", - "markdown>=3.2.1", - "markupsafe>=2.0.1", - "mergedeep>=1.3.4", - "packaging>=20.5", - "pathspec>=0.11.1", - "platformdirs>=2.2.0", - "pyyaml-env-tag>=0.1", - "pyyaml>=5.1", - "watchdog>=2.0", -] -files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, -] - -[[package]] -name = "mkdocs-autorefs" -version = "0.5.0" -requires_python = ">=3.8" -summary = "Automatically link across pages in MkDocs." -dependencies = [ - "Markdown>=3.3", - "mkdocs>=1.1", -] -files = [ - {file = "mkdocs_autorefs-0.5.0-py3-none-any.whl", hash = "sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff"}, - {file = "mkdocs_autorefs-0.5.0.tar.gz", hash = "sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0"}, -] - -[[package]] -name = "mkdocs-blog-plugin" -version = "0.25" -summary = "Keeps a really simple blog section inside your MkDocs site." -files = [ - {file = "mkdocs-blog-plugin-0.25.tar.gz", hash = "sha256:36dcd804f6ab1314d19c32e46bf21076d5f4425e1bca22f2e735b9b4baaec836"}, -] - -[[package]] -name = "mkdocs-git-authors-plugin" -version = "0.7.2" -requires_python = ">=3.7" -summary = "Mkdocs plugin to display git authors of a page" -dependencies = [ - "mkdocs>=1.0", -] -files = [ - {file = "mkdocs-git-authors-plugin-0.7.2.tar.gz", hash = "sha256:f541730e4cabdafa0ac758c94d28ba5e8ddca4c859e5de4c89f1226cb6ccd0ad"}, - {file = "mkdocs_git_authors_plugin-0.7.2-py3-none-any.whl", hash = "sha256:c8a2784a867db79ad3b477a96ee96875d17b09192b6d3be71f08df25afff76c4"}, -] - -[[package]] -name = "mkdocs-git-committers-plugin-2" -version = "1.2.0" -requires_python = ">=3.8,<4" -summary = "An MkDocs plugin to create a list of contributors on the page. The git-committers plugin will seed the template context with a list of github committers and other useful GIT info such as last modified date" -dependencies = [ - "beautifulsoup4", - "gitpython", - "lxml>=4.9", - "mkdocs>=1.0.3", - "requests", -] -files = [ - {file = "mkdocs-git-committers-plugin-2-1.2.0.tar.gz", hash = "sha256:921da26b3f4393e6c170279ac34089151dfc22cd29ec4fbce3506218541685c8"}, - {file = "mkdocs_git_committers_plugin_2-1.2.0-py3-none-any.whl", hash = "sha256:0bb5d71cdd9d43fec0dec16e52a9aad2784256b0fa6ef9bb0cceffc36c081ab3"}, -] - -[[package]] -name = "mkdocs-git-revision-date-localized-plugin" -version = "1.2.1" -requires_python = ">=3.6" -summary = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." -dependencies = [ - "GitPython", - "babel>=2.7.0", - "mkdocs>=1.0", - "pytz", -] -files = [ - {file = "mkdocs-git-revision-date-localized-plugin-1.2.1.tar.gz", hash = "sha256:fc5b23a9d572cbba0114e9e17152001d01724990cb308830e58291fa614faf73"}, - {file = "mkdocs_git_revision_date_localized_plugin-1.2.1-py3-none-any.whl", hash = "sha256:d57dc99d67af917899e69c392f1ebccd1779fa243d641255469b03f8a3596b96"}, -] - -[[package]] -name = "mkdocs-material" -version = "9.4.6" -requires_python = ">=3.8" -summary = "Documentation that simply works" -dependencies = [ - "babel~=2.10", - "colorama~=0.4", - "jinja2~=3.0", - "markdown~=3.2", - "mkdocs-material-extensions~=1.2", - "mkdocs>=1.5.3,~=1.5", - "paginate~=0.5", - "pygments~=2.16", - "pymdown-extensions~=10.2", - "regex>=2022.4", - "requests~=2.26", -] -files = [ - {file = "mkdocs_material-9.4.6-py3-none-any.whl", hash = "sha256:78802035d5768a78139c84ad7dce0c6493e8f7dc4861727d36ed91d1520a54da"}, - {file = "mkdocs_material-9.4.6.tar.gz", hash = "sha256:09665e60df7ee9e5ff3a54af173f6d45be718b1ee7dd962bcff3102b81fb0c14"}, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3" -requires_python = ">=3.8" -summary = "Extension pack for Python Markdown and MkDocs Material." -files = [ - {file = "mkdocs_material_extensions-1.3-py3-none-any.whl", hash = "sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc"}, - {file = "mkdocs_material_extensions-1.3.tar.gz", hash = "sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd"}, -] - -[[package]] -name = "mkdocs-material" -version = "9.4.6" -extras = ["imaging"] -requires_python = ">=3.8" -summary = "Documentation that simply works" -dependencies = [ - "cairosvg~=2.6", - "mkdocs-material==9.4.6", - "pillow~=9.4", -] -files = [ - {file = "mkdocs_material-9.4.6-py3-none-any.whl", hash = "sha256:78802035d5768a78139c84ad7dce0c6493e8f7dc4861727d36ed91d1520a54da"}, - {file = "mkdocs_material-9.4.6.tar.gz", hash = "sha256:09665e60df7ee9e5ff3a54af173f6d45be718b1ee7dd962bcff3102b81fb0c14"}, -] - -[[package]] -name = "mkdocs-minify-plugin" -version = "0.7.1" -requires_python = ">=3.7" -summary = "An MkDocs plugin to minify HTML, JS or CSS files prior to being written to disk" -dependencies = [ - "csscompressor>=0.9.5", - "htmlmin2>=0.1.13", - "jsmin>=3.0.1", - "mkdocs>=1.4.1", -] -files = [ - {file = "mkdocs-minify-plugin-0.7.1.tar.gz", hash = "sha256:6abf8f5a0fddb476bddd38faba28390fd8c41ab63b0d7202e3ce3deeb9ab98cb"}, - {file = "mkdocs_minify_plugin-0.7.1-py3-none-any.whl", hash = "sha256:29bd6a1aa5b0217a55b08333194e20cf1ff83b63fb6a22a33f10f8fa9745c28a"}, -] - -[[package]] -name = "mkdocs-redirects" -version = "1.2.1" -requires_python = ">=3.6" -summary = "A MkDocs plugin for dynamic page redirects to prevent broken links." -dependencies = [ - "mkdocs>=1.1.1", -] -files = [ - {file = "mkdocs-redirects-1.2.1.tar.gz", hash = "sha256:9420066d70e2a6bb357adf86e67023dcdca1857f97f07c7fe450f8f1fb42f861"}, -] - -[[package]] -name = "mkdocs-rss-plugin" -version = "1.8.0" -requires_python = ">=3.8, <4" -summary = "MkDocs plugin which generates a static RSS feed using git log and page.meta." -dependencies = [ - "GitPython<3.2,>=3.1", - "mkdocs<2,>=1.1", - "pytz==2022.*; python_version < \"3.9\"", - "tzdata==2023.*; python_version >= \"3.9\" and sys_platform == \"win32\"", -] -files = [ - {file = "mkdocs-rss-plugin-1.8.0.tar.gz", hash = "sha256:475bf4ea05cbe786af38d519b55352b1b2eb87597ad680fcbc309056864ed5c4"}, - {file = "mkdocs_rss_plugin-1.8.0-py2.py3-none-any.whl", hash = "sha256:0fa13c99730c1d3ad9ec05102ff3d148c9849396c40b30be789339d38722fa8f"}, -] - -[[package]] -name = "mkdocstrings" -version = "0.23.0" -requires_python = ">=3.8" -summary = "Automatic documentation from sources, for MkDocs." -dependencies = [ - "Jinja2>=2.11.1", - "Markdown>=3.3", - "MarkupSafe>=1.1", - "importlib-metadata>=4.6; python_version < \"3.10\"", - "mkdocs-autorefs>=0.3.1", - "mkdocs>=1.2", - "pymdown-extensions>=6.3", - "typing-extensions>=4.1; python_version < \"3.10\"", -] -files = [ - {file = "mkdocstrings-0.23.0-py3-none-any.whl", hash = "sha256:051fa4014dfcd9ed90254ae91de2dbb4f24e166347dae7be9a997fe16316c65e"}, - {file = "mkdocstrings-0.23.0.tar.gz", hash = "sha256:d9c6a37ffbe7c14a7a54ef1258c70b8d394e6a33a1c80832bce40b9567138d1c"}, -] - -[[package]] -name = "neoteroi-mkdocs" -version = "1.0.4" -requires_python = ">=3.7" -summary = "Plugins for MkDocs and Python Markdown" -dependencies = [ - "click", - "essentials-openapi", - "httpx", - "jinja2", - "mkdocs", - "rich", -] -files = [ - {file = "neoteroi_mkdocs-1.0.4-py3-none-any.whl", hash = "sha256:ed2b93a206173b10cd5635e050ce0d8e83df0e51cfd5ee6d41751bfcaa4fe197"}, - {file = "neoteroi_mkdocs-1.0.4.tar.gz", hash = "sha256:3825bcecc4b2c7755ef6adbf96d754762dafd76db508fdd3a23533c88e744228"}, -] - -[[package]] -name = "packaging" -version = "23.2" -requires_python = ">=3.7" -summary = "Core utilities for Python packages" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "paginate" -version = "0.5.6" -summary = "Divides large result sets into pages for easier browsing" -files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, -] - -[[package]] -name = "pathspec" -version = "0.11.2" -requires_python = ">=3.7" -summary = "Utility library for gitignore style pattern matching of file paths." -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] - [[package]] name = "pillow" version = "9.5.0" @@ -1017,16 +125,6 @@ files = [ {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, ] -[[package]] -name = "platformdirs" -version = "3.11.0" -requires_python = ">=3.7" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, -] - [[package]] name = "pycairo" version = "1.25.0" @@ -1050,16 +148,6 @@ files = [ {file = "pycairo-1.25.0.tar.gz", hash = "sha256:37842b9bfa6339c45a5025f752e1d78d5840b1a0f82303bdd5610846ad8b5c4f"}, ] -[[package]] -name = "pycparser" -version = "2.21" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -summary = "C parser in Python" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - [[package]] name = "pydantic" version = "2.4.2" @@ -1180,193 +268,6 @@ files = [ {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, ] -[[package]] -name = "pygments" -version = "2.16.1" -requires_python = ">=3.7" -summary = "Pygments is a syntax highlighting package written in Python." -files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, -] - -[[package]] -name = "pymdown-extensions" -version = "10.3" -requires_python = ">=3.8" -summary = "Extension pack for Python Markdown." -dependencies = [ - "markdown>=3.2", - "pyyaml", -] -files = [ - {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, - {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, -] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[[package]] -name = "pytz" -version = "2022.7.1" -summary = "World timezone definitions, modern and historical" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -requires_python = ">=3.6" -summary = "YAML parser and emitter for Python" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -requires_python = ">=3.6" -summary = "A custom YAML tag for referencing environment variables in YAML files. " -dependencies = [ - "pyyaml", -] -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] - -[[package]] -name = "regex" -version = "2023.10.3" -requires_python = ">=3.7" -summary = "Alternative regular expression module, to replace re." -files = [ - {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, - {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55"}, - {file = "regex-2023.10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a"}, - {file = "regex-2023.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a"}, - {file = "regex-2023.10.3-cp310-cp310-win32.whl", hash = "sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec"}, - {file = "regex-2023.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e"}, - {file = "regex-2023.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac"}, - {file = "regex-2023.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6"}, - {file = "regex-2023.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54"}, - {file = "regex-2023.10.3-cp311-cp311-win32.whl", hash = "sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2"}, - {file = "regex-2023.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037"}, - {file = "regex-2023.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841"}, - {file = "regex-2023.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292"}, - {file = "regex-2023.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a"}, - {file = "regex-2023.10.3-cp312-cp312-win32.whl", hash = "sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a"}, - {file = "regex-2023.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533"}, - {file = "regex-2023.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07"}, - {file = "regex-2023.10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711"}, - {file = "regex-2023.10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4"}, - {file = "regex-2023.10.3-cp38-cp38-win32.whl", hash = "sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d"}, - {file = "regex-2023.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af"}, - {file = "regex-2023.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b"}, - {file = "regex-2023.10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca"}, - {file = "regex-2023.10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48"}, - {file = "regex-2023.10.3-cp39-cp39-win32.whl", hash = "sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd"}, - {file = "regex-2023.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988"}, - {file = "regex-2023.10.3.tar.gz", hash = "sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f"}, -] - [[package]] name = "reportlab" version = "4.0.6" @@ -1396,37 +297,6 @@ files = [ {file = "reportlab-4.0.6.tar.gz", hash = "sha256:069aa35da7c882921f419f6e26327e14dac1d9d0adeb40b584cdadd974d99fc0"}, ] -[[package]] -name = "requests" -version = "2.31.0" -requires_python = ">=3.7" -summary = "Python HTTP for Humans." -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, -] - -[[package]] -name = "rich" -version = "13.6.0" -requires_python = ">=3.7.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, -] - [[package]] name = "rlpycairo" version = "0.3.0" @@ -1440,69 +310,6 @@ files = [ {file = "rlPyCairo-0.3.0-py3-none-any.whl", hash = "sha256:f6ec712a76914f78c1491351e1d79eeb1a40d072accf5d36075e399963c17b17"}, ] -[[package]] -name = "setuptools" -version = "68.2.2" -requires_python = ">=3.8" -summary = "Easily download, build, install, upgrade, and uninstall Python packages" -files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, -] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "smmap" -version = "5.0.1" -requires_python = ">=3.7" -summary = "A pure Python implementation of a sliding window memory map manager" -files = [ - {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, - {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, -] - -[[package]] -name = "sniffio" -version = "1.3.0" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -requires_python = ">=3.8" -summary = "A modern CSS selector implementation for Beautiful Soup." -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "tinycss2" -version = "1.2.1" -requires_python = ">=3.7" -summary = "A tiny CSS parser" -dependencies = [ - "webencodings>=0.4", -] -files = [ - {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"}, - {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"}, -] - [[package]] name = "typing-extensions" version = "4.8.0" @@ -1513,78 +320,6 @@ files = [ {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] -[[package]] -name = "tzdata" -version = "2023.3" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, -] - -[[package]] -name = "urllib3" -version = "2.0.6" -requires_python = ">=3.7" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -files = [ - {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, - {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, -] - -[[package]] -name = "verspec" -version = "0.1.0" -summary = "Flexible version handling" -files = [ - {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, - {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, -] - -[[package]] -name = "watchdog" -version = "3.0.0" -requires_python = ">=3.7" -summary = "Filesystem events monitoring" -files = [ - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, - {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9"}, - {file = "watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc"}, - {file = "watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0"}, - {file = "watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8"}, - {file = "watchdog-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100"}, - {file = "watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346"}, - {file = "watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d"}, - {file = "watchdog-3.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33"}, - {file = "watchdog-3.0.0-py3-none-win32.whl", hash = "sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f"}, - {file = "watchdog-3.0.0-py3-none-win_amd64.whl", hash = "sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c"}, - {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, - {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -summary = "Character encoding aliases for legacy web content" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - [[package]] name = "win32-setctime" version = "1.1.0" @@ -1594,13 +329,3 @@ files = [ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] - -[[package]] -name = "zipp" -version = "3.17.0" -requires_python = ">=3.8" -summary = "Backport of pathlib-compatible object wrapper for zip files" -files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, -] diff --git a/pyproject.toml b/pyproject.toml index 20f6c671..a7f20ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,13 @@ name = "infini" version = "1.0.4" description = "The `Core` of `HydroRoll`, the `Loader` of your rules packages." authors = [{ name = "简律纯", email = "i@jyunko.cn" }] -dependencies = [ +dependencies = ["reportlab[pycairo]>=4.0.5", "pydantic>=2.4.2", "loguru>=0.7.2"] +requires-python = ">=3.8" +readme = "README.md" +license = { text = "MIT" } + +[tool.pdm.dev-dependencies] +dev = [ "mkdocs>=1.5.3", "mkdocs-material[imaging]>=9.4.4", "mkdocs-blog-plugin>=0.25", @@ -20,15 +26,7 @@ dependencies = [ "mkdocs-redirects>=1.2.1", "mike>=1.1.2", "mkdocstrings>=0.23.0", - "distribute>=0.7.3", - "reportlab[pycairo]>=4.0.5", - "pydantic>=2.4.2", - "loguru>=0.7.2", ] -requires-python = ">=3.8" -readme = "README.md" -license = { text = "MIT" } - [project.scripts] infini = "infini.cli:Cli.parse_args" @@ -37,4 +35,3 @@ ifi = "infini.cli:Cli.parse_args" [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d074e98d..00000000 --- a/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -mkdocs -mkdocs-material -mkdocs-blog-plugin -mkdocs-git-revision-date-localized-plugin>=1.0 -mkdocs-git-authors-plugin -mkdocs-material-extensions>=1.1 -neoteroi-mkdocs -pillow<11 -cairosvg>=2.5 -mkdocs-git-committers-plugin-2>=1.1.1 -lxml -mkdocs-minify-plugin>=0.3 -mkdocs-rss-plugin>=1.1 -mkdocs-redirects>=1.0 diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 3119f231..00000000 --- a/src/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -logs/ -test/ \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index f875ee63..00000000 --- a/src/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -__version__ = "$md-version$" diff --git a/src/extensions/__init__.py b/src/extensions/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/extensions/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/extensions/emoji.py b/src/extensions/emoji.py deleted file mode 100644 index c8c955cc..00000000 --- a/src/extensions/emoji.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -import codecs -import functools -import material -import os - -from glob import iglob -from inspect import getfile -from markdown import Markdown -from pymdownx import emoji, twemoji_db -from xml.etree.ElementTree import Element - -# ----------------------------------------------------------------------------- -# Functions -# ----------------------------------------------------------------------------- - -# Create twemoji index -def twemoji(options: object, md: Markdown): - paths = options.get("custom_icons", [])[:] - return _load_twemoji_index(tuple(paths)) - -# Create emoji or icon -def to_svg( - index: str, shortname: str, alias: str, uc: str | None, alt: str, - title: str, category: str, options: object, md: Markdown -): - if not uc: - icons = md.inlinePatterns["emoji"].emoji_index["emoji"] - - # Create and return element to host icon - el = Element("span", { "class": options.get("classes", index) }) - el.text = md.htmlStash.store(_load(icons[shortname]["path"])) - return el - - # Delegate to `pymdownx.emoji` extension - return emoji.to_svg( - index, shortname, alias, uc, alt, title, category, options, md - ) - -# ----------------------------------------------------------------------------- -# Helper functions -# ----------------------------------------------------------------------------- - -# Load icon -@functools.lru_cache(maxsize = None) -def _load(file: str): - with codecs.open(file, encoding = "utf-8") as f: - return f.read() - -# Load twemoji index and add icons -@functools.lru_cache(maxsize = None) -def _load_twemoji_index(paths): - index = { - "name": "twemoji", - "emoji": twemoji_db.emoji, - "aliases": twemoji_db.aliases - } - - # Compute path to theme root and traverse all icon directories - root = os.path.dirname(getfile(material)) - root = os.path.join(root, "templates", ".icons") - for path in [*paths, root]: - base = os.path.normpath(path) - - # Index icons provided by the theme and via custom icons - glob = os.path.join(base, "**", "*.svg") - glob = iglob(os.path.normpath(glob), recursive = True) - for file in glob: - icon = file[len(base) + 1:-4].replace(os.path.sep, "-") - - # Add icon to index - name = f":{icon}:" - if not any(name in index[key] for key in ["emoji", "aliases"]): - index["emoji"][name] = { "name": name, "path": file } - - # Return index - return index diff --git a/src/hydrorollcore/__init__.py b/src/hydrorollcore/__init__.py deleted file mode 100644 index 9c88b4f9..00000000 --- a/src/hydrorollcore/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from infini.cli import Cli -from infini.rule import Rule, Result, Dice -from infini.typing import Config as ConfigTyping -from infini.event import Event - -__all__ = ["Rule", "Cli", "Result", "Dice", "Event", "ConfigTyping"] diff --git a/src/hydrorollcore/cli.py b/src/hydrorollcore/cli.py deleted file mode 100644 index e372b4b4..00000000 --- a/src/hydrorollcore/cli.py +++ /dev/null @@ -1,45 +0,0 @@ -from pathlib import Path -from .consts import templates -from .logging import logger - -import argparse -import os -import sys -import importlib - - -class Cli: - def parse_args(self, argv: list[str] | None = None): - parser = argparse.ArgumentParser(description="HydroRoll 命令行工具") - - parser.add_argument("--new", action="store_true", help="创建一个 HydroRoll 规则包模板") - parser.add_argument("--run", action="store_true", help="运行 HydroRoll 规则包") - parser.add_argument("--gui", action="store_true", help="显示弹窗") - parser.add_argument("--path", help="指定路径") - - args = parser.parse_args(argv if argv else sys.argv[1:]) - - if args.gui: - logger.critical("选项[--gui]尚未被支持!") - sys.exit(1) - - path = Path(args.path).resolve() if args.path else Path(os.getcwd()).resolve() - if args.new and args.run: - logger.error("无法确定的指令要求: 你同时指定了new与run指令。") - sys.exit(1) - - if args.new: - if path.exists(): - logger.error("指定的文件夹已经存在!") - sys.exit(1) - - path.mkdir(parents=True, exist_ok=True) - (path / "rule.py").write_text(templates.RULE, encoding="utf-8") - (path / "event.py").write_text(templates.EVENT, encoding="utf-8") - (path / "dice.py").write_text(templates.DICE, encoding="utf-8") - - logger.success("HydroRoll 规则包模板已创建!") - - if args.run: - sys.path.append(str(path)) - importlib.import_module("event") diff --git a/src/hydrorollcore/consts/__init__.py b/src/hydrorollcore/consts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/hydrorollcore/consts/templates.py b/src/hydrorollcore/consts/templates.py deleted file mode 100644 index e2edc2d6..00000000 --- a/src/hydrorollcore/consts/templates.py +++ /dev/null @@ -1,69 +0,0 @@ -RULE = """from infini import Rule, Result, Dice - - -class MyRule(Rule): - \"\"\"自设规则包\"\"\" - - name = "MyRule" - priority: int = 0 - - def __init__(self) -> None: - \"\"\"初始化你的规则包\"\"\" - - def check(self, dice: Dice) -> Result: - \"\"\"声明规则包检定方式\"\"\" - return Result("myevent.event1", True) -""" - -EVENT = """from infini import Event - -__events__ = ["MyEvent"] - - -class MyEvent(Event): - name = "event1" - output = "检定成功!" -""" - -DICE = """from infini import Dice - -import random -import re - - -class BaseDice(Dice): - \"\"\"多面骰\"\"\" - - def __init__(self, roll_string: str = "") -> None: - self.roll_string = roll_string - self.parse() - - def parse(self) -> "Dice": - self.dices = [] - split = re.split(r"[dD]", self.roll_string) - - if split[0]: - self.a = int(split[0]) - else: - self.a = 1 - - if split[1]: - self.b = int(split[1]) - else: - self.b = 100 - - self.db = f"{self.a}D{self.b}" - self.dices += [f"D{self.b}"] * self.a - return self - - def roll(self) -> int: - self.results = [] - - for _ in range(self.a): - result = random.randint(1, self.b) - - self.results.append(result) - - self.outcome = sum(self.results) - return self.outcome -""" diff --git a/src/hydrorollcore/event.py b/src/hydrorollcore/event.py deleted file mode 100644 index 9ce34bd9..00000000 --- a/src/hydrorollcore/event.py +++ /dev/null @@ -1,43 +0,0 @@ -from .typing import Dict -from .logging import logger - -import re - -__all__ = ["Event", "events"] - - -class Events: - """事件集合""" - - _events: Dict[str, str] = {} - - def regist(self, name: str, output: str) -> None: - self._events[name.lower()] = output - - def process(self, name: str, **kwargs) -> str: - if string := self._events.get(name.lower()): - return self._format(string, **kwargs) - logger.warning(f"事件[{name.lower()}]不存在,将返回空字符串!") - return "" - - def _format(self, string: str, **kwargs): - pattern = r"{(.*?)}" - values = re.findall(pattern, string) - for value in values: - kwarg = kwargs.get(value) - value = kwarg if kwarg else "" - string = re.sub(pattern, value, string) - return string - - -class Event: - """事件基类""" - - name: str - output: str - - def __init_subclass__(cls) -> None: - events.regist(cls.name, cls.output) - - -events = Events() diff --git a/src/hydrorollcore/exceptions.py b/src/hydrorollcore/exceptions.py deleted file mode 100644 index 62c88fa1..00000000 --- a/src/hydrorollcore/exceptions.py +++ /dev/null @@ -1,6 +0,0 @@ -class HydroError(Exception): - """HydroRoll 异常基类""" - - -class RuleLoadError(HydroError): - """规则导入错误""" diff --git a/src/hydrorollcore/logging.py b/src/hydrorollcore/logging.py deleted file mode 100644 index a1458ef4..00000000 --- a/src/hydrorollcore/logging.py +++ /dev/null @@ -1,40 +0,0 @@ -"""infini 日志。 - -infini 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。 -自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。 -""" -from datetime import datetime -from multilogging import multilogger -from pathlib import Path -from .settings import DEBUG - - -__all__ = ["logger", "error_or_exception"] - -logger = multilogger( - name="HydroRoll", payload="Core", level="DEBUG" if DEBUG else "INFO" -) -current_path = Path(__file__).resolve().parent -LOG_PATH = current_path / "logs" -if not LOG_PATH.exists(): - LOG_PATH.mkdir(parents=True, exist_ok=True) -logger.add( - sink=LOG_PATH / (datetime.now().strftime("%Y-%m-%d") + ".log"), - level="INFO", - rotation="10 MB", -) # 每个日志文件最大为 10MB - - -def error_or_exception(message: str, exception: Exception, verbose: bool = True): - # 弃用的方法 - # logger.remove() - # logger.add( - # sys.stderr, - # format="{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}] > {name}:{function}:{line} - {message}", - # ) - - if verbose: - logger.exception(exception) - logger.critical(message) - else: - logger.critical(f"{message} {exception!r}") diff --git a/src/hydrorollcore/manager.py b/src/hydrorollcore/manager.py deleted file mode 100644 index 42e79563..00000000 --- a/src/hydrorollcore/manager.py +++ /dev/null @@ -1,18 +0,0 @@ -from .event import Events, events -from .logging import logger -from .typing import Dict - - -class Manager: - """事件处理单元""" - - events: Events - - def __init__(self, _events: Events = None) -> None: - self.events = _events if _events else events - - def roll(self): - ... - - -manager = Manager() diff --git a/src/hydrorollcore/rule.py b/src/hydrorollcore/rule.py deleted file mode 100644 index a1f04151..00000000 --- a/src/hydrorollcore/rule.py +++ /dev/null @@ -1,75 +0,0 @@ -from abc import ABCMeta, abstractmethod -from enum import Enum -from .exceptions import HydroError -from .typing import Dict - -__all__ = ["RuleLoadType", "Result", "Dice", "Rule"] - - -class RuleLoadType(Enum): - """The Type Of Rules To Be Loaded""" - - DIR = "dir" - NAME = "name" - FILE = "file" - CLASS = "class" - - -class Result(metaclass=ABCMeta): - """规则检定结果基类""" - - event: str - status: bool - exception: HydroError | None = None - - def __init__(self, event: str, status: bool, exception: HydroError | None) -> None: - self.event = event - self.status = status - self.exception = exception - - def ok(self): - """规则执行期间是否产生异常""" - return isinstance(self.exception, HydroError) - - -class Dice(metaclass=ABCMeta): - """掷骰基类""" - - roll_string: str - db: str - outcome: int - - def __repr__(self) -> str: - return f'' - - def __str__(self) -> str: - return self.db.upper() - - def __int__(self) -> int: - return self.outcome - - @abstractmethod - def parse(self) -> "Dice": - """解析传入的掷骰字符串`roll_string`,返回一个`Dice`对象""" - raise NotImplementedError - - @abstractmethod - def roll(self) -> int: - """掷骰方法,返回掷骰结果""" - raise NotImplementedError - - -class Rule(metaclass=ABCMeta): - """规则基类""" - - name: str - dices: Dict[str, str] = {} - priority: int = 0 - - @abstractmethod - def __init__(self) -> None: - raise NotImplementedError - - @abstractmethod - def check(self, dice: Dice) -> Result: - raise NotImplementedError diff --git a/src/hydrorollcore/settings.py b/src/hydrorollcore/settings.py deleted file mode 100644 index 51c64dd2..00000000 --- a/src/hydrorollcore/settings.py +++ /dev/null @@ -1 +0,0 @@ -DEBUG = True diff --git a/src/hydrorollcore/typing.py b/src/hydrorollcore/typing.py deleted file mode 100644 index 333a763b..00000000 --- a/src/hydrorollcore/typing.py +++ /dev/null @@ -1,41 +0,0 @@ -from pydantic import BaseModel -from typing import ( - Dict as Dict, - TYPE_CHECKING as TYPE_CHECKING, - TypeVar as TypeVar, - Callable as Callable, - NoReturn as NoReturn, - Awaitable as Awaitable, -) - - -class Config(BaseModel): - rule_dir: list = [] - rules: list = [] - - -class DiceConfig(BaseModel): - sides: int - counts: int - init_dice_pool: int - - -class PlayerCard(BaseModel): - name: str - traits: list - - -class Bonus(BaseModel): - level: int - cost: int - - -class WikiPage(BaseModel): - title: str - content: str - tags: list = [] - - -class WikiModel: - class Setting(BaseModel): - desc: str diff --git a/src/infini/__init__.py b/src/infini/__init__.py new file mode 100644 index 00000000..9c88b4f9 --- /dev/null +++ b/src/infini/__init__.py @@ -0,0 +1,6 @@ +from infini.cli import Cli +from infini.rule import Rule, Result, Dice +from infini.typing import Config as ConfigTyping +from infini.event import Event + +__all__ = ["Rule", "Cli", "Result", "Dice", "Event", "ConfigTyping"] diff --git a/src/infini/cli.py b/src/infini/cli.py new file mode 100644 index 00000000..e372b4b4 --- /dev/null +++ b/src/infini/cli.py @@ -0,0 +1,45 @@ +from pathlib import Path +from .consts import templates +from .logging import logger + +import argparse +import os +import sys +import importlib + + +class Cli: + def parse_args(self, argv: list[str] | None = None): + parser = argparse.ArgumentParser(description="HydroRoll 命令行工具") + + parser.add_argument("--new", action="store_true", help="创建一个 HydroRoll 规则包模板") + parser.add_argument("--run", action="store_true", help="运行 HydroRoll 规则包") + parser.add_argument("--gui", action="store_true", help="显示弹窗") + parser.add_argument("--path", help="指定路径") + + args = parser.parse_args(argv if argv else sys.argv[1:]) + + if args.gui: + logger.critical("选项[--gui]尚未被支持!") + sys.exit(1) + + path = Path(args.path).resolve() if args.path else Path(os.getcwd()).resolve() + if args.new and args.run: + logger.error("无法确定的指令要求: 你同时指定了new与run指令。") + sys.exit(1) + + if args.new: + if path.exists(): + logger.error("指定的文件夹已经存在!") + sys.exit(1) + + path.mkdir(parents=True, exist_ok=True) + (path / "rule.py").write_text(templates.RULE, encoding="utf-8") + (path / "event.py").write_text(templates.EVENT, encoding="utf-8") + (path / "dice.py").write_text(templates.DICE, encoding="utf-8") + + logger.success("HydroRoll 规则包模板已创建!") + + if args.run: + sys.path.append(str(path)) + importlib.import_module("event") diff --git a/src/infini/consts/__init__.py b/src/infini/consts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/infini/consts/templates.py b/src/infini/consts/templates.py new file mode 100644 index 00000000..e2edc2d6 --- /dev/null +++ b/src/infini/consts/templates.py @@ -0,0 +1,69 @@ +RULE = """from infini import Rule, Result, Dice + + +class MyRule(Rule): + \"\"\"自设规则包\"\"\" + + name = "MyRule" + priority: int = 0 + + def __init__(self) -> None: + \"\"\"初始化你的规则包\"\"\" + + def check(self, dice: Dice) -> Result: + \"\"\"声明规则包检定方式\"\"\" + return Result("myevent.event1", True) +""" + +EVENT = """from infini import Event + +__events__ = ["MyEvent"] + + +class MyEvent(Event): + name = "event1" + output = "检定成功!" +""" + +DICE = """from infini import Dice + +import random +import re + + +class BaseDice(Dice): + \"\"\"多面骰\"\"\" + + def __init__(self, roll_string: str = "") -> None: + self.roll_string = roll_string + self.parse() + + def parse(self) -> "Dice": + self.dices = [] + split = re.split(r"[dD]", self.roll_string) + + if split[0]: + self.a = int(split[0]) + else: + self.a = 1 + + if split[1]: + self.b = int(split[1]) + else: + self.b = 100 + + self.db = f"{self.a}D{self.b}" + self.dices += [f"D{self.b}"] * self.a + return self + + def roll(self) -> int: + self.results = [] + + for _ in range(self.a): + result = random.randint(1, self.b) + + self.results.append(result) + + self.outcome = sum(self.results) + return self.outcome +""" diff --git a/src/infini/event.py b/src/infini/event.py new file mode 100644 index 00000000..9ce34bd9 --- /dev/null +++ b/src/infini/event.py @@ -0,0 +1,43 @@ +from .typing import Dict +from .logging import logger + +import re + +__all__ = ["Event", "events"] + + +class Events: + """事件集合""" + + _events: Dict[str, str] = {} + + def regist(self, name: str, output: str) -> None: + self._events[name.lower()] = output + + def process(self, name: str, **kwargs) -> str: + if string := self._events.get(name.lower()): + return self._format(string, **kwargs) + logger.warning(f"事件[{name.lower()}]不存在,将返回空字符串!") + return "" + + def _format(self, string: str, **kwargs): + pattern = r"{(.*?)}" + values = re.findall(pattern, string) + for value in values: + kwarg = kwargs.get(value) + value = kwarg if kwarg else "" + string = re.sub(pattern, value, string) + return string + + +class Event: + """事件基类""" + + name: str + output: str + + def __init_subclass__(cls) -> None: + events.regist(cls.name, cls.output) + + +events = Events() diff --git a/src/infini/exceptions.py b/src/infini/exceptions.py new file mode 100644 index 00000000..62c88fa1 --- /dev/null +++ b/src/infini/exceptions.py @@ -0,0 +1,6 @@ +class HydroError(Exception): + """HydroRoll 异常基类""" + + +class RuleLoadError(HydroError): + """规则导入错误""" diff --git a/src/infini/logging.py b/src/infini/logging.py new file mode 100644 index 00000000..a1458ef4 --- /dev/null +++ b/src/infini/logging.py @@ -0,0 +1,40 @@ +"""infini 日志。 + +infini 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。 +自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。 +""" +from datetime import datetime +from multilogging import multilogger +from pathlib import Path +from .settings import DEBUG + + +__all__ = ["logger", "error_or_exception"] + +logger = multilogger( + name="HydroRoll", payload="Core", level="DEBUG" if DEBUG else "INFO" +) +current_path = Path(__file__).resolve().parent +LOG_PATH = current_path / "logs" +if not LOG_PATH.exists(): + LOG_PATH.mkdir(parents=True, exist_ok=True) +logger.add( + sink=LOG_PATH / (datetime.now().strftime("%Y-%m-%d") + ".log"), + level="INFO", + rotation="10 MB", +) # 每个日志文件最大为 10MB + + +def error_or_exception(message: str, exception: Exception, verbose: bool = True): + # 弃用的方法 + # logger.remove() + # logger.add( + # sys.stderr, + # format="{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}] > {name}:{function}:{line} - {message}", + # ) + + if verbose: + logger.exception(exception) + logger.critical(message) + else: + logger.critical(f"{message} {exception!r}") diff --git a/src/infini/manager.py b/src/infini/manager.py new file mode 100644 index 00000000..42e79563 --- /dev/null +++ b/src/infini/manager.py @@ -0,0 +1,18 @@ +from .event import Events, events +from .logging import logger +from .typing import Dict + + +class Manager: + """事件处理单元""" + + events: Events + + def __init__(self, _events: Events = None) -> None: + self.events = _events if _events else events + + def roll(self): + ... + + +manager = Manager() diff --git a/src/infini/rule.py b/src/infini/rule.py new file mode 100644 index 00000000..a1f04151 --- /dev/null +++ b/src/infini/rule.py @@ -0,0 +1,75 @@ +from abc import ABCMeta, abstractmethod +from enum import Enum +from .exceptions import HydroError +from .typing import Dict + +__all__ = ["RuleLoadType", "Result", "Dice", "Rule"] + + +class RuleLoadType(Enum): + """The Type Of Rules To Be Loaded""" + + DIR = "dir" + NAME = "name" + FILE = "file" + CLASS = "class" + + +class Result(metaclass=ABCMeta): + """规则检定结果基类""" + + event: str + status: bool + exception: HydroError | None = None + + def __init__(self, event: str, status: bool, exception: HydroError | None) -> None: + self.event = event + self.status = status + self.exception = exception + + def ok(self): + """规则执行期间是否产生异常""" + return isinstance(self.exception, HydroError) + + +class Dice(metaclass=ABCMeta): + """掷骰基类""" + + roll_string: str + db: str + outcome: int + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + return self.db.upper() + + def __int__(self) -> int: + return self.outcome + + @abstractmethod + def parse(self) -> "Dice": + """解析传入的掷骰字符串`roll_string`,返回一个`Dice`对象""" + raise NotImplementedError + + @abstractmethod + def roll(self) -> int: + """掷骰方法,返回掷骰结果""" + raise NotImplementedError + + +class Rule(metaclass=ABCMeta): + """规则基类""" + + name: str + dices: Dict[str, str] = {} + priority: int = 0 + + @abstractmethod + def __init__(self) -> None: + raise NotImplementedError + + @abstractmethod + def check(self, dice: Dice) -> Result: + raise NotImplementedError diff --git a/src/infini/settings.py b/src/infini/settings.py new file mode 100644 index 00000000..51c64dd2 --- /dev/null +++ b/src/infini/settings.py @@ -0,0 +1 @@ +DEBUG = True diff --git a/src/infini/typing.py b/src/infini/typing.py new file mode 100644 index 00000000..333a763b --- /dev/null +++ b/src/infini/typing.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import ( + Dict as Dict, + TYPE_CHECKING as TYPE_CHECKING, + TypeVar as TypeVar, + Callable as Callable, + NoReturn as NoReturn, + Awaitable as Awaitable, +) + + +class Config(BaseModel): + rule_dir: list = [] + rules: list = [] + + +class DiceConfig(BaseModel): + sides: int + counts: int + init_dice_pool: int + + +class PlayerCard(BaseModel): + name: str + traits: list + + +class Bonus(BaseModel): + level: int + cost: int + + +class WikiPage(BaseModel): + title: str + content: str + tags: list = [] + + +class WikiModel: + class Setting(BaseModel): + desc: str diff --git a/src/overrides/assets/javascripts/components/_/index.ts b/src/overrides/assets/javascripts/components/_/index.ts deleted file mode 100644 index 3cb4c18e..00000000 --- a/src/overrides/assets/javascripts/components/_/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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( - 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( - 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 deleted file mode 100644 index f509a6f9..00000000 --- a/src/overrides/assets/javascripts/components/iconsearch/_/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* 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> { - const config = configuration() - const index$ = requestJSON( - new URL("assets/javascripts/iconsearch_index.json", config.base) - ) - - /* Retrieve query and result components */ - const query = getComponentElement("iconsearch-query", el) - const result = getComponentElement("iconsearch-result", el) - - /* Create and return component */ - const query$ = mountIconSearchQuery(query) - const result$ = mountIconSearchResult(result, { index$, query$ }) - return merge(query$, result$) -} diff --git a/src/overrides/assets/javascripts/components/iconsearch/index.ts b/src/overrides/assets/javascripts/components/iconsearch/index.ts deleted file mode 100644 index 9d856774..00000000 --- a/src/overrides/assets/javascripts/components/iconsearch/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 deleted file mode 100644 index 03a3daad..00000000 --- a/src/overrides/assets/javascripts/components/iconsearch/query/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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> { - - /* 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 deleted file mode 100644 index 2b9d97fb..00000000 --- a/src/overrides/assets/javascripts/components/iconsearch/result/index.ts +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* Search index observable */ - query$: Observable /* Search query observable */ -} - -/** - * Mount options - */ -interface MountOptions { - index$: Observable /* Search index observable */ - query$: Observable /* 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 { - 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(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(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> { - const push$ = new Subject() - 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 deleted file mode 100644 index ec6c9dce..00000000 --- a/src/overrides/assets/javascripts/components/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 deleted file mode 100644 index 711f423a..00000000 --- a/src/overrides/assets/javascripts/components/sponsorship/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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> { - const sponsorship$ = requestJSON( - "https://3if8u9o552.execute-api.us-east-1.amazonaws.com/_/" - ) - - /* Retrieve adjacent components */ - const count = getComponentElement("sponsorship-count") - const total = getComponentElement("sponsorship-total") - - /* Render sponsorship */ - sponsorship$.subscribe(sponsorship => { - el.removeAttribute("hidden") - - /* Render public sponsors with avatar and links */ - const list = getElement(":scope > :first-child", el) - for (const sponsor of sponsorship.sponsors) - if (sponsor.type === "public") - list.appendChild(renderPublicSponsor(sponsor.user)) - - /* Render combined private sponsors */ - list.appendChild(renderPrivateSponsor( - sponsorship.sponsors.filter(({ type }) => ( - type === "private" - )).length - )) - - /* Render sponsorship count and total */ - count.innerText = `${sponsorship.sponsors.length}` - total.innerText = `$ ${sponsorship.total - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ",") - } a month` - }) - - // /* Create and return component */ - return sponsorship$ - .pipe( - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/overrides/assets/javascripts/custom.ts b/src/overrides/assets/javascripts/custom.ts deleted file mode 100644 index 7c3c3847..00000000 --- a/src/overrides/assets/javascripts/custom.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { merge, switchMap } from "rxjs" - -import { - getComponentElements, - mountIconSearch, - mountSponsorship -} from "./components" -import { setupAnalytics } from "./integrations" - -/* ---------------------------------------------------------------------------- - * Application - * ------------------------------------------------------------------------- */ - -/* Set up extra analytics events */ -setupAnalytics() - -/* Set up extra component observables */ -const component$ = document$ - .pipe( - switchMap(() => merge( - - /* Icon search */ - ...getComponentElements("iconsearch") - .map(el => mountIconSearch(el)), - - /* Sponsorship */ - ...getComponentElements("sponsorship") - .map(el => mountSponsorship(el)) - )) - ) - -/* Subscribe to all components */ -component$.subscribe() diff --git a/src/overrides/assets/javascripts/integrations/analytics/index.ts b/src/overrides/assets/javascripts/integrations/analytics/index.ts deleted file mode 100644 index 658add2a..00000000 --- a/src/overrides/assets/javascripts/integrations/analytics/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 deleted file mode 100644 index 9179f2a2..00000000 --- a/src/overrides/assets/javascripts/integrations/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 deleted file mode 100644 index 13cafa6d..00000000 --- a/src/overrides/assets/javascripts/templates/iconsearch/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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: "", - tagClose: "" - } - }) -} - -/* ---------------------------------------------------------------------------- - * 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 ( -
  • - - - - -
  • - ) -} diff --git a/src/overrides/assets/javascripts/templates/index.ts b/src/overrides/assets/javascripts/templates/index.ts deleted file mode 100644 index 02376b3d..00000000 --- a/src/overrides/assets/javascripts/templates/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 deleted file mode 100644 index 7891c2e0..00000000 --- a/src/overrides/assets/javascripts/templates/sponsorship/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( - - - - ) -} - -/** - * Render private sponsor - * - * @param count - Number of private sponsors - * - * @returns Element - */ -export function renderPrivateSponsor( - count: number -): HTMLElement { - return ( - - +{count} - - ) -} diff --git a/src/overrides/assets/stylesheets/custom.scss b/src/overrides/assets/stylesheets/custom.scss deleted file mode 100644 index 8235e7d0..00000000 --- a/src/overrides/assets/stylesheets/custom.scss +++ /dev/null @@ -1,44 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Dependencies -// ---------------------------------------------------------------------------- - -@import "material-color"; -@import "material-shadows"; - -// ---------------------------------------------------------------------------- -// Local imports -// ---------------------------------------------------------------------------- - -@import "utilities/break"; -@import "utilities/convert"; - -@import "config"; - -@import "custom/typeset"; - -@import "custom/layout/banner"; -@import "custom/layout/hero"; -@import "custom/layout/iconsearch"; -@import "custom/layout/sponsorship"; diff --git a/src/overrides/assets/stylesheets/custom/_typeset.scss b/src/overrides/assets/stylesheets/custom/_typeset.scss deleted file mode 100644 index bef30073..00000000 --- a/src/overrides/assets/stylesheets/custom/_typeset.scss +++ /dev/null @@ -1,294 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Keyframes -// ---------------------------------------------------------------------------- - -// Pumping heart animation -@keyframes heart { - 0%, - 40%, - 80%, - 100% { - transform: scale(1); - } - - 20%, - 60% { - transform: scale(1.15); - } -} - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Twitter icon - .twitter { - color: #00acee; - } - - // Mastodon icon - it's not the exact brand color, because that doesn't work - // well on dark backgrounds, so we lightened it up a bit. - .mastodon { - color: #897ff8; - } - - // Insiders video - .mdx-video { - width: auto; - - // Insiders video container - &__inner { - position: relative; - width: 100%; - height: 0; - padding-bottom: 56.138%; - } - - // Insiders video iframe - iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: hidden; - border: none; - } - } - - // Pumping heart - .mdx-heart { - animation: heart 1000ms infinite; - } - - // Insiders color (for links, etc.) // remove - .mdx-insiders { - color: $clr-pink-500; - } - - // BETA ##################################################################### - - // Badge - .mdx-badge { - font-size: 0.85em; - - // Badge with heart - &--heart { - --md-typeset-a-color: hsla(#{hex2hsl($clr-pink-500)}, 1); - --md-accent-fg-color: hsla(#{hex2hsl($clr-pink-a200)}, 1); - --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-pink-500)}, 0.1); - - // Animate icon - .twemoji { - animation: heart 1000ms infinite; - } - } - - // Badge moved to the right - &--right { - float: right; - margin-left: 0.35em; - } - - // Badge icon - &__icon { - padding: px2rem(4px); - background: var(--md-accent-fg-color--transparent); - border-start-start-radius: px2rem(2px); - border-end-start-radius: px2rem(2px); - - // If icon is alone, round corners - &:last-child { - border-radius: px2rem(2px); - } - } - - // Badge text - &__text { - padding: px2rem(4px) px2rem(6px); - border-start-end-radius: px2rem(2px); - border-end-end-radius: px2rem(2px); - box-shadow: 0 0 0 1px inset var(--md-accent-fg-color--transparent); - } - } - - // BETA ##################################################################### - - // Switch buttons - .mdx-switch button { - cursor: pointer; - transition: opacity 250ms; - - // Button on focus/hover - &:is(:focus, :hover) { - opacity: 0.75; - } - - // Code block - > code { - display: block; - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color); - } - } - - // Two-column layout - .mdx-columns { - - // Column - ol, - ul { - columns: 2; - - // [mobile portrait -]: Reset columns on mobile - @include break-to-device(mobile portrait) { - columns: initial; - } - } - - // Column item - li { - break-inside: avoid; - } - } - - // Language list - .mdx-flags { - margin: 2em auto; - - // Language list - ol { - list-style: none; - - // Language list item - li { - margin-bottom: 1em; - } - } - - // Language item - &__item { - display: flex; - gap: px2rem(12px); - } - - // Language content - &__content { - display: flex; - flex: 1; - flex-direction: column; - - // Language name - span { - display: inline-flex; - align-items: baseline; - justify-content: space-between; - } - - // Language link - > span:nth-child(2) { - font-size: 80%; - } - - // Language code - code { - float: right; - } - } - } - - // Social card - .mdx-social { - position: relative; - height: min(#{px2rem(540px)}, 80vw); - - // Social card image on hover - &:hover .mdx-social__image { - background-color: rgba(228, 228, 228, 0.05); - } - - // Social card layer - &__layer { - position: absolute; - margin-top: px2rem(80px); - transition: 250ms cubic-bezier(0.7, 0, 0.3, 1); - transform-style: preserve-3d; - - // Social card layer on hover - &:hover { - - // Social card label - .mdx-social__label { - opacity: 1; - } - - // Social card image - .mdx-social__image { - background-color: rgba(127, 127, 127, 0.99); - } - - // Hide top layers - ~ .mdx-social__layer { - opacity: 0; - } - } - } - - // Social card image - &__image { - box-shadow: - px2rem(-5px) px2rem(5px) px2rem(10px) - rgba(0, 0, 0, 0.05); - transition: all 250ms; - transform: rotate(-40deg) skew(15deg, 15deg) scale(0.7); - - // Actual image - img { - display: block; - } - } - - // Social card label - &__label { - position: absolute; - display: block; - padding: px2rem(4px) px2rem(8px); - color: var(--md-default-bg-color); - background-color: var(--md-default-fg-color--light); - opacity: 0; - transition: all 250ms; - } - - // Transform on hover - @for $i from 6 through 0 { - &:hover .mdx-social__layer:nth-child(#{$i}) { - transform: translateY(#{($i - 3) * -10}px); - } - } - } -} diff --git a/src/overrides/assets/stylesheets/custom/layout/_banner.scss b/src/overrides/assets/stylesheets/custom/layout/_banner.scss deleted file mode 100644 index b67d7fff..00000000 --- a/src/overrides/assets/stylesheets/custom/layout/_banner.scss +++ /dev/null @@ -1,66 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Banner for announcements and warnings -.md-banner { - color: var(--md-footer-fg-color--lighter); - - // Don't wrap name of blog article - strong { - color: var(--md-footer-fg-color); - white-space: nowrap; - } - - a { - color: var(--md-footer-fg-color); - - &:focus, - &:hover { - color: currentcolor; - - .twemoji { - background-color: var(--md-footer-fg-color); - box-shadow: none; - } - } - } - - .twemoji { - display: inline-block; - width: px2rem(24px); - height: px2rem(24px); - padding: px2rem(5px); - vertical-align: bottom; - border-radius: 100%; - box-shadow: 0 0 0 px2rem(1px) currentcolor inset; - transition: all 250ms; - - svg { - display: block; - max-height: initial; - } - } -} diff --git a/src/overrides/assets/stylesheets/custom/layout/_hero.scss b/src/overrides/assets/stylesheets/custom/layout/_hero.scss deleted file mode 100644 index 428cd37e..00000000 --- a/src/overrides/assets/stylesheets/custom/layout/_hero.scss +++ /dev/null @@ -1,123 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER 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,") 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,") no-repeat bottom, - linear-gradient( - to bottom, - var(--md-primary-fg-color), - hsla(230, 15%, 25%, 1) 99%, - var(--md-default-bg-color) 99% - ); - } -} - -// Landing page hero -.mdx-hero { - margin: 0 px2rem(16px); - color: var(--md-primary-bg-color); - - // Hero headline - h1 { - margin-bottom: px2rem(20px); - font-weight: 700; - color: currentcolor; - - // [mobile portrait -]: Larger hero headline - @include break-to-device(mobile portrait) { - font-size: px2rem(28px); - } - } - - // Hero content - &__content { - padding-bottom: px2rem(120px); - } - - // [tablet landscape +]: Columnar display - @include break-from-device(tablet landscape) { - display: flex; - align-items: stretch; - - // Adjust spacing and set dimensions - &__content { - max-width: px2rem(380px); - padding-bottom: 14vw; - margin-top: px2rem(70px); - } - - // Hero image - &__image { - order: 1; - width: px2rem(760px); - transform: translateX(#{px2rem(80px)}); - } - } - - // [screen +]: Columnar display and adjusted spacing - @include break-from-device(screen) { - - // Hero image - &__image { - transform: translateX(#{px2rem(160px)}); - } - } - - // Button - .md-button { - margin-top: px2rem(10px); - margin-right: px2rem(10px); - color: var(--md-primary-bg-color); - - // Button on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-bg-color); - background-color: var(--md-accent-fg-color); - border-color: var(--md-accent-fg-color); - } - - // Primary button - &--primary { - color: hsla(280, 37%, 48%, 1); - background-color: var(--md-primary-bg-color); - border-color: var(--md-primary-bg-color); - } - } -} diff --git a/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss b/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss deleted file mode 100644 index 651c4135..00000000 --- a/src/overrides/assets/stylesheets/custom/layout/_iconsearch.scss +++ /dev/null @@ -1,136 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Icon search - .mdx-iconsearch { - position: relative; - background-color: var(--md-default-bg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z1); - transition: box-shadow 125ms; - - // Icon search on focus/hover - &:is(:focus-within, :hover) { - box-shadow: var(--md-shadow-z2); - } - - // Icon search input - .md-input { - background: var(--md-default-bg-color); - box-shadow: none; - - // Slate theme, i.e. dark mode - [data-md-color-scheme="slate"] & { - background: var(--md-code-bg-color); - } - } - } - - // Icon search result - .mdx-iconsearch-result { - max-height: 50vh; - overflow-y: auto; - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - touch-action: pan-y; - scrollbar-width: thin; - scrollbar-color: var(--md-default-fg-color--lighter) transparent; - - // Icon search result inside tooltip - .md-tooltip & { - max-height: px2rem(205px); - } - - // Webkit scrollbar - &::-webkit-scrollbar { - width: px2rem(4px); - height: px2rem(4px); - } - - // Webkit scrollbar thumb - &::-webkit-scrollbar-thumb { - background-color: var(--md-default-fg-color--lighter); - - // Webkit scrollbar thumb on hover - &:hover { - background-color: var(--md-accent-fg-color); - } - } - - // Icon search result metadata - &__meta { - position: absolute; - top: px2rem(8px); - right: px2rem(12px); - font-size: px2rem(12.8px); - color: var(--md-default-fg-color--lighter); - } - - // Icon search result list - &__list { - padding: 0; - margin: 0; - // Hack: necessary because of increased specificity due to the PostCSS - // plugin which prefixes this with `[dir=...]` selectors. - margin-inline-start: 0; - list-style: none; - } - - // Icon search result item - &__item { - padding: px2rem(4px) px2rem(12px); - margin: 0; - // Hack: necessary because of increased specificity due to the PostCSS - // plugin which prefixes this with `[dir=...]` selectors. - margin-inline-start: 0; - border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); - - // Omit border on last child - &:last-child { - border-bottom: none; - } - - // Item content - > * { - margin-right: px2rem(12px); - } - - // Set icon dimensions to fit - img { - width: px2rem(18px); - height: px2rem(18px); - - // Slate theme, i.e. dark mode - [data-md-color-scheme="slate"] &[src*="squidfunk"] { - filter: invert(1); /* stylelint-disable-line */ - } - } - } - } -} diff --git a/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss b/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss deleted file mode 100644 index e2b16570..00000000 --- a/src/overrides/assets/stylesheets/custom/layout/_sponsorship.scss +++ /dev/null @@ -1,128 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Premium sponsors - .mdx-premium { - - // Paragraphs - p { - margin: 2em 0; - text-align: center; - } - - // Premium sponsor image - img { - height: px2rem(65px); - } - - // Premium sponsor list - p:last-child { - display: flex; - flex-wrap: wrap; - justify-content: center; - - // Premium sponsor link - > a { - display: block; - flex-shrink: 0; - } - } - } - - // Sponsorship - .mdx-sponsorship { - - // Sponsorship list - &__list { - margin: 2em 0; - - // Clearfix, because we can't use overflow: auto - &::after { - display: block; - clear: both; - content: ""; - } - } - - // Sponsorship item - &__item { - display: block; - float: inline-start; - width: px2rem(32px); - height: px2rem(32px); - margin: px2rem(4px); - overflow: hidden; - border-radius: 100%; - transition: - color 125ms, - transform 125ms; - transform: scale(1); - - // Sponsor item on focus/hover - &:is(:focus, :hover) { - transform: scale(1.1); - - // Sponsor avatar - img { - filter: grayscale(0%); - } - } - - // Private sponsor - &--private { - font-size: px2rem(12px); - font-weight: 700; - line-height: px2rem(32px); - color: var(--md-default-fg-color--lighter); - text-align: center; - background: var(--md-default-fg-color--lightest); - } - - // Sponsor avatar - img { - display: block; - width: 100%; - height: auto; - filter: grayscale(100%) opacity(75%); - transition: filter 125ms; - } - } - } - - // Sponsorship button - .mdx-sponsorship-button { - font-weight: 400; - } - - // Sponsorship count and total - .mdx-sponsorship-count, - .mdx-sponsorship-total { - font-weight: 700; - } -} diff --git a/src/overrides/home.html b/src/overrides/home.html deleted file mode 100644 index 3f54ca82..00000000 --- a/src/overrides/home.html +++ /dev/null @@ -1,106 +0,0 @@ - - -{% extends "main.html" %} - - -{% block tabs %} - {{ super() }} - - - - - -
    -
    -
    - - -
    - -
    - - -
    -

    Technical documentation that just works

    -

    {{ config.site_description }}. Set up in 5 minutes.

    - - Quick start - - - Get Insiders - -
    -
    -
    -
    -{% endblock %} - - -{% block content %}{% endblock %} - - -{% block footer %}{% endblock %} diff --git a/src/overrides/hooks/shortcodes.py b/src/overrides/hooks/shortcodes.py deleted file mode 100644 index 5b02e3cf..00000000 --- a/src/overrides/hooks/shortcodes.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -import posixpath -import re - -from mkdocs.config.defaults import MkDocsConfig -from mkdocs.structure.files import File, Files -from mkdocs.structure.pages import Page -from re import Match - -# ----------------------------------------------------------------------------- -# Hooks -# ----------------------------------------------------------------------------- - -# @todo -def on_page_markdown( - markdown: str, *, page: Page, config: MkDocsConfig, files: Files -): - - # Replace callback - def replace(match: Match): - type, args = match.groups() - args = args.strip() - if type == "version": - if args.startswith("insiders-"): - return _badge_for_version_insiders(args, page, files) - else: - return _badge_for_version(args, page, files) - elif type == "sponsors": return _badge_for_sponsors(page, files) - elif type == "flag": return flag(args, page, files) - elif type == "option": return option(args) - elif type == "setting": return setting(args) - elif type == "feature": return _badge_for_feature(args, page, files) - elif type == "plugin": return _badge_for_plugin(args, page, files) - elif type == "extension": return _badge_for_extension(args, page, files) - elif type == "utility": return _badge_for_utility(args, page, files) - elif type == "example": return _badge_for_example(args, page, files) - elif type == "default": - if args == "none": return _badge_for_default_none(page, files) - elif args == "computed": return _badge_for_default_computed(page, files) - else: return _badge_for_default(args, page, files) - - # Otherwise, raise an error - raise RuntimeError(f"Unknown shortcode: {type}") - - # Find and replace all external asset URLs in current page - return re.sub( - r"", - replace, markdown, flags = re.I | re.M - ) - -# ----------------------------------------------------------------------------- -# Helper functions -# ----------------------------------------------------------------------------- - -# Create a flag of a specific type -def flag(args: str, page: Page, files: Files): - type, *_ = args.split(" ", 1) - if type == "experimental": return _badge_for_experimental(page, files) - elif type == "required": return _badge_for_required(page, files) - elif type == "customization": return _badge_for_customization(page, files) - elif type == "metadata": return _badge_for_metadata(page, files) - elif type == "multiple": return _badge_for_multiple(page, files) - raise RuntimeError(f"Unknown type: {type}") - -# Create a linkable option -def option(type: str): - _, *_, name = re.split(r"[.:]", type) - return f"[`{name}`](#+{type}){{ #+{type} }}\n\n" - -# Create a linkable setting - @todo append them to the bottom of the page -def setting(type: str): - _, *_, name = re.split(r"[.*]", type) - return f"`{name}` {{ #{type} }}\n\n[{type}]: #{type}\n\n" - -# ----------------------------------------------------------------------------- - -# Resolve path of file relative to given page - the posixpath always includes -# one additional level of `..` which we need to remove -def _resolve_path(path: str, page: Page, files: Files): - path, anchor, *_ = f"{path}#".split("#") - path = _resolve(files.get_file_from_path(path), page) - return "#".join([path, anchor]) if anchor else path - -# Resolve path of file relative to given page - the posixpath always includes -# one additional level of `..` which we need to remove -def _resolve(file: File, page: Page): - path = posixpath.relpath(file.src_uri, page.file.src_uri) - return posixpath.sep.join(path.split(posixpath.sep)[1:]) - -# ----------------------------------------------------------------------------- - -# Create badge -def _badge(icon: str, text: str = "", type: str = ""): - classes = f"mdx-badge mdx-badge--{type}" if type else "mdx-badge" - return "".join([ - f"", - *([f"{icon}"] if icon else []), - *([f"{text}"] if text else []), - f"", - ]) - -# Create sponsors badge -def _badge_for_sponsors(page: Page, files: Files): - icon = "material-heart" - href = _resolve_path("insiders/index.md", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Sponsors only')", - type = "heart" - ) - -# Create badge for version -def _badge_for_version(text: str, page: Page, files: Files): - spec = text - path = f"changelog/index.md#{spec}" - - # Return badge - icon = "material-tag-outline" - href = _resolve_path("conventions.md#version", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Minimum version')", - text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" - ) - -# Create badge for version of Insiders -def _badge_for_version_insiders(text: str, page: Page, files: Files): - spec = text.replace("insiders-", "") - path = f"insiders/changelog/index.md#{spec}" - - # Return badge - icon = "material-tag-heart-outline" - href = _resolve_path("conventions.md#version-insiders", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Minimum version')", - text = f"[{text}]({_resolve_path(path, page, files)})" if spec else "" - ) - -# Create badge for feature -def _badge_for_feature(text: str, page: Page, files: Files): - icon = "material-toggle-switch" - href = _resolve_path("conventions.md#feature", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Optional feature')", - text = text - ) - -# Create badge for plugin -def _badge_for_plugin(text: str, page: Page, files: Files): - icon = "material-floppy" - href = _resolve_path("conventions.md#plugin", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Plugin')", - text = text - ) - -# Create badge for extension -def _badge_for_extension(text: str, page: Page, files: Files): - icon = "material-language-markdown" - href = _resolve_path("conventions.md#extension", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Markdown extension')", - text = text - ) - -# Create badge for utility -def _badge_for_utility(text: str, page: Page, files: Files): - icon = "material-package-variant" - href = _resolve_path("conventions.md#utility", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Third-party utility')", - text = text - ) - -# Create badge for example -def _badge_for_example(text: str, page: Page, files: Files): - return "\n".join([ - _badge_for_example_download(text, page, files), - _badge_for_example_view(text, page, files) - ]) - -# Create badge for example view -def _badge_for_example_view(text: str, page: Page, files: Files): - icon = "material-folder-eye" - href = f"https://mkdocs-material.github.io/examples/{text}/" - return _badge( - icon = f"[:{icon}:]({href} 'View example')", - type = "right" - ) - -# Create badge for example download -def _badge_for_example_download(text: str, page: Page, files: Files): - icon = "material-folder-download" - href = f"https://mkdocs-material.github.io/examples/{text}.zip" - return _badge( - icon = f"[:{icon}:]({href} 'Download example')", - text = f"[`.zip`]({href})", - type = "right" - ) - -# Create badge for default value -def _badge_for_default(text: str, page: Page, files: Files): - icon = "material-water" - href = _resolve_path("conventions.md#default", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Default value')", - text = text - ) - -# Create badge for empty default value -def _badge_for_default_none(page: Page, files: Files): - icon = "material-water-outline" - href = _resolve_path("conventions.md#default", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Default value is empty')" - ) - -# Create badge for computed default value -def _badge_for_default_computed(page: Page, files: Files): - icon = "material-water-check" - href = _resolve_path("conventions.md#default", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Default value is computed')" - ) - -# Create badge for metadata property flag -def _badge_for_metadata(page: Page, files: Files): - icon = "material-list-box-outline" - href = _resolve_path("conventions.md#metadata", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Metadata property')" - ) - -# Create badge for required value flag -def _badge_for_required(page: Page, files: Files): - icon = "material-alert" - href = _resolve_path("conventions.md#required", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Required value')" - ) - -# Create badge for customization flag -def _badge_for_customization(page: Page, files: Files): - icon = "material-brush-variant" - href = _resolve_path("conventions.md#customization", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Customization')" - ) - -# Create badge for multiple instance flag -def _badge_for_multiple(page: Page, files: Files): - icon = "material-inbox-multiple" - href = _resolve_path("conventions.md#multiple-instances", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Multiple instances')" - ) - -# Create badge for experimental flag -def _badge_for_experimental(page: Page, files: Files): - icon = "material-flask-outline" - href = _resolve_path("conventions.md#experimental", page, files) - return _badge( - icon = f"[:{icon}:]({href} 'Experimental')" - ) diff --git a/src/overrides/hooks/translations.html b/src/overrides/hooks/translations.html deleted file mode 100644 index ab41c77d..00000000 --- a/src/overrides/hooks/translations.html +++ /dev/null @@ -1,54 +0,0 @@ - - - -{% macro render_language(language) %} -
    - :flag_{{ language.flag }}:{ .lg .middle } - - - {{ language.name }} - {{ language.code }} - - {% if language.miss %} - - - {{ language.miss | length }} translations missing - - - {% else %} - Complete - {% endif %} - -
    -{% endmacro %} - - -{% macro render(translations, start = 1) %} -
    -
      - {% for language in translations %} -
    1. {{ render_language(language) }}
    2. - {% endfor %} -
    -
    -{% endmacro %} diff --git a/src/overrides/hooks/translations.py b/src/overrides/hooks/translations.py deleted file mode 100644 index 661fd18e..00000000 --- a/src/overrides/hooks/translations.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import os -import re - -from glob import iglob -from mkdocs.config.defaults import MkDocsConfig -from mkdocs.structure.pages import Page -from urllib.parse import urlencode, urlparse - -# ----------------------------------------------------------------------------- -# Hooks -# ----------------------------------------------------------------------------- - -# Determine missing translations and render language overview in the setup -# guide, including links to provide missing translations. -def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): - issue_url = "https://github.com/squidfunk/mkdocs-material/issues/new" - if page.file.src_uri != "setup/changing-the-language.md": - return - - # Collect all existing languages - names: dict[str, str] = {} - known: dict[str, dict[str, str]] = {} - for path in iglob("src/templates/partials/languages/*.html"): - with open(path, "r", encoding = "utf-8") as f: - data = f.read() - - # Extract language code and name - name, = re.findall(r"", data) - code, _ = os.path.splitext(os.path.basename(path)) - - # Map names and available translations - names[code] = name - known[code] = dict(re.findall( - r"^ \"([^\"]+)\": \"([^\"]*)\"(?:,|$)?", data, - re.MULTILINE - )) - - # Remove technical stuff - for key in [ - "direction", - "search.config.pipeline", - "search.config.lang", - "search.config.separator" - ]: - if key in known[code]: - del known[code][key] - - # Traverse all languages and compute missing translations - languages = [] - reference = set(known["en"]) - for code, name in names.items(): - miss = reference - set(known[code]) - - # Check each translations - translations: list[str] = [] - for key, value in known["en"].items(): - if key in known[code]: - translations.append( - f" \"{key}\": \"{known[code][key]}\"" - ) - else: - translations.append( - f" \"{key}\": \"{value} ⬅️\"" - ) - - # Assemble GitHub issue URL - link = urlparse(issue_url) - link = link._replace(query = urlencode({ - "template": "04-add-translations.yml", - "title": f"Update {name} translations", - "translations": "\n".join([ - "{% macro t(key) %}{{ {", - ",\n".join(translations), - "}[key] }}{% endmacro %}" - ]), - "country-flag": f":flag_{countries[code]}:" - })) - - # Add translation - languages.append({ - "flag": countries[code], - "code": code, - "name": name, - "link": link.geturl(), - "miss": miss - }) - - # Load template and render translations - env = config.theme.get_env() - template = env.get_template( "hooks/translations.html") - translations = template.module.render( - sorted(languages, key = lambda language: language["name"]) - ) - - # Replace translation marker - return markdown.replace( - "", "\n".join( - [line.lstrip() for line in translations.split("\n") - ] - )) - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Map ISO 639-1 (languages) to ISO 3166 (countries) -countries = dict({ - "af": "za", - "ar": "ae", - "be": "by", - "bg": "bg", - "bn": "bd", - "ca": "es", - "cs": "cz", - "da": "dk", - "de": "de", - "el": "gr", - "en": "us", - "eo": "eu", - "es": "es", - "et": "ee", - "eu": "es", - "fa": "ir", - "fi": "fi", - "fr": "fr", - "gl": "es", - "he": "il", - "hi": "in", - "hr": "hr", - "hu": "hu", - "hy": "am", - "id": "id", - "is": "is", - "it": "it", - "ja": "jp", - "ka": "ge", - "kn": "in", - "ko": "kr", - "ku-IQ": "iq", - "lb": "lu", - "lt": "lt", - "lv": "lv", - "mk": "mk", - "mn": "mn", - "ms": "my", - "my": "mm", - "nb": "no", - "nl": "nl", - "nn": "no", - "pl": "pl", - "pt-BR": "br", - "pt": "pt", - "ro": "ro", - "ru": "ru", - "sa": "in", - "sh": "rs", - "si": "lk", - "sk": "sk", - "sl": "si", - "sr": "rs", - "sv": "se", - "te": "in", - "th": "th", - "tl": "ph", - "tr": "tr", - "uk": "ua", - "ur": "pk", - "uz": "uz", - "vi": "vn", - "zh": "cn", - "zh-Hant": "cn", - "zh-TW": "tw" -}) diff --git a/src/overrides/main.html b/src/overrides/main.html deleted file mode 100644 index 39b68b5a..00000000 --- a/src/overrides/main.html +++ /dev/null @@ -1,59 +0,0 @@ - - -{% extends "base.html" %} - - -{% block extrahead %} - - - -{% endblock %} - - -{% block announce %} - For updates follow @squidfunk on - - - {% include ".icons/fontawesome/brands/mastodon.svg" %} - - Fosstodon - - and - - - Twitter - -{% endblock %} - - -{% block scripts %} - {{ super() }} - - - -{% endblock %} diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/blog/__init__.py b/src/plugins/blog/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/blog/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/blog/author.py b/src/plugins/blog/author.py deleted file mode 100644 index 1dcfc2de..00000000 --- a/src/plugins/blog/author.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.base import Config -from mkdocs.config.config_options import DictOfItems, SubConfig, Type - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Author -class Author(Config): - name = Type(str) - description = Type(str) - avatar = Type(str) - -# ----------------------------------------------------------------------------- - -# Authors -class Authors(Config): - authors = DictOfItems(SubConfig(Author), default = {}) diff --git a/src/plugins/blog/config.py b/src/plugins/blog/config.py deleted file mode 100644 index c7a85095..00000000 --- a/src/plugins/blog/config.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from functools import partial -from markdown.extensions.toc import slugify -from mkdocs.config.config_options import Choice, Deprecated, Optional, Type -from mkdocs.config.base import Config - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Blog plugin configuration -class BlogConfig(Config): - enabled = Type(bool, default = True) - - # Settings for blog - blog_dir = Type(str, default = "blog") - blog_toc = Type(bool, default = False) - - # Settings for posts - post_dir = Type(str, default = "{blog}/posts") - post_date_format = Type(str, default = "long") - post_url_date_format = Type(str, default = "yyyy/MM/dd") - post_url_format = Type(str, default = "{date}/{slug}") - post_url_max_categories = Type(int, default = 1) - post_slugify = Type((type(slugify), partial), default = slugify) - post_slugify_separator = Type(str, default = "-") - post_excerpt = Choice(["optional", "required"], default = "optional") - post_excerpt_max_authors = Type(int, default = 1) - post_excerpt_max_categories = Type(int, default = 5) - post_excerpt_separator = Type(str, default = "") - post_readtime = Type(bool, default = True) - post_readtime_words_per_minute = Type(int, default = 265) - - # Settings for archive - archive = Type(bool, default = True) - archive_name = Type(str, default = "blog.archive") - archive_date_format = Type(str, default = "yyyy") - archive_url_date_format = Type(str, default = "yyyy") - archive_url_format = Type(str, default = "archive/{date}") - archive_toc = Optional(Type(bool)) - - # Settings for categories - categories = Type(bool, default = True) - categories_name = Type(str, default = "blog.categories") - categories_url_format = Type(str, default = "category/{slug}") - categories_slugify = Type((type(slugify), partial), default = slugify) - categories_slugify_separator = Type(str, default = "-") - categories_allowed = Type(list, default = []) - categories_toc = Optional(Type(bool)) - - # Settings for pagination - pagination = Type(bool, default = True) - pagination_per_page = Type(int, default = 10) - pagination_url_format = Type(str, default = "page/{page}") - pagination_format = Type(str, default = "~2~") - pagination_if_single_page = Type(bool, default = False) - pagination_keep_content = Type(bool, default = False) - - # Settings for authors - authors = Type(bool, default = True) - authors_file = Type(str, default = "{blog}/.authors.yml") - - # Settings for drafts - draft = Type(bool, default = False) - draft_on_serve = Type(bool, default = True) - draft_if_future_date = Type(bool, default = False) - - # Deprecated settings - pagination_template = Deprecated(moved_to = "pagination_format") diff --git a/src/plugins/blog/plugin.py b/src/plugins/blog/plugin.py deleted file mode 100644 index 375b8cfe..00000000 --- a/src/plugins/blog/plugin.py +++ /dev/null @@ -1,884 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -import logging -import os -import posixpath -import yaml - -from babel.dates import format_date -from datetime import datetime -from mkdocs.config.defaults import MkDocsConfig -from mkdocs.exceptions import PluginError -from mkdocs.plugins import BasePlugin, event_priority -from mkdocs.structure import StructureItem -from mkdocs.structure.files import File, Files, InclusionLevel -from mkdocs.structure.nav import Navigation, Section -from mkdocs.structure.pages import Page -from mkdocs.utils import copy_file, get_relative_url -from paginate import Page as Pagination -from shutil import rmtree -from tempfile import mkdtemp -from yaml import SafeLoader - -from .author import Authors -from .config import BlogConfig -from .readtime import readtime -from .structure import Archive, Category, Excerpt, Post, View -from .templates import url_filter - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Blog plugin -class BlogPlugin(BasePlugin[BlogConfig]): - supports_multiple_instances = True - - # Initialize plugin - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Initialize incremental builds - self.is_serve = False - self.is_dirty = False - - # Initialize temporary directory - self.temp_dir = mkdtemp() - - # Determine whether we're serving the site - def on_startup(self, *, command, dirty): - self.is_serve = command == "serve" - self.is_dirty = dirty - - # Initialize authors and set defaults - def on_config(self, config): - if not self.config.enabled: - return - - # Initialize entrypoint - self.blog: View - - # Initialize and resolve authors, if enabled - if self.config.authors: - self.authors = self._resolve_authors(config) - - # Initialize table of contents settings - if not isinstance(self.config.archive_toc, bool): - self.config.archive_toc = self.config.blog_toc - if not isinstance(self.config.categories_toc, bool): - self.config.categories_toc = self.config.blog_toc - - # By default, drafts are rendered when the documentation is served, - # but not when it is built, for a better user experience - if self.is_serve and self.config.draft_on_serve: - self.config.draft = True - - # Resolve and load posts and generate views (run later) - we want to allow - # other plugins to add generated posts or views, so we run this plugin as - # late as possible. We also need to remove the posts from the navigation - # before navigation is constructed, as the entrypoint should be considered - # to be the active page for each post. The URLs of posts are computed before - # Markdown processing, so that when linking to and from posts, behavior is - # exactly the same as with regular documentation pages. We create all pages - # related to posts as part of this plugin, so we control the entire process. - @event_priority(-50) - def on_files(self, files, *, config): - if not self.config.enabled: - return - - # Resolve path to entrypoint and site directory - root = posixpath.normpath(self.config.blog_dir) - site = config.site_dir - - # Compute path to posts directory - path = self.config.post_dir.format(blog = root) - path = posixpath.normpath(path) - - # Adjust destination paths for media files - for file in files.media_files(): - if not file.src_uri.startswith(path): - continue - - # We need to adjust destination paths for assets to remove the - # purely functional posts directory prefix when building - file.dest_uri = file.dest_uri.replace(path, root) - file.abs_dest_path = os.path.join(site, file.dest_path) - file.url = file.url.replace(path, root) - - # Resolve entrypoint and posts sorted by descending date - if the posts - # directory or entrypoint do not exist, they are automatically created - self.blog = self._resolve(files, config) - self.blog.posts = sorted( - self._resolve_posts(files, config), - key = lambda post: post.config.date.created, - reverse = True - ) - - # Generate views for archive - if self.config.archive: - views = self._generate_archive(config, files) - self.blog.views.extend(views) - - # Generate views for categories - if self.config.categories: - views = self._generate_categories(config, files) - self.blog.views.extend(views) - - # Generate pages for views - if self.config.pagination: - for view in self._resolve_views(self.blog): - for page in self._generate_pages(view, config, files): - page.file.inclusion = InclusionLevel.EXCLUDED - view.pages.append(page) - - # Ensure that entrypoint is always included in navigation - self.blog.file.inclusion = InclusionLevel.INCLUDED - - # Attach posts and views to navigation (run later) - again, we allow other - # plugins to alter the navigation before we start to attach posts and views - # generated by this plugin at the correct locations in the navigation. Also, - # we make sure to correct links to the parent and siblings of each page. - @event_priority(-50) - def on_nav(self, nav, *, config, files): - if not self.config.enabled: - return - - # If we're not building a standalone blog, the entrypoint will always - # have a parent when it is included in the navigation. The parent is - # essential to correctly resolve the location where the archive and - # category views are attached. If the entrypoint doesn't have a parent, - # we know that the author did not include it in the navigation, so we - # explicitly mark it as not included. - if not self.blog.parent and self.config.blog_dir != ".": - self.blog.file.inclusion = InclusionLevel.NOT_IN_NAV - - # Attach posts to entrypoint without adding them to the navigation, so - # that the entrypoint is considered to be the active page for each post - self._attach(self.blog, [None, *reversed(self.blog.posts), None]) - for post in self.blog.posts: - post.file.inclusion = InclusionLevel.NOT_IN_NAV - - # Revert temporary exclusion of views from navigation - for view in self._resolve_views(self.blog): - for page in view.pages: - page.file.inclusion = self.blog.file.inclusion - - # Attach views for archive - if self.config.archive: - title = self._translate(self.config.archive_name, config) - views = [_ for _ in self.blog.views if isinstance(_, Archive)] - - # Attach and link views for archive - if self.blog.file.inclusion.is_in_nav(): - self._attach_to(self.blog, Section(title, views), nav) - - # Attach views for categories - if self.config.categories: - title = self._translate(self.config.categories_name, config) - views = [_ for _ in self.blog.views if isinstance(_, Category)] - - # Attach and link views for categories, if any - if self.blog.file.inclusion.is_in_nav() and views: - self._attach_to(self.blog, Section(title, views), nav) - - # Attach pages for views - if self.config.pagination: - for view in self._resolve_views(self.blog): - for at in range(1, len(view.pages)): - self._attach_at(view.parent, view, view.pages[at]) - - # Prepare post for rendering (run later) - allow other plugins to alter - # the contents or metadata of a post before it is rendered and make sure - # that the post includes a separator, which is essential for rendering - # excerpts that should be included in views - @event_priority(-50) - def on_page_markdown(self, markdown, *, page, config, files): - if not self.config.enabled: - return - - # Skip if page is not a post managed by this instance - this plugin has - # support for multiple instances, which is why this check is necessary - if page not in self.blog.posts: - if not self.config.pagination: - return - - # We set the contents of the view to its title if pagination should - # not keep the content of the original view on paginated views - if not self.config.pagination_keep_content: - view = self._resolve_original(page) - if view in self._resolve_views(self.blog): - - # If the current view is paginated, use the rendered title - # of the original view in case the author set the title in - # the page's contents, or it would be overridden with the - # one set in mkdocs.yml, leading to inconsistent headings - assert isinstance(view, View) - if view != page: - name = view._title_from_render or view.title - return f"# {name}" - - # Nothing more to be done for views - return - - # Extract and assign authors to post, if enabled - if self.config.authors: - for name in page.config.authors: - if name not in self.authors: - raise PluginError(f"Couldn't find author '{name}'") - - # Append to list of authors - page.authors.append(self.authors[name]) - - # Extract settings for excerpts - separator = self.config.post_excerpt_separator - max_authors = self.config.post_excerpt_max_authors - max_categories = self.config.post_excerpt_max_categories - - # Ensure presence of separator and throw, if its absent and required - - # we append the separator to the end of the contents of the post, if it - # is not already present, so we can remove footnotes or other content - # from the excerpt without affecting the content of the excerpt - if separator not in page.markdown: - path = page.file.src_path - if self.config.post_excerpt == "required": - raise PluginError( - f"Couldn't find '{separator}' separator in '{path}'" - ) - else: - page.markdown += f"\n\n{separator}" - - # Create excerpt for post and inherit authors and categories - excerpts - # can contain a subset of the authors and categories of the post - page.excerpt = Excerpt(page, config, files) - page.excerpt.authors = page.authors[:max_authors] - page.excerpt.categories = page.categories[:max_categories] - - # Process posts - def on_page_content(self, html, *, page, config, files): - if not self.config.enabled: - return - - # Skip if page is not a post managed by this instance - this plugin has - # support for multiple instances, which is why this check is necessary - if page not in self.blog.posts: - return - - # Compute readtime of post, if enabled and not explicitly set - if self.config.post_readtime: - words_per_minute = self.config.post_readtime_words_per_minute - if not page.config.readtime: - page.config.readtime = readtime(html, words_per_minute) - - # Register template filters for plugin - def on_env(self, env, *, config, files): - if not self.config.enabled: - return - - # Filter for formatting dates related to posts - def date_filter(date: datetime): - return self._format_date_for_post(date, config) - - # Register custom template filters - env.filters["date"] = date_filter - env.filters["url"] = url_filter - - # Prepare view for rendering (run latest) - views are rendered last, as we - # need to mutate the navigation to account for pagination. The main problem - # is that we need to replace the view in the navigation, because otherwise - # the view would not be considered active. - @event_priority(-100) - def on_page_context(self, context, *, page, config, nav): - if not self.config.enabled: - return - - # Skip if page is not a view managed by this instance - this plugin has - # support for multiple instances, which is why this check is necessary - view = self._resolve_original(page) - if view not in self._resolve_views(self.blog): - return - - # If the current view is paginated, replace and rewire it - the current - # view temporarily becomes the main view, and is reset after rendering - assert isinstance(view, View) - if view != page: - prev = view.pages[view.pages.index(page) - 1] - - # Replace previous page with current page - items = self._resolve_siblings(view, nav) - items[items.index(prev)] = page - - # Render excerpts and prepare pagination - posts, pagination = self._render(page) - - # Render pagination links - def pager(args: object): - return pagination.pager( - format = self.config.pagination_format, - show_if_single_page = self.config.pagination_if_single_page, - **args - ) - - # Assign posts and pagination to context - context["posts"] = posts - context["pagination"] = pager if pagination else None - - # After rendering a paginated view, replace the URL of the paginated view - # with the URL of the original view - since we need to replace the original - # view with a paginated view in `on_page_context` for correct resolution of - # the active state, we must fix the paginated view URLs after rendering - def on_post_page(self, output, *, page, config): - if not self.config.enabled: - return - - # Skip if page is not a view managed by this instance - this plugin has - # support for multiple instances, which is why this check is necessary - view = self._resolve_original(page) - if view not in self._resolve_views(self.blog): - return - - # If the current view is paginated, replace the URL of the paginated - # view with the URL of the original view - see https://t.ly/Yeh-P - assert isinstance(view, View) - if view != page: - page.file.url = view.file.url - - # Remove temporary directory on shutdown - def on_shutdown(self): - rmtree(self.temp_dir) - - # ------------------------------------------------------------------------- - - # Check if the given post is excluded - def _is_excluded(self, post: Post): - if self.config.draft: - return False - - # If a post was not explicitly marked or unmarked as draft, and the - # date should be taken into account, we automatically mark it as draft - # if the publishing date is in the future. This, of course, is opt-in - # and must be explicitly enabled by the author. - if not isinstance(post.config.draft, bool): - if self.config.draft_if_future_date: - return post.config.date.created > datetime.now() - - # Post might be a draft - return bool(post.config.draft) - - # ------------------------------------------------------------------------- - - # Resolve entrypoint - the entrypoint of the blog must have been created - # if it did not exist before, and hosts all posts sorted by descending date - def _resolve(self, files: Files, config: MkDocsConfig): - path = os.path.join(self.config.blog_dir, "index.md") - path = os.path.normpath(path) - - # Create entrypoint, if it does not exist - note that the entrypoint is - # created in the docs directory, not in the temporary directory - docs = os.path.relpath(config.docs_dir) - name = os.path.join(docs, path) - if not os.path.isfile(name): - file = self._path_to_file(path, config, temp = False) - files.append(file) - - # Create file in docs directory - self._save_to_file(file.abs_src_path, "# Blog\n\n") - - # Create and return entrypoint - file = files.get_file_from_path(path) - return View(None, file, config) - - # Resolve post - the caller must make sure that the given file points to an - # actual post (and not a page), or behavior might be unpredictable - def _resolve_post(self, file: File, config: MkDocsConfig): - post = Post(file, config) - - # Compute path and create a temporary file for path resolution - path = self._format_path_for_post(post, config) - temp = self._path_to_file(path, config, temp = False) - - # Replace destination file system path and URL - file.dest_uri = temp.dest_uri - file.abs_dest_path = temp.abs_dest_path - file.url = temp.url - - # Replace canonical URL and return post - post._set_canonical_url(config.site_url) - return post - - # Resolve posts from directory - traverse all documentation pages and filter - # and yield those that are located in the posts directory - def _resolve_posts(self, files: Files, config: MkDocsConfig): - path = self.config.post_dir.format(blog = self.config.blog_dir) - path = os.path.normpath(path) - - # Create posts directory, if it does not exist - docs = os.path.relpath(config.docs_dir) - name = os.path.join(docs, path) - if not os.path.isdir(name): - os.makedirs(name, exist_ok = True) - - # Filter posts from pages - for file in files.documentation_pages(): - if not file.src_path.startswith(path): - continue - - # Temporarily remove post from navigation - file.inclusion = InclusionLevel.EXCLUDED - - # Resolve post - in order to determine whether a post should be - # excluded, we must load it and analyze its metadata. All posts - # marked as drafts are excluded, except for when the author has - # configured drafts to be included in the navigation. - post = self._resolve_post(file, config) - if not self._is_excluded(post): - yield post - - # Resolve authors - check if there's an authors file at the configured - # location, and if one was found, load and validate it - def _resolve_authors(self, config: MkDocsConfig): - path = self.config.authors_file.format(blog = self.config.blog_dir) - path = os.path.normpath(path) - - # Resolve path relative to docs directory - docs = os.path.relpath(config.docs_dir) - file = os.path.join(docs, path) - - # If the authors file does not exist, return here - config: Authors = Authors() - if not os.path.isfile(file): - return config.authors - - # Open file and parse as YAML - with open(file, encoding = "utf-8") as f: - config.config_file_path = os.path.abspath(file) - try: - config.load_dict(yaml.load(f, SafeLoader) or {}) - - # The authors file could not be loaded because of a syntax error, - # which we display to the author with a nice error message - except Exception as e: - raise PluginError( - f"Error reading authors file '{path}' in '{docs}':\n" - f"{e}" - ) - - # Validate authors and throw if errors occurred - errors, warnings = config.validate() - if not config.authors and warnings: - log.warning( - f"Action required: the format of the authors file changed.\n" - f"All authors must now be located under the 'authors' key.\n" - f"Please adjust '{file}' to match:\n" - f"\n" - f"authors:\n" - f" squidfunk:\n" - f" avatar: https://avatars.githubusercontent.com/u/932156\n" - f" description: Creator\n" - f" name: Martin Donath\n" - f"\n" - ) - for _, w in warnings: - log.warning(w) - for _, e in errors: - raise PluginError( - f"Error reading authors file '{path}' in '{docs}':\n" - f"{e}" - ) - - # Return authors - return config.authors - - # Resolve views of the given view in pre-order - def _resolve_views(self, view: View): - yield view - - # Resolve views recursively - for page in view.views: - for next in self._resolve_views(page): - assert isinstance(next, View) - yield next - - # Resolve siblings of a navigation item - def _resolve_siblings(self, item: StructureItem, nav: Navigation): - if isinstance(item.parent, Section): - return item.parent.children - else: - return nav.items - - # Resolve original page or view (e.g. for paginated views) - def _resolve_original(self, page: Page): - if isinstance(page, View): - return page.pages[0] - else: - return page - - # ------------------------------------------------------------------------- - - # Generate views for archive - analyze posts and generate the necessary - # views, taking the date format provided by the author into account - def _generate_archive(self, config: MkDocsConfig, files: Files): - for post in self.blog.posts: - date = post.config.date.created - - # Compute name and path of archive view - name = self._format_date_for_archive(date, config) - path = self._format_path_for_archive(post, config) - - # Create file for view, if it does not exist - file = files.get_file_from_path(path) - if not file or self.temp_dir not in file.abs_src_path: - file = self._path_to_file(path, config) - files.append(file) - - # Create file in temporary directory - self._save_to_file(file.abs_src_path, f"# {name}") - - # Create and yield view - we don't explicitly set the title of - # the view, so authors can override them in the page's content - if not isinstance(file.page, Archive): - yield Archive(None, file, config) - - # Assign post to archive - assert isinstance(file.page, Archive) - file.page.posts.append(post) - - # Generate views for categories - analyze posts and generate the necessary - # views, taking the allowed categories as set by the author into account - def _generate_categories(self, config: MkDocsConfig, files: Files): - for post in self.blog.posts: - for name in post.config.categories: - path = self._format_path_for_category(name) - - # Ensure category is in non-empty allow list - categories = self.config.categories_allowed or [name] - if name not in categories: - docs = os.path.relpath(config.docs_dir) - path = os.path.relpath(post.file.abs_src_path, docs) - raise PluginError( - f"Error reading categories of post '{path}' in " - f"'{docs}': category '{name}' not in allow list" - ) - - # Create file for view, if it does not exist - file = files.get_file_from_path(path) - if not file or self.temp_dir not in file.abs_src_path: - file = self._path_to_file(path, config) - files.append(file) - - # Create file in temporary directory - self._save_to_file(file.abs_src_path, f"# {name}") - - # Create and yield view - we don't explicitly set the title of - # the view, so authors can override them in the page's content - if not isinstance(file.page, Category): - yield Category(None, file, config) - - # Assign post to category and vice versa - assert isinstance(file.page, Category) - file.page.posts.append(post) - post.categories.append(file.page) - - # Generate pages for pagination - analyze view and generate the necessary - # pages, creating a chain of views for simple rendering and replacement - def _generate_pages(self, view: View, config: MkDocsConfig, files: Files): - yield view - - # Compute pagination boundaries and create pages - pages are internally - # handled as copies of a view, as they map to the same source location - step = self.config.pagination_per_page - for at in range(step, len(view.posts), step): - path = self._format_path_for_pagination(view, 1 + at // step) - - # Create file for view, if it does not exist - file = files.get_file_from_path(path) - if not file or self.temp_dir not in file.abs_src_path: - file = self._path_to_file(path, config) - files.append(file) - - # Copy file to temporary directory - copy_file(view.file.abs_src_path, file.abs_src_path) - - # Create view and attach to previous page - if not isinstance(file.page, View): - yield View(None, file, config) - - # Assign pages and posts to view - assert isinstance(file.page, View) - file.page.pages = view.pages - file.page.posts = view.posts - - # ------------------------------------------------------------------------- - - # Attach a list of pages to each other and to the given parent item without - # explicitly adding them to the navigation, which can be done by the caller - def _attach(self, parent: StructureItem, pages: list[Page]): - for tail, page, head in zip(pages, pages[1:], pages[2:]): - - # Link page to parent and siblings - page.parent = parent - page.previous_page = tail - page.next_page = head - - # If the page is a view, we know that we generated it and need to - # link its siblings back to the view - if isinstance(page, View): - view = self._resolve_original(page) - if tail: tail.next_page = view - if head: head.previous_page = view - - # Attach a page to the given parent and link it to the previous and next - # page of the given host - this is exclusively used for paginated views - def _attach_at(self, parent: StructureItem, host: Page, page: Page): - self._attach(parent, [host.previous_page, page, host.next_page]) - - # Attach a section as a sibling to the given view, make sure its pages are - # part of the navigation, and ensure all pages are linked correctly - def _attach_to(self, view: View, section: Section, nav: Navigation): - section.parent = view.parent - - # Resolve siblings, which are the children of the parent section, or - # the top-level list of navigation items if the view is at the root of - # the project, and append the given section to it. It's currently not - # possible to chose the position of a section. - items = self._resolve_siblings(view, nav) - items.append(section) - - # Find last sibling that is a page, skipping sections, as we need to - # append the given section after all other pages - tail = next(item for item in reversed(items) if isinstance(item, Page)) - head = tail.next_page - - # Attach section to navigation and pages to each other - nav.pages.extend(section.children) - self._attach(section, [tail, *section.children, head]) - - # ------------------------------------------------------------------------- - - # Render excerpts and pagination for the given view - def _render(self, view: View): - posts, pagination = view.posts, None - - # Create pagination, if enabled - if self.config.pagination: - at = view.pages.index(view) - - # Compute pagination boundaries - step = self.config.pagination_per_page - p, q = at * step, at * step + step - - # Extract posts in pagination boundaries - posts = view.posts[p:q] - pagination = self._render_pagination(view, (p, q)) - - # Render excerpts for selected posts - posts = [ - self._render_post(post.excerpt, view) - for post in posts if post.excerpt - ] - - # Return posts and pagination - return posts, pagination - - # Render excerpt in the context of the given view - def _render_post(self, excerpt: Excerpt, view: View): - excerpt.render(view, self.config.post_excerpt_separator) - - # Determine whether to add posts to the table of contents of the view - - # note that those settings can be changed individually for each type of - # view, which is why we need to check the type of view and the table of - # contents setting for that type of view - toc = self.config.blog_toc - if isinstance(view, Archive): - toc = self.config.archive_toc - if isinstance(view, Category): - toc = self.config.categories_toc - - # Attach top-level table of contents item to view if it should be added - # and both, the view and excerpt contain table of contents items - if toc and excerpt.toc.items and view.toc.items: - view.toc.items[0].children.append(excerpt.toc.items[0]) - - # Return excerpt - return excerpt - - # Create pagination for the given view and range - def _render_pagination(self, view: View, range: tuple[int, int]): - p, q = range - - # Create URL from the given page to another page - def url_maker(n: int): - return get_relative_url(view.pages[n - 1].url, view.url) - - # Return pagination - return Pagination( - view.posts, page = q // (q - p), - items_per_page = q - p, - url_maker = url_maker - ) - - # ------------------------------------------------------------------------- - - # Format path for post - def _format_path_for_post(self, post: Post, config: MkDocsConfig): - categories = post.config.categories[:self.config.post_url_max_categories] - categories = [self._slugify_category(name) for name in categories] - - # Replace placeholders in format string - date = post.config.date.created - path = self.config.post_url_format.format( - categories = "/".join(categories), - date = self._format_date_for_post_url(date, config), - file = post.file.name, - slug = post.config.slug or self._slugify_post(post) - ) - - # Normalize path and strip slashes at the beginning and end - path = posixpath.normpath(path.strip("/")) - return posixpath.join(self.config.blog_dir, f"{path}.md") - - # Format path for archive - def _format_path_for_archive(self, post: Post, config: MkDocsConfig): - date = post.config.date.created - path = self.config.archive_url_format.format( - date = self._format_date_for_archive_url(date, config) - ) - - # Normalize path and strip slashes at the beginning and end - path = posixpath.normpath(path.strip("/")) - return posixpath.join(self.config.blog_dir, f"{path}.md") - - # Format path for category - def _format_path_for_category(self, name: str): - path = self.config.categories_url_format.format( - slug = self._slugify_category(name) - ) - - # Normalize path and strip slashes at the beginning and end - path = posixpath.normpath(path.strip("/")) - return posixpath.join(self.config.blog_dir, f"{path}.md") - - # Format path for pagination - def _format_path_for_pagination(self, view: View, page: int): - path = self.config.pagination_url_format.format( - page = page - ) - - # Compute base path for pagination - if the given view is an index file, - # we need to pop the file name from the base so it's not part of the URL - # and we need to append `index` to the path, so the paginated view is - # also an index page - see https://t.ly/71MKF - base, _ = posixpath.splitext(view.file.src_uri) - if view.is_index: - base = posixpath.dirname(base) - path = posixpath.join(path, "index") - - # Normalize path and strip slashes at the beginning and end - path = posixpath.normpath(path.strip("/")) - return posixpath.join(base, f"{path}.md") - - # ------------------------------------------------------------------------- - - # Format date - def _format_date(self, date: datetime, format: str, config: MkDocsConfig): - locale = config.theme["language"] - return format_date(date, format = format, locale = locale) - - # Format date for post - def _format_date_for_post(self, date: datetime, config: MkDocsConfig): - format = self.config.post_date_format - return self._format_date(date, format, config) - - # Format date for post URL - def _format_date_for_post_url(self, date: datetime, config: MkDocsConfig): - format = self.config.post_url_date_format - return self._format_date(date, format, config) - - # Format date for archive - def _format_date_for_archive(self, date: datetime, config: MkDocsConfig): - format = self.config.archive_date_format - return self._format_date(date, format, config) - - # Format date for archive URL - def _format_date_for_archive_url(self, date: datetime, config: MkDocsConfig): - format = self.config.archive_url_date_format - return self._format_date(date, format, config) - - # ------------------------------------------------------------------------- - - # Slugify post title - def _slugify_post(self, post: Post): - separator = self.config.post_slugify_separator - return self.config.post_slugify(post.title, separator) - - # Slugify category - def _slugify_category(self, name: str): - separator = self.config.categories_slugify_separator - return self.config.categories_slugify(name, separator) - - # ------------------------------------------------------------------------- - - # Create a file for the given path, which must point to a valid source file, - # either inside the temporary directory or the docs directory - def _path_to_file(self, path: str, config: MkDocsConfig, *, temp = True): - assert path.endswith(".md") - file = File( - path, - config.docs_dir if not temp else self.temp_dir, - config.site_dir, - config.use_directory_urls - ) - - # Hack: mark file as generated, so other plugins don't think it's part - # of the file system. This is more or less a new quasi-standard that - # still needs to be adopted by MkDocs, and was introduced by the - # git-revision-date-localized-plugin - see https://bit.ly/3ZUmdBx - if temp: - file.generated_by = "material/blog" - - # Return file - return file - - # Create a file with the given content on disk - def _save_to_file(self, path: str, content: str): - os.makedirs(os.path.dirname(path), exist_ok = True) - with open(path, "w", encoding = "utf-8") as f: - f.write(content) - - # ------------------------------------------------------------------------- - - # Translate the placeholder referenced by the given key - def _translate(self, key: str, config: MkDocsConfig) -> str: - env = config.theme.get_env() - template = env.get_template( - "partials/language.html", globals = { "config": config } - ) - - # Translate placeholder - return template.module.t(key) - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.blog") diff --git a/src/plugins/blog/readtime/__init__.py b/src/plugins/blog/readtime/__init__.py deleted file mode 100644 index a0c149b9..00000000 --- a/src/plugins/blog/readtime/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import re - -from math import ceil - -from .parser import ReadtimeParser - -# ----------------------------------------------------------------------------- -# Functions -# ----------------------------------------------------------------------------- - -# Compute readtime - we first used the original readtime library, but the list -# of dependencies it brings with it increased the size of the Docker image by -# 20 MB (packed), which is an increase of 50%. For this reason, we adapt the -# original readtime algorithm to our needs - see https://t.ly/fPZ7L -def readtime(html: str, words_per_minute: int): - parser = ReadtimeParser() - parser.feed(html) - parser.close() - - # Extract words from text and compute readtime in seconds - words = len(re.split(r"\W+", "".join(parser.text))) - seconds = ceil(words / words_per_minute * 60) - - # Account for additional images - delta = 12 - for _ in range(parser.images): - seconds += delta - if delta > 3: delta -= 1 - - # Return readtime in minutes - return ceil(seconds / 60) diff --git a/src/plugins/blog/readtime/parser.py b/src/plugins/blog/readtime/parser.py deleted file mode 100644 index b91a7b30..00000000 --- a/src/plugins/blog/readtime/parser.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from html.parser import HTMLParser - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Readtime parser -class ReadtimeParser(HTMLParser): - - # Initialize parser - def __init__(self): - super().__init__(convert_charrefs = True) - - # Keep track of text and images - self.text = [] - self.images = 0 - - # Collect images - def handle_starttag(self, tag, attrs): - if tag == "img": - self.images += 1 - - # Collect text - def handle_data(self, data): - self.text.append(data) diff --git a/src/plugins/blog/structure/__init__.py b/src/plugins/blog/structure/__init__.py deleted file mode 100644 index 2fc541fe..00000000 --- a/src/plugins/blog/structure/__init__.py +++ /dev/null @@ -1,292 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -import logging -import os -import yaml - -from copy import copy -from markdown import Markdown -from material.plugins.blog.author import Author -from mkdocs.config.defaults import MkDocsConfig -from mkdocs.exceptions import PluginError -from mkdocs.structure.files import File, Files -from mkdocs.structure.nav import Section -from mkdocs.structure.pages import Page, _RelativePathTreeprocessor -from mkdocs.structure.toc import get_toc -from mkdocs.utils.meta import YAML_RE -from re import Match -from yaml import SafeLoader - -from .config import PostConfig -from .markdown import ExcerptTreeprocessor - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Post -class Post(Page): - - # Initialize post - posts are never listed in the navigation, which is why - # they will never include a title that was manually set, so we can omit it - def __init__(self, file: File, config: MkDocsConfig): - super().__init__(None, file, config) - - # Resolve path relative to docs directory - docs = os.path.relpath(config.docs_dir) - path = os.path.relpath(file.abs_src_path, docs) - - # Read contents and metadata immediately - with open(file.abs_src_path, encoding = "utf-8") as f: - self.markdown = f.read() - - # Sadly, MkDocs swallows any exceptions that occur during parsing. - # As we want to provide the best possible authoring experience, we - # need to catch errors early and display them nicely. We decided to - # drop support for MkDocs' MultiMarkdown syntax, because it is not - # correctly implemented anyway. When using MultiMarkdown syntax, all - # date formats are returned as strings and list are not properly - # supported. Thus, we just use the relevants parts of `get_data`. - match: Match = YAML_RE.match(self.markdown) - if not match: - raise PluginError( - f"Error reading metadata of post '{path}' in '{docs}':\n" - f"Expected metadata to be defined but found nothing" - ) - - # Extract metadata and parse as YAML - try: - self.meta = yaml.load(match.group(1), SafeLoader) or {} - self.markdown = self.markdown[match.end():].lstrip("\n") - - # The post's metadata could not be parsed because of a syntax error, - # which we display to the user with a nice error message - except Exception as e: - raise PluginError( - f"Error reading metadata of post '{path}' in '{docs}':\n" - f"{e}" - ) - - # Initialize post configuration, but remove all keys that this plugin - # doesn't care about, or they will be reported as invalid configuration - self.config: PostConfig = PostConfig(file.abs_src_path) - self.config.load_dict({ - key: self.meta[key] for key in ( - set(self.meta.keys()) & - set(self.config.keys()) - ) - }) - - # Validate configuration and throw if errors occurred - errors, warnings = self.config.validate() - for _, w in warnings: - log.warning(w) - for k, e in errors: - raise PluginError( - f"Error reading metadata '{k}' of post '{path}' in '{docs}':\n" - f"{e}" - ) - - # Excerpts are subsets of posts that are used in pages like archive and - # category views. They are not rendered as standalone pages, but are - # rendered in the context of a view. Each post has a dedicated excerpt - # instance which is reused when rendering views. - self.excerpt: Excerpt = None - - # Initialize authors and actegories - self.authors: list[Author] = [] - self.categories: list[Category] = [] - - # Ensure template is set or use default - self.meta.setdefault("template", "blog-post.html") - - # Ensure template hides navigation - self.meta["hide"] = self.meta.get("hide", []) - if "navigation" not in self.meta["hide"]: - self.meta["hide"].append("navigation") - - # The contents and metadata were already read in the constructor (and not - # in `read_source` as for pages), so this function must be set to a no-op - def read_source(self, config: MkDocsConfig): - pass - -# ----------------------------------------------------------------------------- - -# Excerpt -class Excerpt(Page): - - # Initialize an excerpt for the given post - we create the Markdown parser - # when intitializing the excerpt in order to improve rendering performance - # for excerpts, as they are reused across several different views, because - # posts might be referenced from multiple different locations - def __init__(self, post: Post, config: MkDocsConfig, files: Files): - self.file = copy(post.file) - self.post = post - - # Set canonical URL, or we can't print excerpts when debugging the - # blog plugin, as the `abs_url` property would be missing - self._set_canonical_url(config.site_url) - - # Initialize configuration and metadata - self.config = post.config - self.meta = post.meta - - # Initialize authors and categories - note that views usually contain - # subsets of those lists, which is why we need to manage them here - self.authors: list[Author] = [] - self.categories: list[Category] = [] - - # Initialize parser - note that we need to patch the configuration, - # more specifically the table of contents extension - config = _patch(config) - self.md = Markdown( - extensions = config.markdown_extensions, - extension_configs = config.mdx_configs, - ) - - # Register excerpt tree processor - this processor resolves anchors to - # posts from within views, so they point to the correct location - self.md.treeprocessors.register( - ExcerptTreeprocessor(post), - "excerpt", - 0 - ) - - # Register relative path tree processor - this processor resolves links - # to other pages and assets, and is used by MkDocs itself - self.md.treeprocessors.register( - _RelativePathTreeprocessor(self.file, files, config), - "relpath", - 1 - ) - - # Render an excerpt of the post on the given page - note that this is not - # thread-safe because excerpts are shared across views, as it cuts down on - # the cost of initialization. However, if in the future, we decide to render - # posts and views concurrently, we must change this behavior. - def render(self, page: Page, separator: str): - self.file.url = page.url - - # Retrieve excerpt tree processor and set page as base - at = self.md.treeprocessors.get_index_for_name("excerpt") - processor: ExcerptTreeprocessor = self.md.treeprocessors[at] - processor.base = page - - # Ensure that the excerpt includes a title in its content, since the - # title is linked to the post when rendering - see https://t.ly/5Gg2F - self.markdown = self.post.markdown - if not self.post._title_from_render: - self.markdown = "\n\n".join([f"# {self.post.title}", self.markdown]) - - # Convert Markdown to HTML and extract excerpt - self.content = self.md.convert(self.markdown) - self.content, *_ = self.content.split(separator, 1) - - # Extract table of contents and reset post URL - if we wouldn't reset - # the excerpt URL, linking to the excerpt from the view would not work - self.toc = get_toc(getattr(self.md, "toc_tokens", [])) - self.file.url = self.post.url - -# ----------------------------------------------------------------------------- - -# View -class View(Page): - - # Initialize view - def __init__(self, title: str | None, file: File, config: MkDocsConfig): - super().__init__(title, file, config) - self.parent: View | Section - - # Initialize posts and views - self.posts: list[Post] = [] - self.views: list[View] = [] - - # Initialize pages for pagination - self.pages: list[View] = [] - - # Set necessary metadata - def read_source(self, config: MkDocsConfig): - super().read_source(config) - - # Ensure template is set or use default - self.meta.setdefault("template", "blog.html") - -# ----------------------------------------------------------------------------- - -# Archive view -class Archive(View): - pass - -# ----------------------------------------------------------------------------- - -# Category view -class Category(View): - pass - -# ----------------------------------------------------------------------------- -# Helper functions -# ----------------------------------------------------------------------------- - -# Patch configuration -def _patch(config: MkDocsConfig): - config = copy(config) - - # Copy parts of configuration that needs to be patched - config.validation = copy(config.validation) - config.validation.links = copy(config.validation.links) - config.markdown_extensions = copy(config.markdown_extensions) - config.mdx_configs = copy(config.mdx_configs) - - # Make sure that the author did not add another instance of the table of - # contents extension to the configuration, as this leads to weird behavior - if "markdown.extensions.toc" in config.markdown_extensions: - config.markdown_extensions.remove("markdown.extensions.toc") - - # In order to render excerpts for posts, we need to make sure that the - # table of contents extension is appropriately configured - config.mdx_configs["toc"] = { - **config.mdx_configs.get("toc", {}), - **{ - "anchorlink": True, # Render headline as clickable - "baselevel": 2, # Render h1 as h2 and so forth - "permalink": False, # Remove permalinks - "toc_depth": 2 # Remove everything below h2 - } - } - - # Additionally, we disable link validation when rendering excerpts, because - # invalid links have already been reported when rendering the page - links = config.validation.links - links.not_found = logging.DEBUG - links.absolute_links = logging.DEBUG - links.unrecognized_links = logging.DEBUG - - # Return patched configuration - return config - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.blog") diff --git a/src/plugins/blog/structure/config.py b/src/plugins/blog/structure/config.py deleted file mode 100644 index 129491b9..00000000 --- a/src/plugins/blog/structure/config.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.base import Config -from mkdocs.config.config_options import ListOfItems, Optional, Type - -from .options import PostDate - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Post configuration -class PostConfig(Config): - authors = ListOfItems(Type(str), default = []) - categories = ListOfItems(Type(str), default = []) - date = PostDate() - draft = Optional(Type(bool)) - readtime = Optional(Type(int)) - slug = Optional(Type(str)) diff --git a/src/plugins/blog/structure/markdown.py b/src/plugins/blog/structure/markdown.py deleted file mode 100644 index 64ade554..00000000 --- a/src/plugins/blog/structure/markdown.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from markdown.treeprocessors import Treeprocessor -from mkdocs.structure.pages import Page -from mkdocs.utils import get_relative_url -from xml.etree.ElementTree import Element - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Excerpt tree processor -class ExcerptTreeprocessor(Treeprocessor): - - # Initialize excerpt tree processor - def __init__(self, page: Page, base: Page = None): - self.page = page - self.base = base - - # Transform HTML after Markdown processing - def run(self, root: Element): - main = True - - # We're only interested in anchors, which is why we continue when the - # link does not start with an anchor tag - for el in root.iter("a"): - anchor = el.get("href") - if not anchor.startswith("#"): - continue - - # The main headline should link to the post page, not to a specific - # anchor, which is why we remove the anchor in that case - path = get_relative_url(self.page.url, self.base.url) - if main: - el.set("href", path) - else: - el.set("href", path + anchor) - - # Main headline has been seen - main = False diff --git a/src/plugins/blog/structure/options.py b/src/plugins/blog/structure/options.py deleted file mode 100644 index 281dec9f..00000000 --- a/src/plugins/blog/structure/options.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from datetime import date, datetime, time -from mkdocs.config.base import BaseConfigOption, Config, ValidationError -from typing import Dict - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Date dictionary -class DateDict(Dict[str, datetime]): - - # Initialize date dictionary - def __init__(self, data: dict): - super().__init__(data) - - # Ensure presence of `date.created` - self.created: datetime = data["created"] - - # Allow attribute access - def __getattr__(self, name: str): - if name in self: - return self[name] - -# ----------------------------------------------------------------------------- - -# Post date option -class PostDate(BaseConfigOption[DateDict]): - - # Initialize post dates - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Normalize the supported types for post dates to datetime - def pre_validation(self, config: Config, key_name: str): - - # If the date points to a scalar value, convert it to a dictionary, - # since we want to allow the user to specify custom and arbitrary date - # values for posts. Currently, only the `created` date is mandatory, - # because it's needed to sort posts for views. - if not isinstance(config[key_name], dict): - config[key_name] = { "created": config[key_name] } - - # Convert all date values to datetime - for key, value in config[key_name].items(): - if isinstance(value, date): - config[key_name][key] = datetime.combine(value, time()) - - # Initialize date dictionary - config[key_name] = DateDict(config[key_name]) - - # Ensure each date value is of type datetime - def run_validation(self, value: DateDict): - for key in value: - if not isinstance(value[key], datetime): - raise ValidationError( - f"Expected type: {date} or {datetime} " - f"but received: {type(value[key])}" - ) - - # Ensure presence of `date.created` - if not value.created: - raise ValidationError( - "Expected 'created' date when using dictionary syntax" - ) - - # Return date dictionary - return value diff --git a/src/plugins/blog/templates/__init__.py b/src/plugins/blog/templates/__init__.py deleted file mode 100644 index 9f7d794b..00000000 --- a/src/plugins/blog/templates/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from jinja2 import pass_context -from jinja2.runtime import Context -from material.plugins.blog.structure import View -from mkdocs.utils.templates import url_filter as _url_filter - -# ----------------------------------------------------------------------------- -# Functions -# ----------------------------------------------------------------------------- - -# Filter for normalizing URLs with support for paginated views -@pass_context -def url_filter(context: Context, url: str): - page = context["page"] - - # If the current page is a view, check if the URL links to the page - # itself, and replace it with the URL of the main view - if isinstance(page, View): - if page.url == url: - url = page.pages[0].url - - # Forward to original template filter - return _url_filter(context, url) diff --git a/src/plugins/group/__init__.py b/src/plugins/group/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/group/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/group/config.py b/src/plugins/group/config.py deleted file mode 100644 index fb19222a..00000000 --- a/src/plugins/group/config.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from __future__ import annotations - -from mkdocs.config.config_options import Type -from mkdocs.config.base import Config - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Group plugin configuration -class GroupConfig(Config): - enabled = Type(bool, default = False) - plugins = Type(list | dict) diff --git a/src/plugins/group/plugin.py b/src/plugins/group/plugin.py deleted file mode 100644 index 4ab13dbf..00000000 --- a/src/plugins/group/plugin.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import logging - -from collections.abc import Callable -from mkdocs.config.config_options import Plugins -from mkdocs.config.defaults import MkDocsConfig -from mkdocs.exceptions import PluginError -from mkdocs.plugins import BasePlugin, event_priority - -from .config import GroupConfig - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Group plugin -class GroupPlugin(BasePlugin[GroupConfig]): - supports_multiple_instances = True - - # Determine whether we're serving the site - def on_startup(self, *, command, dirty): - self.is_serve = command == "serve" - self.is_dirty = dirty - - # If the group is enabled, conditionally load plugins - at first, this might - # sound easier than it actually is, as we need to jump through some hoops to - # ensure correct ordering among plugins. We're effectively initializing the - # plugins that are part of the group after all MkDocs finished initializing - # all other plugins, so we need to patch the order of the methods. Moreover, - # we must use MkDocs existing plugin collection, or we might have collisions - # with other plugins that are not part of the group. As so often, this is a - # little hacky, but has huge potential making plugin configuration easier. - # There's one little caveat: the `__init__` and `on_startup` methods of the - # plugins that are part of the group are called after all other plugins, so - # the `event_priority` decorator for `on_startup` events and is effectively - # useless. However, the `on_startup` event is only intended to set up the - # plugin and doesn't receive anything else than the invoked command and - # whether we're running a dirty build, so there should be no problems. - @event_priority(150) - def on_config(self, config): - if not self.config.enabled: - return - - # Retrieve plugin collection from configuration - option: Plugins = dict(config._schema)["plugins"] - assert isinstance(option, Plugins) - - # Load all plugins in group - self.plugins: dict[str, BasePlugin] = {} - try: - for name, plugin in self._load(option): - self.plugins[name] = plugin - - # The plugin could not be loaded, likely because it's not installed or - # misconfigured, so we raise a plugin error for a nicer error message - except Exception as e: - raise PluginError(str(e)) - - # Patch order of plugin methods - for events in option.plugins.events.values(): - self._patch(events, config) - - # Invoke `on_startup` event for plugins in group - command = "serve" if self.is_serve else "build" - for method in option.plugins.events["startup"]: - plugin = self._get_plugin(method) - - # Ensure that we have a method bound to a plugin (and not a hook) - if plugin and plugin in self.plugins.values(): - method(command = command, dirty = self.is_dirty) - - # ------------------------------------------------------------------------- - - # Retrieve plugin instance for bound method or nothing - def _get_plugin(self, method: Callable): - return getattr(method, "__self__", None) - - # Retrieve priority of plugin method - def _get_priority(self, method: Callable): - return getattr(method, "mkdocs_priority", 0) - - # Retrieve position of plugin - def _get_position(self, plugin: BasePlugin, config: MkDocsConfig) -> int: - for at, (_, candidate) in enumerate(config.plugins.items()): - if plugin == candidate: - return at - - # ------------------------------------------------------------------------- - - # Load plugins that are part of the group - def _load(self, option: Plugins): - for name, data in option._parse_configs(self.config.plugins): - yield option.load_plugin_with_namespace(name, data) - - # ------------------------------------------------------------------------- - - # Patch order of plugin methods - all other plugin methods are already in - # the right order, so we only need to check those that are part of the group - # and bubble them up into the right location. Some plugin methods may define - # priorities, so we need to make sure to order correctly within those. - def _patch(self, methods: list[Callable], config: MkDocsConfig): - position = self._get_position(self, config) - for at in reversed(range(1, len(methods))): - tail = methods[at - 1] - head = methods[at] - - # Skip if the plugin is not part of the group - plugin = self._get_plugin(head) - if not plugin or plugin not in self.plugins.values(): - continue - - # Skip if the previous method has a higher priority than the current - # one, because we know we can't swap them anyway - if self._get_priority(tail) > self._get_priority(head): - continue - - # Ensure that we have a method bound to a plugin (and not a hook) - plugin = self._get_plugin(tail) - if not plugin: - continue - - # Both methods have the same priority, so we check if the ordering - # of both methods is violated, and if it is, swap them - if (position < self._get_position(plugin, config)): - methods[at], methods[at - 1] = tail, head - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.group") diff --git a/src/plugins/info/__init__.py b/src/plugins/info/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/info/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/info/config.py b/src/plugins/info/config.py deleted file mode 100644 index cbd64d4c..00000000 --- a/src/plugins/info/config.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.config_options import Type -from mkdocs.config.base import Config - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Info plugin configuration -class InfoConfig(Config): - enabled = Type(bool, default = True) - enabled_on_serve = Type(bool, default = False) - - # Settings for archive - archive = Type(bool, default = True) - archive_stop_on_violation = Type(bool, default = True) diff --git a/src/plugins/info/plugin.py b/src/plugins/info/plugin.py deleted file mode 100644 index 7c6fdc17..00000000 --- a/src/plugins/info/plugin.py +++ /dev/null @@ -1,245 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import json -import logging -import os -import platform -import requests -import sys - -from colorama import Fore, Style -from importlib.metadata import distributions, version -from io import BytesIO -from markdown.extensions.toc import slugify -from mkdocs.plugins import BasePlugin, event_priority -from mkdocs.structure.files import get_files -from mkdocs.utils import get_theme_dir -from zipfile import ZipFile, ZIP_DEFLATED - -from .config import InfoConfig - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Info plugin -class InfoPlugin(BasePlugin[InfoConfig]): - - # Initialize plugin - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Initialize incremental builds - self.is_serve = False - - # Determine whether we're serving the site - def on_startup(self, *, command, dirty): - self.is_serve = command == "serve" - - # Create a self-contained example (run earliest) - determine all files that - # are visible to MkDocs and are used to build the site, create an archive - # that contains all of them, and print a summary of the archive contents. - # The user must attach this archive to the bug report. - @event_priority(100) - def on_config(self, config): - if not self.config.enabled: - return - - # By default, the plugin is disabled when the documentation is served, - # but not when it is built. This should nicely align with the expected - # user experience when creating reproductions. - if not self.config.enabled_on_serve and self.is_serve: - return - - # Resolve latest version - url = "https://github.com/squidfunk/mkdocs-material/releases/latest" - res = requests.get(url, allow_redirects = False) - - # Check if we're running the latest version - _, current = res.headers.get("location").rsplit("/", 1) - present = version("mkdocs-material") - if not present.startswith(current): - log.error("Please upgrade to the latest version.") - self._help_on_versions_and_exit(present, current) - - # Exit if archive creation is disabled - if not self.config.archive: - sys.exit(1) - - # Print message that we're creating a bug report - log.info("Started archive creation for bug report") - - # Check that there are no overrides in place - we need to use a little - # hack to detect whether the custom_dir setting was used without parsing - # mkdocs.yml again - we check at which position the directory provided - # by the theme resides, and if it's not the first one, abort. - if config.theme.dirs.index(get_theme_dir(config.theme.name)): - log.error("Please remove 'custom_dir' setting.") - self._help_on_customizations_and_exit() - - # Check that there are no hooks in place - hooks can alter the behavior - # of MkDocs in unpredictable ways, which is why they must be considered - # being customizations. Thus, we can't offer support for debugging and - # must abort here. - if config.hooks: - log.error("Please remove 'hooks' setting.") - self._help_on_customizations_and_exit() - - # Create in-memory archive and prompt user to enter a short descriptive - # name for the archive, which is also used as the directory name. Note - # that the name is slugified for better readability and stripped of any - # file extension that the user might have entered. - archive = BytesIO() - example = input("\nPlease name your bug report (2-4 words): ") - example, _ = os.path.splitext(example) - example = "-".join([present, slugify(example, "-")]) - - # Create self-contained example from project - files: list[str] = [] - with ZipFile(archive, "a", ZIP_DEFLATED, False) as f: - for path in ["mkdocs.yml", "requirements.txt"]: - if os.path.isfile(path): - f.write(path, os.path.join(example, path)) - - # Append all files visible to MkDocs - for file in get_files(config): - path = os.path.relpath(file.abs_src_path, os.path.curdir) - f.write(path, os.path.join(example, path)) - - # Add information on installed packages - f.writestr( - os.path.join(example, "requirements.lock.txt"), - "\n".join(sorted([ - "==".join([package.name, package.version]) - for package in distributions() - ])) - ) - - # Add information on platform - f.writestr( - os.path.join(example, "platform.json"), - json.dumps( - { - "system": platform.platform(), - "python": platform.python_version() - }, - default = str, - indent = 2 - ) - ) - - # Retrieve list of processed files - for a in f.filelist: - files.append("".join([ - Fore.LIGHTBLACK_EX, a.filename, " ", - _size(a.compress_size) - ])) - - # Finally, write archive to disk - buffer = archive.getbuffer() - with open(f"{example}.zip", "wb") as f: - f.write(archive.getvalue()) - - # Print summary - log.info("Archive successfully created:") - print(Style.NORMAL) - - # Print archive file names - files.sort() - for file in files: - print(f" {file}") - - # Print archive name - print(Style.RESET_ALL) - print("".join([ - " ", f.name, " ", - _size(buffer.nbytes, 10) - ])) - - # Print warning when file size is excessively large - print(Style.RESET_ALL) - if buffer.nbytes > 1000000: - log.warning("Archive exceeds recommended maximum size of 1 MB") - - # Aaaaaand done - sys.exit(1) - - # ------------------------------------------------------------------------- - - # Print help on versions and exit - def _help_on_versions_and_exit(self, have, need): - print(Fore.RED) - print(" When reporting issues, please first upgrade to the latest") - print(" version of Material for MkDocs, as the problem might already") - print(" be fixed in the latest version. This helps reduce duplicate") - print(" efforts and saves us maintainers time.") - print(Style.NORMAL) - print(f" Please update from {have} to {need}.") - print(Style.RESET_ALL) - print(f" pip install --upgrade --force-reinstall mkdocs-material") - print(Style.NORMAL) - - # Exit, unless explicitly told not to - if self.config.archive_stop_on_violation: - sys.exit(1) - - # Print help on customizations and exit - def _help_on_customizations_and_exit(self): - print(Fore.RED) - print(" When reporting issues, you must remove all customizations") - print(" and check if the problem persists. If not, the problem is") - print(" caused by your overrides. Please understand that we can't") - print(" help you debug your customizations. Please remove:") - print(Style.NORMAL) - print(" - theme.custom_dir") - print(" - hooks") - print(Fore.YELLOW) - print(" Additionally, please remove all third-party JavaScript or") - print(" CSS not explicitly mentioned in our documentation:") - print(Style.NORMAL) - print(" - extra_css") - print(" - extra_javascript") - print(Style.RESET_ALL) - - # Exit, unless explicitly told not to - if self.config.archive_stop_on_violation: - sys.exit(1) - -# ----------------------------------------------------------------------------- -# Helper functions -# ----------------------------------------------------------------------------- - -# Print human-readable size -def _size(value, factor = 1): - color = Fore.GREEN - if value > 100000 * factor: color = Fore.RED - elif value > 25000 * factor: color = Fore.YELLOW - for unit in ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB"]: - if abs(value) < 1000.0: - return f"{color}{value:3.1f} {unit}" - value /= 1000.0 - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.info") diff --git a/src/plugins/offline/__init__.py b/src/plugins/offline/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/offline/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/offline/config.py b/src/plugins/offline/config.py deleted file mode 100644 index 49f51a94..00000000 --- a/src/plugins/offline/config.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.config_options import Type -from mkdocs.config.base import Config - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Offline plugin configuration -class OfflineConfig(Config): - enabled = Type(bool, default = True) diff --git a/src/plugins/offline/plugin.py b/src/plugins/offline/plugin.py deleted file mode 100644 index abcb2598..00000000 --- a/src/plugins/offline/plugin.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import os - -from mkdocs.plugins import BasePlugin, event_priority - -from .config import OfflineConfig - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Offline plugin -class OfflinePlugin(BasePlugin[OfflineConfig]): - - # Set configuration for offline build - def on_config(self, config): - if not self.config.enabled: - return - - # Ensure correct resolution of links when viewing the site from the - # file system by disabling directory URLs - config.use_directory_urls = False - - # Append iframe-worker to polyfills/shims - config.extra["polyfills"] = config.extra.get("polyfills", []) - if not any("iframe-worker" in url for url in config.extra["polyfills"]): - script = "https://unpkg.com/iframe-worker/shim" - config.extra["polyfills"].append(script) - - # Add support for offline search (run latest) - the search index is copied - # and inlined into a script, so that it can be used without a server - @event_priority(-100) - def on_post_build(self, *, config): - if not self.config.enabled: - return - - # Ensure presence of search index - path = os.path.join(config.site_dir, "search") - file = os.path.join(path, "search_index.json") - if not os.path.isfile(file): - return - - # Obtain search index contents - with open(file, encoding = "utf-8") as f: - data = f.read() - - # Inline search index contents into script - file = os.path.join(path, "search_index.js") - with open(file, "w", encoding = "utf-8") as f: - f.write(f"var __index = {data}") diff --git a/src/plugins/search/__init__.py b/src/plugins/search/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/search/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/search/config.py b/src/plugins/search/config.py deleted file mode 100644 index e150fbb3..00000000 --- a/src/plugins/search/config.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.config_options import ( - Choice, - Deprecated, - Optional, - ListOfItems, - Type -) -from mkdocs.config.base import Config -from mkdocs.contrib.search import LangOption - -# ----------------------------------------------------------------------------- -# Options -# ----------------------------------------------------------------------------- - -# Options for search pipeline -pipeline = ("stemmer", "stopWordFilter", "trimmer") - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Search plugin configuration -class SearchConfig(Config): - enabled = Type(bool, default = True) - - # Settings for search - lang = Optional(LangOption()) - separator = Optional(Type(str)) - pipeline = ListOfItems(Choice(pipeline), default = []) - - # Settings for text segmentation (Chinese) - jieba_dict = Optional(Type(str)) - jieba_dict_user = Optional(Type(str)) - - # Unsupported settings, originally implemented in MkDocs - indexing = Deprecated(message = "Unsupported option") - prebuild_index = Deprecated(message = "Unsupported option") - min_search_length = Deprecated(message = "Unsupported option") diff --git a/src/plugins/search/plugin.py b/src/plugins/search/plugin.py deleted file mode 100644 index 5c254e3f..00000000 --- a/src/plugins/search/plugin.py +++ /dev/null @@ -1,580 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import json -import logging -import os -import regex as re - -from html import escape -from html.parser import HTMLParser -from mkdocs import utils -from mkdocs.plugins import BasePlugin - -from .config import SearchConfig - -try: - import jieba -except ImportError: - jieba = None - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Search plugin -class SearchPlugin(BasePlugin[SearchConfig]): - - # Initialize plugin - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Initialize incremental builds - self.is_dirtyreload = False - - # Initialize search index cache - self.search_index_prev = None - - # Determine whether we're serving the site - def on_startup(self, *, command, dirty): - self.is_dirty = dirty - - # Initialize plugin - def on_config(self, config): - if not self.config.enabled: - return - - # Retrieve default value for language - if not self.config.lang: - self.config.lang = [self._translate( - config, "search.config.lang" - )] - - # Retrieve default value for separator - if not self.config.separator: - self.config.separator = self._translate( - config, "search.config.separator" - ) - - # Retrieve default value for pipeline - if not self.config.pipeline: - self.config.pipeline = list(filter(len, re.split( - r"\s*,\s*", self._translate(config, "search.config.pipeline") - ))) - - # Initialize search index - self.search_index = SearchIndex(**self.config) - - # Set jieba dictionary, if given - if self.config.jieba_dict: - path = os.path.normpath(self.config.jieba_dict) - if os.path.isfile(path): - jieba.set_dictionary(path) - log.debug(f"Loading jieba dictionary: {path}") - else: - log.warning( - f"Configuration error for 'search.jieba_dict': " - f"'{self.config.jieba_dict}' does not exist." - ) - - # Set jieba user dictionary, if given - if self.config.jieba_dict_user: - path = os.path.normpath(self.config.jieba_dict_user) - if os.path.isfile(path): - jieba.load_userdict(path) - log.debug(f"Loading jieba user dictionary: {path}") - else: - log.warning( - f"Configuration error for 'search.jieba_dict_user': " - f"'{self.config.jieba_dict_user}' does not exist." - ) - - # Add page to search index - def on_page_context(self, context, *, page, config, nav): - if not self.config.enabled: - return - - # Index page - self.search_index.add_entry_from_context(page) - page.content = re.sub( - r"\s?data-search-\w+=\"[^\"]+\"", - "", - page.content - ) - - # Generate search index - def on_post_build(self, *, config): - if not self.config.enabled: - return - - # Write search index - base = os.path.join(config.site_dir, "search") - path = os.path.join(base, "search_index.json") - - # Generate and write search index to file - data = self.search_index.generate_search_index(self.search_index_prev) - utils.write_file(data.encode("utf-8"), path) - - # Persist search index for repeated invocation - if self.is_dirty: - self.search_index_prev = self.search_index - - # Determine whether we're running under dirty reload - def on_serve(self, server, *, config, builder): - self.is_dirtyreload = self.is_dirty - - # ------------------------------------------------------------------------- - - # Translate the given placeholder value - def _translate(self, config, value): - env = config.theme.get_env() - - # Load language template and return translation for placeholder - language = "partials/language.html" - template = env.get_template(language, None, { "config": config }) - return template.module.t(value) - -# ----------------------------------------------------------------------------- - -# Search index with support for additional fields -class SearchIndex: - - # Initialize search index - def __init__(self, **config): - self.config = config - self.entries = [] - - # Add page to search index - def add_entry_from_context(self, page): - search = page.meta.get("search", {}) - if search.get("exclude"): - return - - # Divide page content into sections - parser = Parser() - parser.feed(page.content) - parser.close() - - # Add sections to index - for section in parser.data: - if not section.is_excluded(): - self.create_entry_for_section(section, page.toc, page.url, page) - - # Override: graceful indexing and additional fields - def create_entry_for_section(self, section, toc, url, page): - item = self._find_toc_by_id(toc, section.id) - if item: - url = url + item.url - elif section.id: - url = url + "#" + section.id - - # Set page title as section title if none was given, which happens when - # the first headline in a Markdown document is not a h1 headline. Also, - # if a page title was set via front matter, use that even though a h1 - # might be given or the page name was specified in nav in mkdocs.yml - if not section.title: - section.title = [str(page.meta.get("title", page.title))] - - # Compute title and text - title = "".join(section.title).strip() - text = "".join(section.text).strip() - - # Segment Chinese characters if jieba is available - if jieba: - title = self._segment_chinese(title) - text = self._segment_chinese(text) - - # Create entry for section - entry = { - "location": url, - "title": title, - "text": text - } - - # Set document tags - tags = page.meta.get("tags") - if isinstance(tags, list): - entry["tags"] = [] - for name in tags: - if name and isinstance(name, (str, int, float, bool)): - entry["tags"].append(name) - - # Set document boost - search = page.meta.get("search", {}) - if "boost" in search: - entry["boost"] = search["boost"] - - # Add entry to index - self.entries.append(entry) - - # Generate search index - def generate_search_index(self, prev): - config = { - key: self.config[key] - for key in ["lang", "separator", "pipeline"] - } - - # Hack: if we're running under dirty reload, the search index will only - # include the entries for the current page. However, MkDocs > 1.4 allows - # us to persist plugin state across rebuilds, which is exactly what we - # do by passing the previously built index to this method. Thus, we just - # remove the previous entries for the current page, and append the new - # entries to the end of the index, as order doesn't matter. - if prev and self.entries: - path = self.entries[0]["location"] - - # Since we're sure that we're running under dirty reload, the list - # of entries will only contain sections for a single page. Thus, we - # use the first entry to remove all entries from the previous run - # that belong to the current page. The rationale behind this is that - # authors might add or remove section headers, so we need to make - # sure that sections are synchronized correctly. - entries = [ - entry for entry in prev.entries - if not entry["location"].startswith(path) - ] - - # Merge previous with current entries - self.entries = entries + self.entries - - # Otherwise just set previous entries - if prev and not self.entries: - self.entries = prev.entries - - # Return search index as JSON - data = { "config": config, "docs": self.entries } - return json.dumps( - data, - separators = (",", ":"), - default = str - ) - - # ------------------------------------------------------------------------- - - # Retrieve item for anchor - def _find_toc_by_id(self, toc, id): - for toc_item in toc: - if toc_item.id == id: - return toc_item - - # Recurse into children of item - toc_item = self._find_toc_by_id(toc_item.children, id) - if toc_item is not None: - return toc_item - - # No item found - return None - - # Find and segment Chinese characters in string - def _segment_chinese(self, data): - expr = re.compile(r"(\p{IsHan}+)", re.UNICODE) - - # Replace callback - def replace(match): - value = match.group(0) - - # Replace occurrence in original string with segmented version and - # surround with zero-width whitespace for efficient indexing - return "".join([ - "\u200b", - "\u200b".join(jieba.cut(value.encode("utf-8"))), - "\u200b", - ]) - - # Return string with segmented occurrences - return expr.sub(replace, data).strip("\u200b") - -# ----------------------------------------------------------------------------- - -# HTML element -class Element: - """ - An element with attributes, essentially a small wrapper object for the - parser to access attributes in other callbacks than handle_starttag. - """ - - # Initialize HTML element - def __init__(self, tag, attrs = {}): - self.tag = tag - self.attrs = attrs - - # String representation - def __repr__(self): - return self.tag - - # Support comparison (compare by tag only) - def __eq__(self, other): - if other is Element: - return self.tag == other.tag - else: - return self.tag == other - - # Support set operations - def __hash__(self): - return hash(self.tag) - - # Check whether the element should be excluded - def is_excluded(self): - return "data-search-exclude" in self.attrs - -# ----------------------------------------------------------------------------- - -# HTML section -class Section: - """ - A block of text with markup, preceded by a title (with markup), i.e., a - headline with a certain level (h1-h6). Internally used by the parser. - """ - - # Initialize HTML section - def __init__(self, el, depth = 0): - self.el = el - self.depth = depth - - # Initialize section data - self.text = [] - self.title = [] - self.id = None - - # String representation - def __repr__(self): - if self.id: - return "#".join([self.el.tag, self.id]) - else: - return self.el.tag - - # Check whether the section should be excluded - def is_excluded(self): - return self.el.is_excluded() - -# ----------------------------------------------------------------------------- - -# HTML parser -class Parser(HTMLParser): - """ - This parser divides the given string of HTML into a list of sections, each - of which are preceded by a h1-h6 level heading. A white- and blacklist of - tags dictates which tags should be preserved as part of the index, and - which should be ignored in their entirety. - """ - - # Initialize HTML parser - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Tags to skip - self.skip = set([ - "object", # Objects - "script", # Scripts - "style" # Styles - ]) - - # Tags to keep - self.keep = set([ - "p", # Paragraphs - "code", "pre", # Code blocks - "li", "ol", "ul", # Lists - "sub", "sup" # Sub- and superscripts - ]) - - # Current context and section - self.context = [] - self.section = None - - # All parsed sections - self.data = [] - - # Called at the start of every HTML tag - def handle_starttag(self, tag, attrs): - attrs = dict(attrs) - - # Ignore self-closing tags - el = Element(tag, attrs) - if not tag in void: - self.context.append(el) - else: - return - - # Handle heading - if tag in ([f"h{x}" for x in range(1, 7)]): - depth = len(self.context) - if "id" in attrs: - - # Ensure top-level section - if tag != "h1" and not self.data: - self.section = Section(Element("hx"), depth) - self.data.append(self.section) - - # Set identifier, if not first section - self.section = Section(el, depth) - if self.data: - self.section.id = attrs["id"] - - # Append section to list - self.data.append(self.section) - - # Handle preface - ensure top-level section - if not self.section: - self.section = Section(Element("hx")) - self.data.append(self.section) - - # Handle special cases to skip - for key, value in attrs.items(): - - # Skip block if explicitly excluded from search - if key == "data-search-exclude": - self.skip.add(el) - return - - # Skip line numbers - see https://bit.ly/3GvubZx - if key == "class" and value == "linenodiv": - self.skip.add(el) - return - - # Render opening tag if kept - if not self.skip.intersection(self.context): - if tag in self.keep: - - # Check whether we're inside the section title - data = self.section.text - if self.section.el in self.context: - data = self.section.title - - # Append to section title or text - data.append(f"<{tag}>") - - # Called at the end of every HTML tag - def handle_endtag(self, tag): - if not self.context or self.context[-1] != tag: - return - - # Check whether we're exiting the current context, which happens when - # a headline is nested in another element. In that case, we close the - # current section, continuing to append data to the previous section, - # which could also be a nested section – see https://bit.ly/3IxxIJZ - if self.section.depth > len(self.context): - for section in reversed(self.data): - if section.depth <= len(self.context): - - # Set depth to infinity in order to denote that the current - # section is exited and must never be considered again. - self.section.depth = float("inf") - self.section = section - break - - # Remove element from skip list - el = self.context.pop() - if el in self.skip: - if el.tag not in ["script", "style", "object"]: - self.skip.remove(el) - return - - # Render closing tag if kept - if not self.skip.intersection(self.context): - if tag in self.keep: - - # Check whether we're inside the section title - data = self.section.text - if self.section.el in self.context: - data = self.section.title - - # Search for corresponding opening tag - index = data.index(f"<{tag}>") - for i in range(index + 1, len(data)): - if not data[i].isspace(): - index = len(data) - break - - # Remove element if empty (or only whitespace) - if len(data) > index: - while len(data) > index: - data.pop() - - # Append to section title or text - else: - data.append(f"") - - # Called for the text contents of each tag - def handle_data(self, data): - if self.skip.intersection(self.context): - return - - # Collapse whitespace in non-pre contexts - if not "pre" in self.context: - if not data.isspace(): - data = data.replace("\n", " ") - else: - data = " " - - # Handle preface - ensure top-level section - if not self.section: - self.section = Section(Element("hx")) - self.data.append(self.section) - - # Handle section headline - if self.section.el in self.context: - permalink = False - for el in self.context: - if el.tag == "a" and el.attrs.get("class") == "headerlink": - permalink = True - - # Ignore permalinks - if not permalink: - self.section.title.append( - escape(data, quote = False) - ) - - # Collapse adjacent whitespace - elif data.isspace(): - if not self.section.text or not self.section.text[-1].isspace(): - self.section.text.append(data) - elif "pre" in self.context: - self.section.text.append(data) - - # Handle everything else - else: - self.section.text.append( - escape(data, quote = False) - ) - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.search") - -# Tags that are self-closing -void = set([ - "area", # Image map areas - "base", # Document base - "br", # Line breaks - "col", # Table columns - "embed", # External content - "hr", # Horizontal rules - "img", # Images - "input", # Input fields - "link", # Links - "meta", # Metadata - "param", # External parameters - "source", # Image source sets - "track", # Text track - "wbr" # Line break opportunities -]) diff --git a/src/plugins/social/__init__.py b/src/plugins/social/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/plugins/social/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/plugins/social/config.py b/src/plugins/social/config.py deleted file mode 100644 index 2d87c25e..00000000 --- a/src/plugins/social/config.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from mkdocs.config.base import Config -from mkdocs.config.config_options import Deprecated, Type - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Social plugin configuration -class SocialConfig(Config): - enabled = Type(bool, default = True) - cache_dir = Type(str, default = ".cache/plugin/social") - - # Settings for social cards - cards = Type(bool, default = True) - cards_dir = Type(str, default = "assets/images/social") - cards_layout_options = Type(dict, default = {}) - - # Deprecated settings - cards_color = Deprecated( - option_type = Type(dict, default = {}), - message = - "Deprecated, use 'cards_layout_options.background_color' " - "and 'cards_layout_options.color' with 'default' layout" - ) - cards_font = Deprecated( - option_type = Type(str), - message = "Deprecated, use 'cards_layout_options.font_family'" - ) diff --git a/src/plugins/social/plugin.py b/src/plugins/social/plugin.py deleted file mode 100644 index 3cdfa3ce..00000000 --- a/src/plugins/social/plugin.py +++ /dev/null @@ -1,516 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -# ----------------------------------------------------------------------------- -# Disclaimer -# ----------------------------------------------------------------------------- -# Please note: this version of the social plugin is not actively development -# anymore. Instead, Material for MkDocs Insiders ships a complete rewrite of -# the plugin which is much more powerful and addresses all shortcomings of -# this implementation. Additionally, the new social plugin allows to create -# entirely custom social cards. You can probably imagine, that this was a lot -# of work to pull off. If you run into problems, or want to have additional -# functionality, please consider sponsoring the project. You can then use the -# new version of the plugin immediately. -# ----------------------------------------------------------------------------- - -import concurrent.futures -import functools -import logging -import os -import posixpath -import re -import requests -import sys - -from collections import defaultdict -from hashlib import md5 -from io import BytesIO -from mkdocs.commands.build import DuplicateFilter -from mkdocs.exceptions import PluginError -from mkdocs.plugins import BasePlugin -from shutil import copyfile -from tempfile import TemporaryFile -from zipfile import ZipFile -try: - from cairosvg import svg2png - from PIL import Image, ImageDraw, ImageFont -except ImportError: - pass - -from .config import SocialConfig - - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Social plugin -class SocialPlugin(BasePlugin[SocialConfig]): - - def __init__(self): - self._executor = concurrent.futures.ThreadPoolExecutor(4) - - # Retrieve configuration - def on_config(self, config): - self.color = colors.get("indigo") - self.config.cards = self.config.enabled - if not self.config.cards: - return - - # Check dependencies - if "Image" not in globals(): - raise PluginError( - "Required dependencies of \"social\" plugin not found. " - "Install with: pip install \"mkdocs-material[imaging]\"" - ) - - # Move color options - if self.config.cards_color: - - # Move background color to new option - value = self.config.cards_color.get("fill") - if value: - self.config.cards_layout_options["background_color"] = value - - # Move color to new option - value = self.config.cards_color.get("text") - if value: - self.config.cards_layout_options["color"] = value - - # Move font family to new option - if self.config.cards_font: - value = self.config.cards_font - self.config.cards_layout_options["font_family"] = value - - # Check if site URL is defined - if not config.site_url: - log.warning( - "The \"site_url\" option is not set. The cards are generated, " - "but not linked, so they won't be visible on social media." - ) - - # Ensure presence of cache directory - self.cache = self.config.cache_dir - if not os.path.isdir(self.cache): - os.makedirs(self.cache) - - # Retrieve palette from theme configuration - theme = config.theme - if "palette" in theme: - palette = theme["palette"] - - # Use first palette, if multiple are defined - if isinstance(palette, list): - palette = palette[0] - - # Set colors according to palette - if "primary" in palette and palette["primary"]: - primary = palette["primary"].replace(" ", "-") - self.color = colors.get(primary, self.color) - - # Retrieve color overrides - options = self.config.cards_layout_options - self.color = { - "fill": options.get("background_color", self.color["fill"]), - "text": options.get("color", self.color["text"]) - } - - # Retrieve logo and font - self._resized_logo_promise = self._executor.submit(self._load_resized_logo, config) - self.font = self._load_font(config) - - self._image_promises = [] - - # Create social cards - def on_page_markdown(self, markdown, page, config, files): - if not self.config.cards: - return - - # Resolve image directory - directory = self.config.cards_dir - file, _ = os.path.splitext(page.file.src_path) - - # Resolve path of image - path = "{}.png".format(os.path.join( - config.site_dir, - directory, - file - )) - - # Resolve path of image directory - directory = os.path.dirname(path) - if not os.path.isdir(directory): - os.makedirs(directory) - - # Compute site name - site_name = config.site_name - - # Compute page title and description - title = page.meta.get("title", page.title) - description = config.site_description or "" - if "description" in page.meta: - description = page.meta["description"] - - # Check type of meta title - see https://t.ly/m1Us - if not isinstance(title, str): - log.error( - f"Page meta title of page '{page.file.src_uri}' must be a " - f"string, but is of type \"{type(title)}\"." - ) - sys.exit(1) - - # Check type of meta description - see https://t.ly/m1Us - if not isinstance(description, str): - log.error( - f"Page meta description of '{page.file.src_uri}' must be a " - f"string, but is of type \"{type(description)}\"." - ) - sys.exit(1) - - # Generate social card if not in cache - hash = md5("".join([ - site_name, - str(title), - description - ]).encode("utf-8")) - file = os.path.join(self.cache, f"{hash.hexdigest()}.png") - self._image_promises.append(self._executor.submit( - self._cache_image, - cache_path = file, dest_path = path, - render_function = lambda: self._render_card(site_name, title, description) - )) - - # Inject meta tags into page - meta = page.meta.get("meta", []) - page.meta["meta"] = meta + self._generate_meta(page, config) - - def on_post_build(self, config): - if not self.config.cards: - return - - # Check for exceptions - for promise in self._image_promises: - promise.result() - - # ------------------------------------------------------------------------- - - # Render image to cache (if not present), then copy from cache to site - def _cache_image(self, cache_path, dest_path, render_function): - if not os.path.isfile(cache_path): - image = render_function() - image.save(cache_path) - - # Copy file from cache - copyfile(cache_path, dest_path) - - @functools.lru_cache(maxsize=None) - def _get_font(self, kind, size): - return ImageFont.truetype(self.font[kind], size) - - # Render social card - def _render_card(self, site_name, title, description): - # Render background and logo - image = self._render_card_background((1200, 630), self.color["fill"]) - image.alpha_composite( - self._resized_logo_promise.result(), - (1200 - 228, 64 - 4) - ) - - # Render site name - font = self._get_font("Bold", 36) - image.alpha_composite( - self._render_text((826, 48), font, site_name, 1, 20), - (64 + 4, 64) - ) - - # Render page title - font = self._get_font("Bold", 92) - image.alpha_composite( - self._render_text((826, 328), font, title, 3, 30), - (64, 160) - ) - - # Render page description - font = self._get_font("Regular", 28) - image.alpha_composite( - self._render_text((826, 80), font, description, 2, 14), - (64 + 4, 512) - ) - - # Return social card image - return image - - # Render social card background - def _render_card_background(self, size, fill): - return Image.new(mode = "RGBA", size = size, color = fill) - - @functools.lru_cache(maxsize=None) - def _tmp_context(self): - image = Image.new(mode = "RGBA", size = (50, 50)) - return ImageDraw.Draw(image) - - @functools.lru_cache(maxsize=None) - def _text_bounding_box(self, text, font): - return self._tmp_context().textbbox((0, 0), text, font = font) - - # Render social card text - def _render_text(self, size, font, text, lmax, spacing = 0): - width = size[0] - lines, words = [], [] - - # Remove remnant HTML tags - text = re.sub(r"(<[^>]+>)", "", text) - - # Retrieve y-offset of textbox to correct for spacing - yoffset = 0 - - # Create drawing context and split text into lines - for word in text.split(" "): - combine = " ".join(words + [word]) - textbox = self._text_bounding_box(combine, font = font) - yoffset = textbox[1] - if not words or textbox[2] <= width: - words.append(word) - else: - lines.append(words) - words = [word] - - # Join words for each line and create image - lines.append(words) - lines = [" ".join(line) for line in lines] - image = Image.new(mode = "RGBA", size = size) - - # Create drawing context and split text into lines - context = ImageDraw.Draw(image) - context.text( - (0, spacing / 2 - yoffset), "\n".join(lines[:lmax]), - font = font, fill = self.color["text"], spacing = spacing - yoffset - ) - - # Return text image - return image - - # ------------------------------------------------------------------------- - - # Generate meta tags - def _generate_meta(self, page, config): - directory = self.config.cards_dir - file, _ = os.path.splitext(page.file.src_uri) - - # Compute page title - title = page.meta.get("title", page.title) - if not page.is_homepage: - title = f"{title} - {config.site_name}" - - # Compute page description - description = config.site_description - if "description" in page.meta: - description = page.meta["description"] - - # Resolve image URL - url = "{}.png".format(posixpath.join( - config.site_url or ".", - directory, - file - )) - - # Ensure forward slashes - url = url.replace(os.path.sep, "/") - - # Return meta tags - return [ - - # Meta tags for Open Graph - { "property": "og:type", "content": "website" }, - { "property": "og:title", "content": title }, - { "property": "og:description", "content": description }, - { "property": "og:image", "content": url }, - { "property": "og:image:type", "content": "image/png" }, - { "property": "og:image:width", "content": "1200" }, - { "property": "og:image:height", "content": "630" }, - { "property": "og:url", "content": page.canonical_url }, - - # Meta tags for Twitter - { "name": "twitter:card", "content": "summary_large_image" }, - # { "name": "twitter:site", "content": user }, - # { "name": "twitter:creator", "content": user }, - { "name": "twitter:title", "content": title }, - { "name": "twitter:description", "content": description }, - { "name": "twitter:image", "content": url } - ] - - def _load_resized_logo(self, config, width = 144): - logo = self._load_logo(config) - height = int(width * logo.height / logo.width) - return logo.resize((width, height)) - - # Retrieve logo image or icon - def _load_logo(self, config): - theme = config.theme - - # Handle images (precedence over icons) - if "logo" in theme: - _, extension = os.path.splitext(theme["logo"]) - - path = os.path.join(config.docs_dir, theme["logo"]) - - # Allow users to put the logo inside their custom_dir (theme["logo"] case) - if theme.custom_dir: - custom_dir_logo = os.path.join(theme.custom_dir, theme["logo"]) - if os.path.exists(custom_dir_logo): - path = custom_dir_logo - - # Load SVG and convert to PNG - if extension == ".svg": - return self._load_logo_svg(path) - - # Load PNG, JPEG, etc. - return Image.open(path).convert("RGBA") - - # Handle icons - icon = theme["icon"] or {} - if "logo" in icon and icon["logo"]: - logo = icon["logo"] - else: - logo = "material/library" - - # Resolve path of package - base = os.path.abspath(os.path.join( - os.path.dirname(__file__), - "../.." - )) - - path = f"{base}/templates/.icons/{logo}.svg" - - # Allow users to put the logo inside their custom_dir (theme["icon"]["logo"] case) - if theme.custom_dir: - custom_dir_logo = os.path.join(theme.custom_dir, ".icons", f"{logo}.svg") - if os.path.exists(custom_dir_logo): - path = custom_dir_logo - - # Load icon data and fill with color - return self._load_logo_svg(path, self.color["text"]) - - # Load SVG file and convert to PNG - def _load_logo_svg(self, path, fill = None): - file = BytesIO() - data = open(path).read() - - # Fill with color, if given - if fill: - data = data.replace(" - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -# ----------------------------------------------------------------------------- -# Functions -# ----------------------------------------------------------------------------- - -# Casefold a string for comparison when sorting -def casefold(tag: str): - return tag.casefold() diff --git a/src/plugins/tags/config.py b/src/plugins/tags/config.py deleted file mode 100644 index f2d95084..00000000 --- a/src/plugins/tags/config.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -from functools import partial -from markdown.extensions.toc import slugify -from mkdocs.config.config_options import Optional, Type -from mkdocs.config.base import Config - -from . import casefold - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Tags plugin configuration -class TagsConfig(Config): - enabled = Type(bool, default = True) - - # Settings for tags - tags = Type(bool, default = True) - tags_file = Optional(Type(str)) diff --git a/src/plugins/tags/plugin.py b/src/plugins/tags/plugin.py deleted file mode 100644 index e5ce6bde..00000000 --- a/src/plugins/tags/plugin.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import logging -import sys - -from collections import defaultdict -from markdown.extensions.toc import slugify -from mkdocs import utils -from mkdocs.plugins import BasePlugin - -# deprecated, but kept for downward compatibility. Use 'material.plugins.tags' -# as an import source instead. This import is removed in the next major version. -from . import casefold -from .config import TagsConfig - -# ----------------------------------------------------------------------------- -# Classes -# ----------------------------------------------------------------------------- - -# Tags plugin -class TagsPlugin(BasePlugin[TagsConfig]): - supports_multiple_instances = True - - # Initialize plugin - def on_config(self, config): - if not self.config.enabled: - return - - # Skip if tags should not be built - if not self.config.tags: - return - - # Initialize tags - self.tags = defaultdict(list) - self.tags_file = None - - # Retrieve tags mapping from configuration - self.tags_map = config.extra.get("tags") - - # Use override of slugify function - toc = { "slugify": slugify, "separator": "-" } - if "toc" in config.mdx_configs: - toc = { **toc, **config.mdx_configs["toc"] } - - # Partially apply slugify function - self.slugify = lambda value: ( - toc["slugify"](str(value), toc["separator"]) - ) - - # Hack: 2nd pass for tags index page(s) - def on_nav(self, nav, config, files): - if not self.config.enabled: - return - - # Skip if tags should not be built - if not self.config.tags: - return - - # Resolve tags index page - file = self.config.tags_file - if file: - self.tags_file = self._get_tags_file(files, file) - - # Build and render tags index page - def on_page_markdown(self, markdown, page, config, files): - if not self.config.enabled: - return - - # Skip if tags should not be built - if not self.config.tags: - return - - # Skip, if page is excluded - if page.file.inclusion.is_excluded(): - return - - # Render tags index page - if page.file == self.tags_file: - return self._render_tag_index(markdown) - - # Add page to tags index - for tag in page.meta.get("tags", []): - self.tags[tag].append(page) - - # Inject tags into page (after search and before minification) - def on_page_context(self, context, page, config, nav): - if not self.config.enabled: - return - - # Skip if tags should not be built - if not self.config.tags: - return - - # Provide tags for page - if "tags" in page.meta: - context["tags"] = [ - self._render_tag(tag) - for tag in page.meta["tags"] - ] - - # ------------------------------------------------------------------------- - - # Obtain tags file - def _get_tags_file(self, files, path): - file = files.get_file_from_path(path) - if not file: - log.error(f"Tags file '{path}' does not exist.") - sys.exit(1) - - # Add tags file to files - files.append(file) - return file - - # Render tags index - def _render_tag_index(self, markdown): - if not "[TAGS]" in markdown: - markdown += "\n[TAGS]" - - # Replace placeholder in Markdown with rendered tags index - return markdown.replace("[TAGS]", "\n".join([ - self._render_tag_links(*args) - for args in sorted(self.tags.items()) - ])) - - # Render the given tag and links to all pages with occurrences - def _render_tag_links(self, tag, pages): - classes = ["md-tag"] - if isinstance(self.tags_map, dict): - classes.append("md-tag-icon") - type = self.tags_map.get(tag) - if type: - classes.append(f"md-tag--{type}") - - # Render section for tag and a link to each page - classes = " ".join(classes) - content = [f"## {tag}", ""] - for page in pages: - url = utils.get_relative_url( - page.file.src_uri, - self.tags_file.src_uri - ) - - # Render link to page - title = page.meta.get("title", page.title) - content.append(f"- [{title}]({url})") - - # Return rendered tag links - return "\n".join(content) - - # Render the given tag, linking to the tags index (if enabled) - def _render_tag(self, tag): - type = self.tags_map.get(tag) if self.tags_map else None - if not self.tags_file or not self.slugify: - return dict(name = tag, type = type) - else: - url = f"{self.tags_file.url}#{self.slugify(tag)}" - return dict(name = tag, type = type, url = url) - -# ----------------------------------------------------------------------------- -# Data -# ----------------------------------------------------------------------------- - -# Set up logging -log = logging.getLogger("mkdocs.material.tags") diff --git a/src/templates/.icons/logo.afdesign b/src/templates/.icons/logo.afdesign deleted file mode 100644 index 07f57d0a..00000000 Binary files a/src/templates/.icons/logo.afdesign and /dev/null differ diff --git a/src/templates/.icons/logo.svg b/src/templates/.icons/logo.svg deleted file mode 100644 index 763eb2c2..00000000 --- a/src/templates/.icons/logo.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/templates/404.html b/src/templates/404.html deleted file mode 100644 index e87e7783..00000000 --- a/src/templates/404.html +++ /dev/null @@ -1,28 +0,0 @@ - - -{% extends "main.html" %} - - -{% block content %} -

    404 - Not found

    -{% endblock %} diff --git a/src/templates/__init__.py b/src/templates/__init__.py deleted file mode 100644 index d1899378..00000000 --- a/src/templates/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. diff --git a/src/templates/assets/images/favicon.png b/src/templates/assets/images/favicon.png deleted file mode 100644 index 1cf13b9f..00000000 Binary files a/src/templates/assets/images/favicon.png and /dev/null differ diff --git a/src/templates/assets/javascripts/_/index.ts b/src/templates/assets/javascripts/_/index.ts deleted file mode 100644 index be0f4a42..00000000 --- a/src/templates/assets/javascripts/_/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { getElement, getLocation } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Feature flag - */ -export type Flag = - | "announce.dismiss" /* Dismissable announcement bar */ - | "content.code.annotate" /* Code annotations */ - | "content.code.copy" /* Code copy button */ - | "content.lazy" /* Lazy content elements */ - | "content.tabs.link" /* Link content tabs */ - | "header.autohide" /* Hide header */ - | "navigation.expand" /* Automatic expansion */ - | "navigation.indexes" /* Section pages */ - | "navigation.instant" /* Instant navigation */ - | "navigation.instant.progress" /* Instant navigation progress */ - | "navigation.sections" /* Section navigation */ - | "navigation.tabs" /* Tabs navigation */ - | "navigation.tabs.sticky" /* Tabs navigation (sticky) */ - | "navigation.top" /* Back-to-top button */ - | "navigation.tracking" /* Anchor tracking */ - | "search.highlight" /* Search highlighting */ - | "search.share" /* Search sharing */ - | "search.suggest" /* Search suggestions */ - | "toc.follow" /* Following table of contents */ - | "toc.integrate" /* Integrated table of contents */ - -/* ------------------------------------------------------------------------- */ - -/** - * Translation - */ -export type Translation = - | "clipboard.copy" /* Copy to clipboard */ - | "clipboard.copied" /* Copied to clipboard */ - | "search.result.placeholder" /* Type to start searching */ - | "search.result.none" /* No matching documents */ - | "search.result.one" /* 1 matching document */ - | "search.result.other" /* # matching documents */ - | "search.result.more.one" /* 1 more on this page */ - | "search.result.more.other" /* # more on this page */ - | "search.result.term.missing" /* Missing */ - | "select.version" /* Version selector */ - -/** - * Translations - */ -export type Translations = - Record - -/* ------------------------------------------------------------------------- */ - -/** - * Versioning - */ -export interface Versioning { - provider: "mike" /* Version provider */ - default?: string | string[] /* Default version */ -} - -/** - * Configuration - */ -export interface Config { - base: string /* Base URL */ - features: Flag[] /* Feature flags */ - translations: Translations /* Translations */ - search: string /* Search worker URL */ - tags?: Record /* Tags mapping */ - version?: Versioning /* Versioning */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Retrieve global configuration and make base URL absolute - */ -const script = getElement("#__config") -const config: Config = JSON.parse(script.textContent!) -config.base = `${new URL(config.base, getLocation())}` - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve global configuration - * - * @returns Global configuration - */ -export function configuration(): Config { - return config -} - -/** - * Check whether a feature flag is enabled - * - * @param flag - Feature flag - * - * @returns Test result - */ -export function feature(flag: Flag): boolean { - return config.features.includes(flag) -} - -/** - * Retrieve the translation for the given key - * - * @param key - Key to be translated - * @param value - Positional value, if any - * - * @returns Translation - */ -export function translation( - key: Translation, value?: string | number -): string { - return typeof value !== "undefined" - ? config.translations[key].replace("#", value.toString()) - : config.translations[key] -} diff --git a/src/templates/assets/javascripts/browser/document/index.ts b/src/templates/assets/javascripts/browser/document/index.ts deleted file mode 100644 index 354c9b5c..00000000 --- a/src/templates/assets/javascripts/browser/document/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - const document$ = new ReplaySubject(1) - fromEvent(document, "DOMContentLoaded", { once: true }) - .subscribe(() => document$.next(document)) - - /* Return document */ - return document$ -} diff --git a/src/templates/assets/javascripts/browser/element/_/.eslintrc b/src/templates/assets/javascripts/browser/element/_/.eslintrc deleted file mode 100644 index 16973760..00000000 --- a/src/templates/assets/javascripts/browser/element/_/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "jsdoc/require-jsdoc": "off", - "jsdoc/require-returns-check": "off" - } -} diff --git a/src/templates/assets/javascripts/browser/element/_/index.ts b/src/templates/assets/javascripts/browser/element/_/index.ts deleted file mode 100644 index b7beb462..00000000 --- a/src/templates/assets/javascripts/browser/element/_/index.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T][] - -export function getElements( - selector: string, node?: ParentNode -): T[] - -export function getElements( - selector: string, node: ParentNode = document -): T[] { - return Array.from(node.querySelectorAll(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( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T] - -export function getElement( - selector: string, node?: ParentNode -): T - -export function getElement( - selector: string, node: ParentNode = document -): T { - const el = getOptionalElement(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( - selector: T, node?: ParentNode -): HTMLElementTagNameMap[T] | undefined - -export function getOptionalElement( - selector: string, node?: ParentNode -): T | undefined - -export function getOptionalElement( - selector: string, node: ParentNode = document -): T | undefined { - return node.querySelector(selector) || undefined -} - -/** - * Retrieve the currently active element - * - * @returns Element or nothing - */ -export function getActiveElement(): HTMLElement | undefined { - return document.activeElement instanceof HTMLElement - ? document.activeElement || undefined - : undefined -} diff --git a/src/templates/assets/javascripts/browser/element/focus/index.ts b/src/templates/assets/javascripts/browser/element/focus/index.ts deleted file mode 100644 index f31fe276..00000000 --- a/src/templates/assets/javascripts/browser/element/focus/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - debounceTime, - distinctUntilChanged, - fromEvent, - map, - merge, - shareReplay, - startWith -} from "rxjs" - -import { getActiveElement } from "../_" - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Focus observable - * - * Previously, this observer used `focus` and `blur` events to determine whether - * an element is focused, but this doesn't work if there are focusable elements - * within the elements itself. A better solutions are `focusin` and `focusout` - * events, which bubble up the tree and allow for more fine-grained control. - * - * `debounceTime` is necessary, because when a focus change happens inside an - * element, the observable would first emit `false` and then `true` again. - */ -const observer$ = merge( - fromEvent(document.body, "focusin"), - fromEvent(document.body, "focusout") -) - .pipe( - debounceTime(1), - startWith(undefined), - map(() => getActiveElement() || document.body), - shareReplay(1) - ) - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch element focus - * - * @param el - Element - * - * @returns Element focus observable - */ -export function watchElementFocus( - el: HTMLElement -): Observable { - return observer$ - .pipe( - map(active => el.contains(active)), - distinctUntilChanged() - ) -} diff --git a/src/templates/assets/javascripts/browser/element/index.ts b/src/templates/assets/javascripts/browser/element/index.ts deleted file mode 100644 index 50ce84b2..00000000 --- a/src/templates/assets/javascripts/browser/element/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./focus" -export * from "./offset" -export * from "./size" -export * from "./visibility" diff --git a/src/templates/assets/javascripts/browser/element/offset/_/index.ts b/src/templates/assets/javascripts/browser/element/offset/_/index.ts deleted file mode 100644 index 6dd229d5..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/_/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - return merge( - fromEvent(window, "load"), - fromEvent(window, "resize") - ) - .pipe( - auditTime(0, animationFrameScheduler), - map(() => getElementOffset(el)), - startWith(getElementOffset(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/offset/content/index.ts b/src/templates/assets/javascripts/browser/element/offset/content/index.ts deleted file mode 100644 index 557301a6..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/content/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - return merge( - fromEvent(el, "scroll"), - fromEvent(window, "resize") - ) - .pipe( - auditTime(0, animationFrameScheduler), - map(() => getElementContentOffset(el)), - startWith(getElementContentOffset(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/offset/index.ts b/src/templates/assets/javascripts/browser/element/offset/index.ts deleted file mode 100644 index 602ff2cf..00000000 --- a/src/templates/assets/javascripts/browser/element/offset/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/size/_/index.ts b/src/templates/assets/javascripts/browser/element/size/_/index.ts deleted file mode 100644 index 35a5e68b..00000000 --- a/src/templates/assets/javascripts/browser/element/size/_/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - NEVER, - Observable, - Subject, - defer, - filter, - finalize, - map, - merge, - of, - shareReplay, - startWith, - switchMap, - tap -} from "rxjs" - -import { watchScript } from "../../../script" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Element offset - */ -export interface ElementSize { - width: number /* Element width */ - height: number /* Element height */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Resize observer entry subject - */ -const entry$ = new Subject() - -/** - * Resize observer observable - * - * This observable will create a `ResizeObserver` on the first subscription - * and will automatically terminate it when there are no more subscribers. - * It's quite important to centralize observation in a single `ResizeObserver`, - * as the performance difference can be quite dramatic, as the link shows. - * - * If the browser doesn't have a `ResizeObserver` implementation available, a - * polyfill is automatically downloaded from unpkg.com. This is also compatible - * with the built-in privacy plugin, which will download the polyfill and put - * it alongside the built site for self-hosting. - * - * @see https://bit.ly/3iIYfEm - Google Groups on performance - */ -const observer$ = defer(() => ( - typeof ResizeObserver === "undefined" - ? watchScript("https://unpkg.com/resize-observer-polyfill") - : of(undefined) -)) - .pipe( - map(() => new ResizeObserver(entries => { - for (const entry of entries) - entry$.next(entry) - })), - switchMap(observer => merge(NEVER, of(observer)) - .pipe( - finalize(() => observer.disconnect()) - ) - ), - shareReplay(1) - ) - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element size - * - * @param el - Element - * - * @returns Element size - */ -export function getElementSize( - el: HTMLElement -): ElementSize { - return { - width: el.offsetWidth, - height: el.offsetHeight - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch element size - * - * This function returns an observable that subscribes to a single internal - * instance of `ResizeObserver` upon subscription, and emit resize events until - * termination. Note that this function should not be called with the same - * element twice, as the first unsubscription will terminate observation. - * - * Sadly, we can't use the `DOMRect` objects returned by the observer, because - * we need the emitted values to be consistent with `getElementSize`, which will - * return the used values (rounded) and not actual values (unrounded). Thus, we - * use the `offset*` properties. See the linked GitHub issue. - * - * @see https://bit.ly/3m0k3he - GitHub issue - * - * @param el - Element - * - * @returns Element size observable - */ -export function watchElementSize( - el: HTMLElement -): Observable { - return observer$ - .pipe( - tap(observer => observer.observe(el)), - switchMap(observer => entry$ - .pipe( - filter(({ target }) => target === el), - finalize(() => observer.unobserve(el)), - map(() => getElementSize(el)) - ) - ), - startWith(getElementSize(el)) - ) -} diff --git a/src/templates/assets/javascripts/browser/element/size/content/index.ts b/src/templates/assets/javascripts/browser/element/size/content/index.ts deleted file mode 100644 index 5ed388cf..00000000 --- a/src/templates/assets/javascripts/browser/element/size/content/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { ElementSize } from "../_" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve element content size (= scroll width and height) - * - * @param el - Element - * - * @returns Element content size - */ -export function getElementContentSize( - el: HTMLElement -): ElementSize { - return { - width: el.scrollWidth, - height: el.scrollHeight - } -} - -/** - * Retrieve the overflowing container of an element, if any - * - * @param el - Element - * - * @returns Overflowing container or nothing - */ -export function getElementContainer( - el: HTMLElement -): HTMLElement | undefined { - let parent = el.parentElement - while (parent) - if ( - el.scrollWidth <= parent.scrollWidth && - el.scrollHeight <= parent.scrollHeight - ) - parent = (el = parent).parentElement - else - break - - /* Return overflowing container */ - return parent ? el : undefined -} diff --git a/src/templates/assets/javascripts/browser/element/size/index.ts b/src/templates/assets/javascripts/browser/element/size/index.ts deleted file mode 100644 index 602ff2cf..00000000 --- a/src/templates/assets/javascripts/browser/element/size/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/visibility/index.ts b/src/templates/assets/javascripts/browser/element/visibility/index.ts deleted file mode 100644 index 1ffe0b8d..00000000 --- a/src/templates/assets/javascripts/browser/element/visibility/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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() - -/** - * 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 { - 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 { - return watchElementContentOffset(el) - .pipe( - map(({ y }) => { - const visible = getElementSize(el) - const content = getElementContentSize(el) - return y >= ( - content.height - visible.height - threshold - ) - }), - distinctUntilChanged() - ) -} diff --git a/src/templates/assets/javascripts/browser/index.ts b/src/templates/assets/javascripts/browser/index.ts deleted file mode 100644 index f1ee2bae..00000000 --- a/src/templates/assets/javascripts/browser/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./document" -export * from "./element" -export * from "./keyboard" -export * from "./location" -export * from "./media" -export * from "./request" -export * from "./script" -export * from "./toggle" -export * from "./viewport" -export * from "./worker" diff --git a/src/templates/assets/javascripts/browser/keyboard/index.ts b/src/templates/assets/javascripts/browser/keyboard/index.ts deleted file mode 100644 index 783f2cda..00000000 --- a/src/templates/assets/javascripts/browser/keyboard/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - filter, - fromEvent, - map, - merge, - share, - startWith, - switchMap -} from "rxjs" - -import { getActiveElement } from "../element" -import { getToggle } from "../toggle" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Keyboard mode - */ -export type KeyboardMode = - | "global" /* Global */ - | "search" /* Search is open */ - -/* ------------------------------------------------------------------------- */ - -/** - * Keyboard - */ -export interface Keyboard { - mode: KeyboardMode /* Keyboard mode */ - type: string /* Key type */ - claim(): void /* Key claim */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Check whether an element may receive keyboard input - * - * @param el - Element - * @param type - Key type - * - * @returns Test result - */ -function isSusceptibleToKeyboard( - el: HTMLElement, type: string -): boolean { - switch (el.constructor) { - - /* Input elements */ - case HTMLInputElement: - /* @ts-expect-error - omit unnecessary type cast */ - if (el.type === "radio") - return /^Arrow/.test(type) - else - return true - - /* Select element and textarea */ - case HTMLSelectElement: - case HTMLTextAreaElement: - return true - - /* Everything else */ - default: - return el.isContentEditable - } -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch composition events - * - * @returns Composition observable - */ -export function watchComposition(): Observable { - return merge( - fromEvent(window, "compositionstart").pipe(map(() => true)), - fromEvent(window, "compositionend").pipe(map(() => false)) - ) - .pipe( - startWith(false) - ) -} - -/** - * Watch keyboard - * - * @returns Keyboard observable - */ -export function watchKeyboard(): Observable { - const keyboard$ = fromEvent(window, "keydown") - .pipe( - filter(ev => !(ev.metaKey || ev.ctrlKey)), - map(ev => ({ - mode: getToggle("search") ? "search" : "global", - type: ev.key, - claim() { - ev.preventDefault() - ev.stopPropagation() - } - } as Keyboard)), - filter(({ mode, type }) => { - if (mode === "global") { - const active = getActiveElement() - if (typeof active !== "undefined") - return !isSusceptibleToKeyboard(active, type) - } - return true - }), - share() - ) - - /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */ - return watchComposition() - .pipe( - switchMap(active => !active ? keyboard$ : EMPTY) - ) -} diff --git a/src/templates/assets/javascripts/browser/location/_/index.ts b/src/templates/assets/javascripts/browser/location/_/index.ts deleted file mode 100644 index 2672fa74..00000000 --- a/src/templates/assets/javascripts/browser/location/_/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { Subject } from "rxjs" - -import { feature } from "~/_" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve location - * - * This function returns a `URL` object (and not `Location`) to normalize the - * typings across the application. Furthermore, locations need to be tracked - * without setting them and `Location` is a singleton which represents the - * current location. - * - * @returns URL - */ -export function getLocation(): URL { - return new URL(location.href) -} - -/** - * Set location - * - * If instant navigation is enabled, this function creates a temporary anchor - * element, sets the `href` attribute, appends it to the body, clicks it, and - * then removes it again. The event will bubble up the DOM and trigger be - * intercepted by the instant loading business logic. - * - * Note that we must append and remove the anchor element, or the event will - * not bubble up the DOM, making it impossible to intercept it. - * - * @param url - URL to navigate to - * @param navigate - Force navigation - */ -export function setLocation( - url: URL | HTMLLinkElement, navigate = false -): void { - if (feature("navigation.instant") && !navigate) { - const el = h("a", { href: url.href }) - document.body.appendChild(el) - el.click() - el.remove() - - // If we're not using instant navigation, and the page should not be reloaded - // just instruct the browser to navigate to the given URL - } else { - location.href = url.href - } -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch location - * - * @returns Location subject - */ -export function watchLocation(): Subject { - return new Subject() -} diff --git a/src/templates/assets/javascripts/browser/location/hash/index.ts b/src/templates/assets/javascripts/browser/location/hash/index.ts deleted file mode 100644 index 5d3a134a..00000000 --- a/src/templates/assets/javascripts/browser/location/hash/index.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - filter, - fromEvent, - map, - merge, - shareReplay, - startWith -} from "rxjs" - -import { getOptionalElement } from "~/browser" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve location hash - * - * @returns Location hash - */ -export function getLocationHash(): string { - return location.hash.slice(1) -} - -/** - * Set location hash - * - * Setting a new fragment identifier via `location.hash` will have no effect - * if the value doesn't change. When a new fragment identifier is set, we want - * the browser to target the respective element at all times, which is why we - * use this dirty little trick. - * - * @param hash - Location hash - */ -export function setLocationHash(hash: string): void { - const el = h("a", { href: hash }) - el.addEventListener("click", ev => ev.stopPropagation()) - el.click() -} - -/* ------------------------------------------------------------------------- */ - -/** - * Watch location hash - * - * @param location$ - Location observable - * - * @returns Location hash observable - */ -export function watchLocationHash( - location$: Observable -): Observable { - return merge( - fromEvent(window, "hashchange"), - location$ - ) - .pipe( - map(getLocationHash), - startWith(getLocationHash()), - filter(hash => hash.length > 0), - shareReplay(1) - ) -} - -/** - * Watch location target - * - * @param location$ - Location observable - * - * @returns Location target observable - */ -export function watchLocationTarget( - location$: Observable -): Observable { - return watchLocationHash(location$) - .pipe( - map(id => getOptionalElement(`[id="${id}"]`)!), - filter(el => typeof el !== "undefined") - ) -} diff --git a/src/templates/assets/javascripts/browser/location/index.ts b/src/templates/assets/javascripts/browser/location/index.ts deleted file mode 100644 index d77a5444..00000000 --- a/src/templates/assets/javascripts/browser/location/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./hash" diff --git a/src/templates/assets/javascripts/browser/media/index.ts b/src/templates/assets/javascripts/browser/media/index.ts deleted file mode 100644 index dd7400d4..00000000 --- a/src/templates/assets/javascripts/browser/media/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - const media = matchMedia(query) - return fromEventPattern(next => ( - media.addListener(() => next(media.matches)) - )) - .pipe( - startWith(media.matches) - ) -} - -/** - * Watch print mode - * - * @returns Print observable - */ -export function watchPrint(): Observable { - 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( - query$: Observable, factory: () => Observable -): Observable { - return query$ - .pipe( - switchMap(active => active ? factory() : EMPTY) - ) -} diff --git a/src/templates/assets/javascripts/browser/request/index.ts b/src/templates/assets/javascripts/browser/request/index.ts deleted file mode 100644 index 74a56a64..00000000 --- a/src/templates/assets/javascripts/browser/request/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - map, - shareReplay, - switchMap -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Options - */ -interface Options { - progress$?: Subject // Progress subject -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch the given URL - * - * If the request fails (e.g. when dispatched from `file://` locations), the - * observable will complete without emitting a value. - * - * @param url - Request URL - * @param options - Options - * - * @returns Response observable - */ -export function request( - url: URL | string, options?: Options -): Observable { - return new Observable(observer => { - const req = new XMLHttpRequest() - req.open("GET", `${url}`) - req.responseType = "blob" - - // Handle response - req.addEventListener("load", () => { - if (req.status >= 200 && req.status < 300) { - observer.next(req.response) - observer.complete() - } else { - observer.error(new Error(req.statusText)) - } - }) - - // Handle network errors - req.addEventListener("error", () => { - observer.error(new Error("Network Error")) - }) - - // Handle aborted requests - req.addEventListener("abort", () => { - observer.error(new Error("Request aborted")) - }) - - // Handle download progress - if (typeof options?.progress$ !== "undefined") { - req.addEventListener("progress", event => { - options.progress$!.next((event.loaded / event.total) * 100) - }) - - // Immediately set progress to 5% to indicate that we're loading - options.progress$.next(5) - } - - // Send request - req.send() - }) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Fetch JSON from the given URL - * - * @template T - Data type - * - * @param url - Request URL - * @param options - Options - * - * @returns Data observable - */ -export function requestJSON( - url: URL | string, options?: Options -): Observable { - return request(url, options) - .pipe( - switchMap(res => res.text()), - map(body => JSON.parse(body) as T), - shareReplay(1) - ) -} - -/** - * Fetch XML from the given URL - * - * @param url - Request URL - * @param options - Options - * - * @returns Data observable - */ -export function requestXML( - url: URL | string, options?: Options -): Observable { - const dom = new DOMParser() - return request(url, options) - .pipe( - switchMap(res => res.text()), - map(res => dom.parseFromString(res, "text/xml")), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/browser/script/index.ts b/src/templates/assets/javascripts/browser/script/index.ts deleted file mode 100644 index ef5c89e6..00000000 --- a/src/templates/assets/javascripts/browser/script/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - defer, - finalize, - fromEvent, - map, - merge, - switchMap, - take, - throwError -} from "rxjs" - -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create and load a `script` element - * - * This function returns an observable that will emit when the script was - * successfully loaded, or throw an error if it wasn't. - * - * @param src - Script URL - * - * @returns Script observable - */ -export function watchScript(src: string): Observable { - const script = h("script", { src }) - return defer(() => { - document.head.appendChild(script) - return merge( - fromEvent(script, "load"), - fromEvent(script, "error") - .pipe( - switchMap(() => ( - throwError(() => new ReferenceError(`Invalid script: ${src}`)) - )) - ) - ) - .pipe( - map(() => undefined), - finalize(() => document.head.removeChild(script)), - take(1) - ) - }) -} diff --git a/src/templates/assets/javascripts/browser/toggle/index.ts b/src/templates/assets/javascripts/browser/toggle/index.ts deleted file mode 100644 index 0be4b29d..00000000 --- a/src/templates/assets/javascripts/browser/toggle/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 = { - 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 { - const el = toggles[name] - return fromEvent(el, "change") - .pipe( - map(() => el.checked), - startWith(el.checked) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/_/index.ts b/src/templates/assets/javascripts/browser/viewport/_/index.ts deleted file mode 100644 index 09c45f32..00000000 --- a/src/templates/assets/javascripts/browser/viewport/_/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - return combineLatest([ - watchViewportOffset(), - watchViewportSize() - ]) - .pipe( - map(([offset, size]) => ({ offset, size })), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/at/index.ts b/src/templates/assets/javascripts/browser/viewport/at/index.ts deleted file mode 100644 index 8769cf3b..00000000 --- a/src/templates/assets/javascripts/browser/viewport/at/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - header$: Observable
    /* 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 { - const size$ = viewport$ - .pipe( - distinctUntilKeyChanged("size") - ) - - /* Compute element offset */ - const offset$ = combineLatest([size$, header$]) - .pipe( - map(() => getElementOffset(el)) - ) - - /* Compute relative viewport, return hot observable */ - return combineLatest([header$, viewport$, offset$]) - .pipe( - map(([{ height }, { offset, size }, { x, y }]) => ({ - offset: { - x: offset.x - x, - y: offset.y - y + height - }, - size - })) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/index.ts b/src/templates/assets/javascripts/browser/viewport/index.ts deleted file mode 100644 index b3d135e9..00000000 --- a/src/templates/assets/javascripts/browser/viewport/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./at" -export * from "./offset" -export * from "./size" diff --git a/src/templates/assets/javascripts/browser/viewport/offset/index.ts b/src/templates/assets/javascripts/browser/viewport/offset/index.ts deleted file mode 100644 index 63d37dd2..00000000 --- a/src/templates/assets/javascripts/browser/viewport/offset/index.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - return merge( - fromEvent(window, "scroll", { passive: true }), - fromEvent(window, "resize", { passive: true }) - ) - .pipe( - map(getViewportOffset), - startWith(getViewportOffset()) - ) -} diff --git a/src/templates/assets/javascripts/browser/viewport/size/index.ts b/src/templates/assets/javascripts/browser/viewport/size/index.ts deleted file mode 100644 index 06694888..00000000 --- a/src/templates/assets/javascripts/browser/viewport/size/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - return fromEvent(window, "resize", { passive: true }) - .pipe( - map(getViewportSize), - startWith(getViewportSize()) - ) -} diff --git a/src/templates/assets/javascripts/browser/worker/index.ts b/src/templates/assets/javascripts/browser/worker/index.ts deleted file mode 100644 index 12e4e63b..00000000 --- a/src/templates/assets/javascripts/browser/worker/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - endWith, - fromEvent, - ignoreElements, - mergeWith, - share, - takeUntil -} from "rxjs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Worker message - */ -export interface WorkerMessage { - type: unknown /* Message type */ - data?: unknown /* Message data */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Create an observable for receiving from a web worker - * - * @template T - Data type - * - * @param worker - Web worker - * - * @returns Message observable - */ -function recv(worker: Worker): Observable { - return fromEvent, T>(worker, "message", ev => ev.data) -} - -/** - * Create a subject for sending to a web worker - * - * @template T - Data type - * - * @param worker - Web worker - * - * @returns Message subject - */ -function send(worker: Worker): Subject { - const send$ = new Subject() - send$.subscribe(data => worker.postMessage(data)) - - /* Return message subject */ - return send$ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a bidirectional communication channel to a web worker - * - * @template T - Data type - * - * @param url - Worker URL - * @param worker - Worker - * - * @returns Worker subject - */ -export function watchWorker( - url: string, worker = new Worker(url) -): Subject { - const recv$ = recv(worker) - const send$ = send(worker) - - /* Create worker subject and forward messages */ - const worker$ = new Subject() - worker$.subscribe(send$) - - /* Return worker subject */ - const done$ = send$.pipe(ignoreElements(), endWith(true)) - return worker$ - .pipe( - ignoreElements(), - mergeWith(recv$.pipe(takeUntil(done$))), - share() - ) as Subject -} diff --git a/src/templates/assets/javascripts/bundle.ts b/src/templates/assets/javascripts/bundle.ts deleted file mode 100644 index 141789c9..00000000 --- a/src/templates/assets/javascripts/bundle.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import "focus-visible" - -import { - EMPTY, - NEVER, - Observable, - Subject, - defer, - delay, - filter, - map, - merge, - mergeWith, - shareReplay, - switchMap -} from "rxjs" - -import { configuration, feature } from "./_" -import { - at, - getActiveElement, - getOptionalElement, - requestJSON, - setLocation, - setToggle, - watchDocument, - watchKeyboard, - watchLocation, - watchLocationTarget, - watchMedia, - watchPrint, - watchScript, - watchViewport -} from "./browser" -import { - getComponentElement, - getComponentElements, - mountAnnounce, - mountBackToTop, - mountConsent, - mountContent, - mountDialog, - mountHeader, - mountHeaderTitle, - mountPalette, - mountProgress, - mountSearch, - mountSearchHiglight, - mountSidebar, - mountSource, - mountTableOfContents, - mountTabs, - watchHeader, - watchMain -} from "./components" -import { - SearchIndex, - setupClipboardJS, - setupInstantNavigation, - setupVersionSelector -} from "./integrations" -import { - patchIndeterminate, - patchScrollfix, - patchScrolllock -} from "./patches" -import "./polyfills" - -/* ---------------------------------------------------------------------------- - * Functions - @todo refactor - * ------------------------------------------------------------------------- */ - -/** - * Fetch search index - * - * @returns Search index observable - */ -function fetchSearchIndex(): Observable { - if (location.protocol === "file:") { - return watchScript( - `${new URL("search/search_index.js", config.base)}` - ) - .pipe( - // @ts-ignore - @todo fix typings - map(() => __index), - shareReplay(1) - ) - } else { - return requestJSON( - new URL("search/search_index.json", config.base) - ) - } -} - -/* ---------------------------------------------------------------------------- - * Application - * ------------------------------------------------------------------------- */ - -/* Yay, JavaScript is available */ -document.documentElement.classList.remove("no-js") -document.documentElement.classList.add("js") - -/* Set up navigation observables and subjects */ -const document$ = watchDocument() -const location$ = watchLocation() -const target$ = watchLocationTarget(location$) -const keyboard$ = watchKeyboard() - -/* Set up media observables */ -const viewport$ = watchViewport() -const tablet$ = watchMedia("(min-width: 960px)") -const screen$ = watchMedia("(min-width: 1220px)") -const print$ = watchPrint() - -/* Retrieve search index, if search is enabled */ -const config = configuration() -const index$ = document.forms.namedItem("search") - ? fetchSearchIndex() - : NEVER - -/* Set up Clipboard.js integration */ -const alert$ = new Subject() -setupClipboardJS({ alert$ }) - -/* Set up progress indicator */ -const progress$ = new Subject() - -/* Set up instant navigation, if enabled */ -if (feature("navigation.instant")) - setupInstantNavigation({ location$, viewport$, progress$ }) - .subscribe(document$) - -/* Set up version selector */ -if (config.version?.provider === "mike") - setupVersionSelector({ document$ }) - -/* Always close drawer and search on navigation */ -merge(location$, target$) - .pipe( - delay(125) - ) - .subscribe(() => { - setToggle("drawer", false) - setToggle("search", false) - }) - -/* Set up global keyboard handlers */ -keyboard$ - .pipe( - filter(({ mode }) => mode === "global") - ) - .subscribe(key => { - switch (key.type) { - - /* Go to previous page */ - case "p": - case ",": - const prev = getOptionalElement("link[rel=prev]") - if (typeof prev !== "undefined") - setLocation(prev) - break - - /* Go to next page */ - case "n": - case ".": - const next = getOptionalElement("link[rel=next]") - if (typeof next !== "undefined") - setLocation(next) - break - - /* Expand navigation, see https://bit.ly/3ZjG5io */ - case "Enter": - const active = getActiveElement() - if (active instanceof HTMLLabelElement) - active.click() - } - }) - -/* Set up patches */ -patchIndeterminate({ document$, tablet$ }) -patchScrollfix({ document$ }) -patchScrolllock({ viewport$, tablet$ }) - -/* Set up header and main area observable */ -const header$ = watchHeader(getComponentElement("header"), { viewport$ }) -const main$ = document$ - .pipe( - map(() => getComponentElement("main")), - switchMap(el => watchMain(el, { viewport$, header$ })), - shareReplay(1) - ) - -/* Set up control component observables */ -const control$ = merge( - - /* Consent */ - ...getComponentElements("consent") - .map(el => mountConsent(el, { target$ })), - - /* Dialog */ - ...getComponentElements("dialog") - .map(el => mountDialog(el, { alert$ })), - - /* Header */ - ...getComponentElements("header") - .map(el => mountHeader(el, { viewport$, header$, main$ })), - - /* Color palette */ - ...getComponentElements("palette") - .map(el => mountPalette(el)), - - /* Progress bar */ - ...getComponentElements("progress") - .map(el => mountProgress(el, { progress$ })), - - /* Search */ - ...getComponentElements("search") - .map(el => mountSearch(el, { index$, keyboard$ })), - - /* Repository information */ - ...getComponentElements("source") - .map(el => mountSource(el)) -) - -/* Set up content component observables */ -const content$ = defer(() => merge( - - /* Announcement bar */ - ...getComponentElements("announce") - .map(el => mountAnnounce(el)), - - /* Content */ - ...getComponentElements("content") - .map(el => mountContent(el, { viewport$, target$, print$ })), - - /* Search highlighting */ - ...getComponentElements("content") - .map(el => feature("search.highlight") - ? mountSearchHiglight(el, { index$, location$ }) - : EMPTY - ), - - /* Header title */ - ...getComponentElements("header-title") - .map(el => mountHeaderTitle(el, { viewport$, header$ })), - - /* Sidebar */ - ...getComponentElements("sidebar") - .map(el => el.getAttribute("data-md-type") === "navigation" - ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ })) - : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ })) - ), - - /* Navigation tabs */ - ...getComponentElements("tabs") - .map(el => mountTabs(el, { viewport$, header$ })), - - /* Table of contents */ - ...getComponentElements("toc") - .map(el => mountTableOfContents(el, { - viewport$, header$, main$, target$ - })), - - /* Back-to-top button */ - ...getComponentElements("top") - .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ })) -)) - -/* Set up component observables */ -const component$ = document$ - .pipe( - switchMap(() => content$), - mergeWith(control$), - shareReplay(1) - ) - -/* Subscribe to all components */ -component$.subscribe() - -/* ---------------------------------------------------------------------------- - * Exports - * ------------------------------------------------------------------------- */ - -window.document$ = document$ /* Document observable */ -window.location$ = location$ /* Location subject */ -window.target$ = target$ /* Location target observable */ -window.keyboard$ = keyboard$ /* Keyboard observable */ -window.viewport$ = viewport$ /* Viewport observable */ -window.tablet$ = tablet$ /* Media tablet observable */ -window.screen$ = screen$ /* Media screen observable */ -window.print$ = print$ /* Media print observable */ -window.alert$ = alert$ /* Alert subject */ -window.progress$ = progress$ /* Progress indicator subject */ -window.component$ = component$ /* Component observable */ diff --git a/src/templates/assets/javascripts/components/_/index.ts b/src/templates/assets/javascripts/components/_/index.ts deleted file mode 100644 index 61c471d9..00000000 --- a/src/templates/assets/javascripts/components/_/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { getElement, getElements } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Component type - */ -export type ComponentType = - | "announce" /* Announcement bar */ - | "container" /* Container */ - | "consent" /* Consent */ - | "content" /* Content */ - | "dialog" /* Dialog */ - | "header" /* Header */ - | "header-title" /* Header title */ - | "header-topic" /* Header topic */ - | "main" /* Main area */ - | "outdated" /* Version warning */ - | "palette" /* Color palette */ - | "progress" /* Progress indicator */ - | "search" /* Search */ - | "search-query" /* Search input */ - | "search-result" /* Search results */ - | "search-share" /* Search sharing */ - | "search-suggest" /* Search suggestions */ - | "sidebar" /* Sidebar */ - | "skip" /* Skip link */ - | "source" /* Repository information */ - | "tabs" /* Navigation tabs */ - | "toc" /* Table of contents */ - | "top" /* Back-to-top button */ - -/** - * Component - * - * @template T - Component type - * @template U - Reference type - */ -export type Component< - T extends {} = {}, - U extends HTMLElement = HTMLElement -> = - T & { - ref: U /* Component reference */ - } - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Component type map - */ -interface ComponentTypeMap { - "announce": HTMLElement /* Announcement bar */ - "container": HTMLElement /* Container */ - "consent": HTMLElement /* Consent */ - "content": HTMLElement /* Content */ - "dialog": HTMLElement /* Dialog */ - "header": HTMLElement /* Header */ - "header-title": HTMLElement /* Header title */ - "header-topic": HTMLElement /* Header topic */ - "main": HTMLElement /* Main area */ - "outdated": HTMLElement /* Version warning */ - "palette": HTMLElement /* Color palette */ - "progress": HTMLElement /* Progress indicator */ - "search": HTMLElement /* Search */ - "search-query": HTMLInputElement /* Search input */ - "search-result": HTMLElement /* Search results */ - "search-share": HTMLAnchorElement /* Search sharing */ - "search-suggest": HTMLElement /* Search suggestions */ - "sidebar": HTMLElement /* Sidebar */ - "skip": HTMLAnchorElement /* Skip link */ - "source": HTMLAnchorElement /* Repository information */ - "tabs": HTMLElement /* Navigation tabs */ - "toc": HTMLElement /* Table of contents */ - "top": HTMLAnchorElement /* Back-to-top button */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Retrieve the element for a given component or throw a reference error - * - * @template T - Component type - * - * @param type - Component type - * @param node - Node of reference - * - * @returns Element - */ -export function getComponentElement( - 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( - type: T, node: ParentNode = document -): ComponentTypeMap[T][] { - return getElements(`[data-md-component=${type}]`, node) -} diff --git a/src/templates/assets/javascripts/components/announce/index.ts b/src/templates/assets/javascripts/components/announce/index.ts deleted file mode 100644 index dd04b4ff..00000000 --- a/src/templates/assets/javascripts/components/announce/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - defer, - finalize, - fromEvent, - map, - tap -} from "rxjs" - -import { feature } from "~/_" -import { getElement } from "~/browser" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Announcement bar - */ -export interface Announce { - hash: number /* Content hash */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch announcement bar - * - * @param el - Announcement bar element - * - * @returns Announcement bar observable - */ -export function watchAnnounce( - el: HTMLElement -): Observable { - 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> { - if (!feature("announce.dismiss") || !el.childElementCount) - return EMPTY - - /* Support instant navigation - see https://t.ly/3FTme */ - if (!el.hidden) { - const content = getElement(".md-typeset", el) - if (__md_hash(content.innerHTML) === __md_get("__announce")) - el.hidden = true - } - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - push$.subscribe(({ hash }) => { - el.hidden = true - - /* Persist preference in local storage */ - __md_set("__announce", hash) - }) - - /* Create and return component */ - return watchAnnounce(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/consent/index.ts b/src/templates/assets/javascripts/components/consent/index.ts deleted file mode 100644 index bc99db58..00000000 --- a/src/templates/assets/javascripts/components/consent/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Consent - */ -export interface Consent { - hidden: boolean /* Consent is hidden */ -} - -/** - * Consent defaults - */ -export interface ConsentDefaults { - analytics?: boolean /* Consent for Analytics */ - github?: boolean /* Consent for GitHub */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - target$: Observable /* Target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch consent - * - * @param el - Consent element - * @param options - Options - * - * @returns Consent observable - */ -export function watchConsent( - el: HTMLElement, { target$ }: WatchOptions -): Observable { - 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> { - const internal$ = new Subject() - internal$.subscribe(({ hidden }) => { - el.hidden = hidden - }) - - /* Create and return component */ - return watchConsent(el, options) - .pipe( - tap(state => internal$.next(state)), - finalize(() => internal$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/_/index.ts b/src/templates/assets/javascripts/components/content/_/index.ts deleted file mode 100644 index 899a695c..00000000 --- a/src/templates/assets/javascripts/components/content/_/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { Observable, merge } from "rxjs" - -import { Viewport, getElements } from "~/browser" - -import { Component } from "../../_" -import { - Annotation, - mountAnnotationBlock -} from "../annotation" -import { - CodeBlock, - mountCodeBlock -} from "../code" -import { - Details, - mountDetails -} from "../details" -import { - Mermaid, - mountMermaid -} from "../mermaid" -import { - DataTable, - mountDataTable -} from "../table" -import { - ContentTabs, - mountContentTabs -} from "../tabs" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content - */ -export type Content = - | Annotation - | CodeBlock - | ContentTabs - | DataTable - | Details - | Mermaid - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - target$: Observable /* Location target observable */ - print$: Observable /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount content - * - * This function mounts all components that are found in the content of the - * actual article, including code blocks, data tables and details. - * - * @param el - Content element - * @param options - Options - * - * @returns Content component observable - */ -export function mountContent( - el: HTMLElement, { viewport$, target$, print$ }: MountOptions -): Observable> { - return merge( - - /* Annotations */ - ...getElements(".annotate:not(.highlight)", el) - .map(child => mountAnnotationBlock(child, { target$, print$ })), - - /* Code blocks */ - ...getElements("pre:not(.mermaid) > code", el) - .map(child => mountCodeBlock(child, { target$, print$ })), - - /* Mermaid diagrams */ - ...getElements("pre.mermaid", el) - .map(child => mountMermaid(child)), - - /* Data tables */ - ...getElements("table:not([class])", el) - .map(child => mountDataTable(child)), - - /* Details */ - ...getElements("details", el) - .map(child => mountDetails(child, { target$, print$ })), - - /* Content tabs */ - ...getElements("[data-tabs]", el) - .map(child => mountContentTabs(child, { viewport$ })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/_/index.ts b/src/templates/assets/javascripts/components/content/annotation/_/index.ts deleted file mode 100644 index c5138fa4..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/_/index.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - auditTime, - combineLatest, - debounceTime, - defer, - delay, - endWith, - filter, - finalize, - fromEvent, - ignoreElements, - map, - merge, - switchMap, - take, - takeUntil, - tap, - throttleTime, - withLatestFrom -} from "rxjs" - -import { - ElementOffset, - getActiveElement, - getElementSize, - watchElementContentOffset, - watchElementFocus, - watchElementOffset, - watchElementVisibility -} from "~/browser" - -import { Component } from "../../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Annotation - */ -export interface Annotation { - active: boolean /* Annotation is active */ - offset: ElementOffset /* Annotation offset */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Location target observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch annotation - * - * @param el - Annotation element - * @param container - Containing element - * - * @returns Annotation observable - */ -export function watchAnnotation( - el: HTMLElement, container: HTMLElement -): Observable { - const offset$ = defer(() => combineLatest([ - watchElementOffset(el), - watchElementContentOffset(container) - ])) - .pipe( - map(([{ x, y }, scroll]): ElementOffset => { - const { width, height } = getElementSize(el) - return ({ - x: x - scroll.x + width / 2, - y: y - scroll.y + height / 2 - }) - }) - ) - - /* Actively watch annotation on focus */ - return watchElementFocus(el) - .pipe( - switchMap(active => offset$ - .pipe( - map(offset => ({ active, offset })), - take(+!active || Infinity) - ) - ) - ) -} - -/** - * Mount annotation - * - * @param el - Annotation element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotation( - el: HTMLElement, container: HTMLElement, { target$ }: MountOptions -): Observable> { - const [tooltip, index] = Array.from(el.children) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ offset }) { - el.style.setProperty("--md-tooltip-x", `${offset.x}px`) - el.style.setProperty("--md-tooltip-y", `${offset.y}px`) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-x") - el.style.removeProperty("--md-tooltip-y") - } - }) - - /* Start animation only when annotation is visible */ - watchElementVisibility(el) - .pipe( - takeUntil(done$) - ) - .subscribe(visible => { - el.toggleAttribute("data-md-visible", visible) - }) - - /* Toggle tooltip presence to mitigate empty lines when copying */ - merge( - push$.pipe(filter(({ active }) => active)), - push$.pipe(debounceTime(250), filter(({ active }) => !active)) - ) - .subscribe({ - - /* Handle emission */ - next({ active }) { - if (active) - el.prepend(tooltip) - else - tooltip.remove() - }, - - /* Handle complete */ - complete() { - el.prepend(tooltip) - } - }) - - /* Toggle tooltip visibility */ - push$ - .pipe( - auditTime(16, animationFrameScheduler) - ) - .subscribe(({ active }) => { - tooltip.classList.toggle("md-tooltip--active", active) - }) - - /* Track relative origin of tooltip */ - push$ - .pipe( - throttleTime(125, animationFrameScheduler), - filter(() => !!el.offsetParent), - map(() => el.offsetParent!.getBoundingClientRect()), - map(({ x }) => x) - ) - .subscribe({ - - /* Handle emission */ - next(origin) { - if (origin) - el.style.setProperty("--md-tooltip-0", `${-origin}px`) - else - el.style.removeProperty("--md-tooltip-0") - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-tooltip-0") - } - }) - - /* Allow to copy link without scrolling to anchor */ - fromEvent(index, "click") - .pipe( - takeUntil(done$), - filter(ev => !(ev.metaKey || ev.ctrlKey)) - ) - .subscribe(ev => { - ev.stopPropagation() - ev.preventDefault() - }) - - /* Allow to open link in new tab or blur on close */ - fromEvent(index, "mousedown") - .pipe( - takeUntil(done$), - withLatestFrom(push$) - ) - .subscribe(([ev, { active }]) => { - - /* Open in new tab */ - if (ev.button !== 0 || ev.metaKey || ev.ctrlKey) { - ev.preventDefault() - - /* Close annotation */ - } else if (active) { - ev.preventDefault() - - /* Focus parent annotation, if any */ - const parent = el.parentElement!.closest(".md-annotation") - if (parent instanceof HTMLElement) - parent.focus() - else - getActiveElement()?.blur() - } - }) - - /* Open and focus annotation on location target */ - target$ - .pipe( - takeUntil(done$), - filter(target => target === tooltip), - delay(125) - ) - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchAnnotation(el, container) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/block/index.ts b/src/templates/assets/javascripts/components/content/annotation/block/index.ts deleted file mode 100644 index c73b01fa..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/block/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { EMPTY, Observable, defer } from "rxjs" - -import { Component } from "../../../_" -import { Annotation } from "../_" -import { mountAnnotationList } from "../list" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Location target observable */ - print$: Observable /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find list element directly following a block - * - * @param el - Annotation block element - * - * @returns List element or nothing - */ -function findList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation block - * - * @param el - Annotation block element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationBlock( - el: HTMLElement, options: MountOptions -): Observable> { - return defer(() => { - const list = findList(el) - return typeof list !== "undefined" - ? mountAnnotationList(list, el, options) - : EMPTY - }) -} diff --git a/src/templates/assets/javascripts/components/content/annotation/index.ts b/src/templates/assets/javascripts/components/content/annotation/index.ts deleted file mode 100644 index c593b723..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./block" -export * from "./list" diff --git a/src/templates/assets/javascripts/components/content/annotation/list/index.ts b/src/templates/assets/javascripts/components/content/annotation/list/index.ts deleted file mode 100644 index 725dd583..00000000 --- a/src/templates/assets/javascripts/components/content/annotation/list/index.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - defer, - endWith, - finalize, - ignoreElements, - merge, - share, - takeUntil -} from "rxjs" - -import { - getElement, - getElements, - getOptionalElement -} from "~/browser" -import { renderAnnotation } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotation -} from "../_" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Location target observable */ - print$: Observable /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find all annotation hosts in the containing element - * - * @param container - Containing element - * - * @returns Annotation hosts - */ -function findHosts(container: HTMLElement): HTMLElement[] { - return container.tagName === "CODE" - ? getElements(".c, .c1, .cm", container) - : [container] -} - -/** - * Find all annotation markers in the containing element - * - * @param container - Containing element - * - * @returns Annotation markers - */ -function findMarkers(container: HTMLElement): Text[] { - const markers: Text[] = [] - for (const el of findHosts(container)) { - const nodes: Text[] = [] - - /* Find all text nodes in current element */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) - nodes.push(node as Text) - - /* Find all markers in each text node */ - for (let text of nodes) { - let match: RegExpExecArray | null - - /* Split text at marker and add to list */ - while ((match = /(\(\d+\))(!)?/.exec(text.textContent!))) { - const [, id, force] = match - if (typeof force === "undefined") { - const marker = text.splitText(match.index) - text = marker.splitText(id.length) - markers.push(marker) - - /* Replace entire text with marker */ - } else { - text.textContent = id - markers.push(text) - break - } - } - } - } - return markers -} - -/** - * Swap the child nodes of two elements - * - * @param source - Source element - * @param target - Target element - */ -function swap(source: HTMLElement, target: HTMLElement): void { - target.append(...Array.from(source.childNodes)) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount annotation list - * - * This function analyzes the containing code block and checks for markers - * referring to elements in the given annotation list. If no markers are found, - * the list is left untouched. Otherwise, list elements are rendered as - * annotations inside the code block. - * - * @param el - Annotation list element - * @param container - Containing element - * @param options - Options - * - * @returns Annotation component observable - */ -export function mountAnnotationList( - el: HTMLElement, container: HTMLElement, { target$, print$ }: MountOptions -): Observable> { - - /* Compute prefix for tooltip anchors */ - const parent = container.closest("[id]") - const prefix = parent?.id - - /* Find and replace all markers with empty annotations */ - const annotations = new Map() - for (const marker of findMarkers(container)) { - const [, id] = marker.textContent!.match(/\((\d+)\)/)! - if (getOptionalElement(`:scope > li:nth-child(${id})`, el)) { - annotations.set(id, renderAnnotation(id, prefix)) - marker.replaceWith(annotations.get(id)!) - } - } - - /* Keep list if there are no annotations to render */ - if (annotations.size === 0) - return EMPTY - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Retrieve container pairs for swapping */ - const pairs: [HTMLElement, HTMLElement][] = [] - for (const [id, annotation] of annotations) - pairs.push([ - getElement(".md-typeset", annotation), - getElement(`:scope > li:nth-child(${id})`, el) - ]) - - /* Handle print mode - see https://bit.ly/3rgPdpt */ - print$.pipe(takeUntil(done$)) - .subscribe(active => { - el.hidden = !active - - /* Add class to discern list element */ - el.classList.toggle("md-annotation-list", active) - - /* Show annotations in code block or list (print) */ - for (const [inner, child] of pairs) - if (!active) - swap(child, inner) - else - swap(inner, child) - }) - - /* Create and return component */ - return merge(...[...annotations] - .map(([, annotation]) => ( - mountAnnotation(annotation, container, { target$ }) - )) - ) - .pipe( - finalize(() => push$.complete()), - share() - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/code/_/index.ts b/src/templates/assets/javascripts/components/content/code/_/index.ts deleted file mode 100644 index ccc09339..00000000 --- a/src/templates/assets/javascripts/components/content/code/_/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import ClipboardJS from "clipboard" -import { - EMPTY, - Observable, - Subject, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - filter, - finalize, - map, - mergeWith, - switchMap, - take, - tap -} from "rxjs" - -import { feature } from "~/_" -import { - getElementContentSize, - watchElementSize, - watchElementVisibility -} from "~/browser" -import { renderClipboardButton } from "~/templates" - -import { Component } from "../../../_" -import { - Annotation, - mountAnnotationList -} from "../../annotation" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Code block - */ -export interface CodeBlock { - scrollable: boolean /* Code block overflows */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Location target observable */ - print$: Observable /* Media print observable */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Global sequence number for code blocks - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Find candidate list element directly following a code block - * - * @param el - Code block element - * - * @returns List element or nothing - */ -function findCandidateList(el: HTMLElement): HTMLElement | undefined { - if (el.nextElementSibling) { - const sibling = el.nextElementSibling as HTMLElement - if (sibling.tagName === "OL") - return sibling - - /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ - else if (sibling.tagName === "P" && !sibling.children.length) - return findCandidateList(sibling) - } - - /* Everything else */ - return undefined -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch code block - * - * This function monitors size changes of the viewport, as well as switches of - * content tabs with embedded code blocks, as both may trigger overflow. - * - * @param el - Code block element - * - * @returns Code block observable - */ -export function watchCodeBlock( - el: HTMLElement -): Observable { - return watchElementSize(el) - .pipe( - map(({ width }) => { - const content = getElementContentSize(el) - return { - scrollable: content.width > width - } - }), - distinctUntilKeyChanged("scrollable") - ) -} - -/** - * Mount code block - * - * This function ensures that an overflowing code block is focusable through - * keyboard, so it can be scrolled without a mouse to improve on accessibility. - * Furthermore, if code annotations are enabled, they are mounted if and only - * if the code block is currently visible, e.g., not in a hidden content tab. - * - * Note that code blocks may be mounted eagerly or lazily. If they're mounted - * lazily (on first visibility), code annotation anchor links will not work, - * as they are evaluated on initial page load, and code annotations in general - * might feel a little bumpier. - * - * @param el - Code block element - * @param options - Options - * - * @returns Code block and annotation component observable - */ -export function mountCodeBlock( - el: HTMLElement, options: MountOptions -): Observable> { - const { matches: hover } = matchMedia("(hover)") - - /* Defer mounting of code block - see https://bit.ly/3vHVoVD */ - const factory$ = defer(() => { - const push$ = new Subject() - push$.subscribe(({ scrollable }) => { - if (scrollable && hover) - el.setAttribute("tabindex", "0") - else - el.removeAttribute("tabindex") - }) - - /* Render button for Clipboard.js integration */ - if (ClipboardJS.isSupported()) { - if (el.closest(".copy") || ( - feature("content.code.copy") && !el.closest(".no-copy") - )) { - const parent = el.closest("pre")! - parent.id = `__code_${sequence++}` - parent.insertBefore( - renderClipboardButton(parent.id), - el - ) - } - } - - /* Handle code annotations */ - const container = el.closest(".highlight") - if (container instanceof HTMLElement) { - const list = findCandidateList(container) - - /* Mount code annotations, if enabled */ - if (typeof list !== "undefined" && ( - container.classList.contains("annotate") || - feature("content.code.annotate") - )) { - const annotations$ = mountAnnotationList(list, el, options) - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - mergeWith( - watchElementSize(container) - .pipe( - map(({ width, height }) => width && height), - distinctUntilChanged(), - switchMap(active => active ? annotations$ : EMPTY) - ) - ) - ) - } - } - - /* Create and return component */ - return watchCodeBlock(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - - /* Mount code block lazily */ - if (feature("content.lazy")) - return watchElementVisibility(el) - .pipe( - filter(visible => visible), - take(1), - switchMap(() => factory$) - ) - - /* Mount code block */ - return factory$ -} diff --git a/src/templates/assets/javascripts/components/content/code/index.ts b/src/templates/assets/javascripts/components/content/code/index.ts deleted file mode 100644 index 3f86e2b4..00000000 --- a/src/templates/assets/javascripts/components/content/code/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" diff --git a/src/templates/assets/javascripts/components/content/details/index.ts b/src/templates/assets/javascripts/components/content/details/index.ts deleted file mode 100644 index 17bfae45..00000000 --- a/src/templates/assets/javascripts/components/content/details/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* Location target observable */ - print$: Observable /* Media print observable */ -} - -/** - * Mount options - */ -interface MountOptions { - target$: Observable /* Location target observable */ - print$: Observable /* 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
    { - 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> { - return defer(() => { - const push$ = new Subject
    () - push$.subscribe(({ action, reveal }) => { - el.toggleAttribute("open", action === "open") - if (reveal) - el.scrollIntoView() - }) - - /* Create and return component */ - return watchDetails(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/content/index.ts b/src/templates/assets/javascripts/components/content/index.ts deleted file mode 100644 index a29d8b41..00000000 --- a/src/templates/assets/javascripts/components/content/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./annotation" -export * from "./code" -export * from "./details" -export * from "./table" -export * from "./tabs" diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.css b/src/templates/assets/javascripts/components/content/mermaid/index.css deleted file mode 100644 index 3092b8ec..00000000 --- a/src/templates/assets/javascripts/components/content/mermaid/index.css +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Rules: general - * ------------------------------------------------------------------------- */ - -/* General node */ -.node circle, -.node ellipse, -.node path, -.node polygon, -.node rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* General marker */ -marker { - fill: var(--md-mermaid-edge-color) !important; -} - -/* General edge label */ -.edgeLabel .label rect { - fill: transparent; -} - -/* ---------------------------------------------------------------------------- - * Rules: flowcharts - * ------------------------------------------------------------------------- */ - -/* Flowchart node label */ -.label { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart node label container */ -.label foreignObject { - overflow: visible; - line-height: initial; -} - -/* Flowchart edge label in node label */ -.label div .edgeLabel { - color: var(--md-mermaid-label-fg-color); - background-color: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge label */ -.edgeLabel, -.edgeLabel rect { - color: var(--md-mermaid-edge-color); - background-color: var(--md-mermaid-label-bg-color); - fill: var(--md-mermaid-label-bg-color); -} - -/* Flowchart edge path */ -.edgePath .path, -.flowchart-link { - stroke: var(--md-mermaid-edge-color); - stroke-width: .05rem; -} - -/* Flowchart arrow head */ -.edgePath .arrowheadPath { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* Flowchart subgraph */ -.cluster rect { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* Flowchart subgraph labels */ -.cluster span { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* Flowchart markers */ -g #flowchart-circleStart, -g #flowchart-circleEnd, -g #flowchart-crossStart, -g #flowchart-crossEnd, -g #flowchart-pointStart, -g #flowchart-pointEnd { - stroke: none; -} - -/* ---------------------------------------------------------------------------- - * Rules: class diagrams - * ------------------------------------------------------------------------- */ - -/* Class group node */ -g.classGroup line, -g.classGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class group node text */ -g.classGroup text { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class label box */ -.classLabel .box { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); -} - -/* Class label text */ -.classLabel .label { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class group divider */ -.node .divider { - stroke: var(--md-mermaid-node-fg-color); -} - -/* Class relation */ -.relation { - stroke: var(--md-mermaid-edge-color); -} - -/* Class relation cardinality */ -.cardinality { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Class relation cardinality text */ -.cardinality text { - fill: inherit !important; -} - -/* Class extension, composition and dependency marker */ -defs #classDiagram-extensionStart, -defs #classDiagram-extensionEnd, -defs #classDiagram-compositionStart, -defs #classDiagram-compositionEnd, -defs #classDiagram-dependencyStart, -defs #classDiagram-dependencyEnd { - fill: var(--md-mermaid-edge-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Class aggregation marker */ -defs #classDiagram-aggregationStart, -defs #classDiagram-aggregationEnd { - fill: var(--md-mermaid-label-bg-color) !important; - stroke: var(--md-mermaid-edge-color) !important; -} - -/* ---------------------------------------------------------------------------- - * Rules: state diagrams - * ------------------------------------------------------------------------- */ - -/* State group node */ -g.stateGroup rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State group title */ -g.stateGroup .state-title { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color) !important; -} - -/* State group background */ -g.stateGroup .composit { - fill: var(--md-mermaid-label-bg-color); -} - -/* State node label */ -.nodeLabel { - color: var(--md-mermaid-label-fg-color); - font-family: var(--md-mermaid-font-family); -} - -/* State start and end marker */ -.start-state, -.node circle.state-start, -.node circle.state-end { - fill: var(--md-mermaid-edge-color); - stroke: none; -} - -/* State end marker */ -.end-state-outer, -.end-state-inner { - fill: var(--md-mermaid-edge-color); -} - -/* State end marker */ -.end-state-inner, -.node circle.state-end { - stroke: var(--md-mermaid-label-bg-color); -} - -/* State transition */ -.transition { - stroke: var(--md-mermaid-edge-color); -} - -/* State fork and join */ -[id^=state-fork] rect, -[id^=state-join] rect { - fill: var(--md-mermaid-edge-color) !important; - stroke: none !important; -} - -/* State cluster (yes, 2x... Mermaid WTF) */ -.statediagram-cluster.statediagram-cluster .inner { - fill: var(--md-default-bg-color); -} - -/* State cluster node */ -.statediagram-cluster rect { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* State cluster divider */ -.statediagram-state rect.divider { - fill: var(--md-default-fg-color--lightest); - stroke: var(--md-default-fg-color--lighter); -} - -/* State diagram markers */ -defs #statediagram-barbEnd { - stroke: var(--md-mermaid-edge-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: entity-relationship diagrams - * ------------------------------------------------------------------------- */ - -/* Attribute box */ -.attributeBoxEven, -.attributeBoxOdd { - fill: var(--md-mermaid-node-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node */ -.entityBox { - fill: var(--md-mermaid-label-bg-color); - stroke: var(--md-mermaid-node-fg-color); -} - -/* Entity node label */ -.entityLabel { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship label container */ -.relationshipLabelBox { - background-color: var(--md-mermaid-label-bg-color); - opacity: 1; - fill: var(--md-mermaid-label-bg-color); - fill-opacity: 1; -} - -/* Entity relationship label */ -.relationshipLabel { - fill: var(--md-mermaid-label-fg-color); -} - -/* Entity relationship line { */ -.relationshipLine { - stroke: var(--md-mermaid-edge-color); -} - -/* Entity relationship line markers */ -defs #ZERO_OR_ONE_START *, -defs #ZERO_OR_ONE_END *, -defs #ZERO_OR_MORE_START *, -defs #ZERO_OR_MORE_END *, -defs #ONLY_ONE_START *, -defs #ONLY_ONE_END *, -defs #ONE_OR_MORE_START *, -defs #ONE_OR_MORE_END * { - stroke: var(--md-mermaid-edge-color) !important; -} - -/* Entity relationship line markers */ -defs #ZERO_OR_MORE_START circle, -defs #ZERO_OR_MORE_END circle { - fill: var(--md-mermaid-label-bg-color); -} - -/* ---------------------------------------------------------------------------- - * Rules: sequence diagrams - * ------------------------------------------------------------------------- */ - -/* Sequence actor */ -.actor { - fill: var(--md-mermaid-sequence-actor-bg-color); - stroke: var(--md-mermaid-sequence-actor-border-color); -} - -/* Sequence actor text */ -text.actor > tspan { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-actor-fg-color); -} - -/* Sequence actor line */ -line { - stroke: var(--md-mermaid-sequence-actor-line-color); -} - -/* Sequence actor */ -.actor-man circle, -.actor-man line { - fill: var(--md-mermaid-sequence-actorman-bg-color); - stroke: var(--md-mermaid-sequence-actorman-line-color); -} - -/* Sequence message line */ -.messageLine0, -.messageLine1 { - stroke: var(--md-mermaid-sequence-message-line-color); -} - -/* Sequence note */ -.note { - fill: var(--md-mermaid-sequence-note-bg-color); - stroke: var(--md-mermaid-sequence-note-border-color); -} - -/* Sequence message, loop and note text */ -.messageText, -.loopText, -.loopText > tspan, -.noteText > tspan { - font-family: var(--md-mermaid-font-family) !important; - stroke: none; -} - -/* Sequence message text */ -.messageText { - fill: var(--md-mermaid-sequence-message-fg-color); -} - -/* Sequence loop text */ -.loopText, -.loopText > tspan { - fill: var(--md-mermaid-sequence-loop-fg-color); -} - -/* Sequence note text */ -.noteText > tspan { - fill: var(--md-mermaid-sequence-note-fg-color); -} - -/* Sequence arrow head */ -#arrowhead path { - fill: var(--md-mermaid-sequence-message-line-color); - stroke: none; -} - -/* Sequence loop line */ -.loopLine { - fill: var(--md-mermaid-sequence-loop-bg-color); - stroke: var(--md-mermaid-sequence-loop-border-color); -} - -/* Sequence label box */ -.labelBox { - fill: var(--md-mermaid-sequence-label-bg-color); - stroke: none; -} - -/* Sequence label text */ -.labelText, -.labelText > span { - font-family: var(--md-mermaid-font-family); - fill: var(--md-mermaid-sequence-label-fg-color); -} - -/* Sequence number */ -.sequenceNumber { - fill: var(--md-mermaid-sequence-number-fg-color); -} - -/* Sequence rectangle */ -rect.rect { - fill: var(--md-mermaid-sequence-box-bg-color); - stroke: none; -} - -/* Sequence rectangle text */ -rect.rect + text.text { - fill: var(--md-mermaid-sequence-box-fg-color); -} - -/* Sequence diagram markers */ -defs #sequencenumber { - fill: var(--md-mermaid-sequence-number-bg-color) !important; -} diff --git a/src/templates/assets/javascripts/components/content/mermaid/index.ts b/src/templates/assets/javascripts/components/content/mermaid/index.ts deleted file mode 100644 index 3f6480fd..00000000 --- a/src/templates/assets/javascripts/components/content/mermaid/index.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 - -/** - * Global sequence number for diagrams - */ -let sequence = 0 - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch Mermaid script - * - * @returns Mermaid scripts observable - */ -function fetchScripts(): Observable { - return typeof mermaid === "undefined" || mermaid instanceof Element - ? watchScript("https://unpkg.com/mermaid@9.4.3/dist/mermaid.min.js") - : of(undefined) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount Mermaid diagram - * - * @param el - Code block element - * - * @returns Mermaid diagram component observable - */ -export function mountMermaid( - el: HTMLElement -): Observable> { - el.classList.remove("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - mermaid$ ||= fetchScripts() - .pipe( - tap(() => mermaid.initialize({ - startOnLoad: false, - themeCSS, - sequence: { - actorFontSize: "16px", // Hack: mitigate https://bit.ly/3y0NEi3 - messageFontSize: "16px", - noteFontSize: "16px" - } - })), - map(() => undefined), - shareReplay(1) - ) - - /* Render diagram */ - mermaid$.subscribe(() => { - el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du - const id = `__mermaid_${sequence++}` - - /* Create host element to replace code block */ - const host = h("div", { class: "mermaid" }) - const text = el.textContent - - /* Render and inject diagram */ - mermaid.mermaidAPI.render(id, text, (svg: string, fn: Function) => { - - /* Create a shadow root and inject diagram */ - const shadow = host.attachShadow({ mode: "closed" }) - shadow.innerHTML = svg - - /* Replace code block with diagram and bind functions */ - el.replaceWith(host) - fn?.(shadow) - }) - }) - - /* Create and return component */ - return mermaid$ - .pipe( - map(() => ({ ref: el })) - ) -} diff --git a/src/templates/assets/javascripts/components/content/table/index.ts b/src/templates/assets/javascripts/components/content/table/index.ts deleted file mode 100644 index c318e7a6..00000000 --- a/src/templates/assets/javascripts/components/content/table/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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> { - el.replaceWith(sentinel) - sentinel.replaceWith(renderTable(el)) - - /* Create and return component */ - return of({ ref: el }) -} diff --git a/src/templates/assets/javascripts/components/content/tabs/index.ts b/src/templates/assets/javascripts/components/content/tabs/index.ts deleted file mode 100644 index f57447e2..00000000 --- a/src/templates/assets/javascripts/components/content/tabs/index.ts +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - merge, - skip, - startWith, - subscribeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContentOffset, - getElementContentSize, - getElementOffset, - getElementSize, - getElements, - watchElementContentOffset, - watchElementSize -} from "~/browser" -import { renderTabbedControl } from "~/templates" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Content tabs - */ -export interface ContentTabs { - active: HTMLLabelElement /* Active tab label */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch content tabs - * - * @param el - Content tabs element - * - * @returns Content tabs observable - */ -export function watchContentTabs( - el: HTMLElement -): Observable { - const inputs = getElements(":scope > input", el) - const initial = inputs.find(input => input.checked) || inputs[0] - return merge(...inputs.map(input => fromEvent(input, "change") - .pipe( - map(() => getElement(`label[for="${input.id}"]`)) - ) - )) - .pipe( - startWith(getElement(`label[for="${initial.id}"]`)), - map(active => ({ active })) - ) -} - -/** - * Mount content tabs - * - * This function scrolls the active tab into view. While this functionality is - * provided by browsers as part of `scrollInfoView`, browsers will always also - * scroll the vertical axis, which we do not want. Thus, we decided to provide - * this functionality ourselves. - * - * @param el - Content tabs element - * @param options - Options - * - * @returns Content tabs component observable - */ -export function mountContentTabs( - el: HTMLElement, { viewport$ }: MountOptions -): Observable> { - - /* 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() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - combineLatest([push$, watchElementSize(el)]) - .pipe( - auditTime(1, animationFrameScheduler), - takeUntil(done$) - ) - .subscribe({ - - /* Handle emission */ - next([{ active }, size]) { - const offset = getElementOffset(active) - const { width } = getElementSize(active) - - /* Set tab indicator offset and width */ - el.style.setProperty("--md-indicator-x", `${offset.x}px`) - el.style.setProperty("--md-indicator-width", `${width}px`) - - /* Scroll container to active content tab */ - const content = getElementContentOffset(container) - if ( - offset.x < content.x || - offset.x + width > content.x + size.width - ) - container.scrollTo({ - left: Math.max(0, offset.x - 16), - behavior: "smooth" - }) - }, - - /* Handle complete */ - complete() { - el.style.removeProperty("--md-indicator-x") - el.style.removeProperty("--md-indicator-width") - } - }) - - /* Hide content tab buttons on borders */ - combineLatest([ - watchElementContentOffset(container), - watchElementSize(container) - ]) - .pipe( - takeUntil(done$) - ) - .subscribe(([offset, size]) => { - const content = getElementContentSize(container) - prev.hidden = offset.x < 16 - next.hidden = offset.x > content.width - size.width - 16 - }) - - /* Paginate content tab container on click */ - merge( - fromEvent(prev, "click").pipe(map(() => -1)), - fromEvent(next, "click").pipe(map(() => +1)) - ) - .pipe( - takeUntil(done$) - ) - .subscribe(direction => { - const { width } = getElementSize(container) - container.scrollBy({ - left: width * direction, - behavior: "smooth" - }) - }) - - /* Set up linking of content tabs, if enabled */ - if (feature("content.tabs.link")) - push$.pipe( - skip(1), - withLatestFrom(viewport$) - ) - .subscribe(([{ active }, { offset }]) => { - const tab = active.innerText.trim() - if (active.hasAttribute("data-md-switching")) { - active.removeAttribute("data-md-switching") - - /* Determine viewport offset of active tab */ - } else { - const y = el.offsetTop - offset.y - - /* Passively activate other tabs */ - for (const set of getElements("[data-tabs]")) - for (const input of getElements( - ":scope > input", set - )) { - const label = getElement(`label[for="${input.id}"]`) - if ( - label !== active && - label.innerText.trim() === tab - ) { - label.setAttribute("data-md-switching", "") - input.click() - break - } - } - - /* Bring active tab into view */ - window.scrollTo({ - top: el.offsetTop - y - }) - - /* Persist active tabs in local storage */ - const tabs = __md_get("__tabs") || [] - __md_set("__tabs", [...new Set([tab, ...tabs])]) - } - }) - - /* Pause media (audio, video) on switch - see https://bit.ly/3Bk6cel */ - push$.pipe(takeUntil(done$)) - .subscribe(() => { - for (const media of getElements("audio, video", el)) - media.pause() - }) - - /* Create and return component */ - return watchContentTabs(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) - .pipe( - subscribeOn(asyncScheduler) - ) -} diff --git a/src/templates/assets/javascripts/components/dialog/index.ts b/src/templates/assets/javascripts/components/dialog/index.ts deleted file mode 100644 index 6ff1bd44..00000000 --- a/src/templates/assets/javascripts/components/dialog/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* Alert subject */ -} - -/** - * Mount options - */ -interface MountOptions { - alert$: Subject /* Alert subject */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch dialog - * - * @param _el - Dialog element - * @param options - Options - * - * @returns Dialog observable - */ -export function watchDialog( - _el: HTMLElement, { alert$ }: WatchOptions -): Observable { - 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> { - const inner = getElement(".md-typeset", el) - return defer(() => { - const push$ = new Subject() - push$.subscribe(({ message, active }) => { - el.classList.toggle("md-dialog--active", active) - inner.textContent = message - }) - - /* Create and return component */ - return watchDialog(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/header/_/index.ts b/src/templates/assets/javascripts/components/header/_/index.ts deleted file mode 100644 index 0f33eb48..00000000 --- a/src/templates/assets/javascripts/components/header/_/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - bufferCount, - combineLatest, - combineLatestWith, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - ignoreElements, - map, - of, - shareReplay, - startWith, - switchMap, - takeUntil -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - watchElementSize, - watchToggle -} from "~/browser" - -import { Component } from "../../_" -import { Main } from "../../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface Header { - height: number /* Header visible height */ - hidden: boolean /* Header is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable /* Viewport observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* Header observable */ - main$: Observable
    /* 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 { - 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
    { - 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> { - return defer(() => { - const push$ = new Subject
    () - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$ - .pipe( - distinctUntilKeyChanged("active"), - combineLatestWith(header$) - ) - .subscribe(([{ active }, { hidden }]) => { - el.classList.toggle("md-header--shadow", active && !hidden) - el.hidden = hidden - }) - - /* Link to main area */ - main$.subscribe(push$) - - /* Create and return component */ - return header$ - .pipe( - takeUntil(done$), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/header/index.ts b/src/templates/assets/javascripts/components/header/index.ts deleted file mode 100644 index cf23ec1a..00000000 --- a/src/templates/assets/javascripts/components/header/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./title" diff --git a/src/templates/assets/javascripts/components/header/title/index.ts b/src/templates/assets/javascripts/components/header/title/index.ts deleted file mode 100644 index f3bc0d08..00000000 --- a/src/templates/assets/javascripts/components/header/title/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - header$: Observable
    /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* 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 { - 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> { - return defer(() => { - const push$ = new Subject() - push$.subscribe({ - - /* Handle emission */ - next({ active }) { - el.classList.toggle("md-header__title--active", active) - }, - - /* Handle complete */ - complete() { - el.classList.remove("md-header__title--active") - } - }) - - /* Obtain headline, if any */ - const heading = getOptionalElement(".md-content h1") - if (typeof heading === "undefined") - return EMPTY - - /* Create and return component */ - return watchHeaderTitle(heading, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/index.ts b/src/templates/assets/javascripts/components/index.ts deleted file mode 100644 index 3d4391d1..00000000 --- a/src/templates/assets/javascripts/components/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./announce" -export * from "./consent" -export * from "./content" -export * from "./dialog" -export * from "./header" -export * from "./main" -export * from "./palette" -export * from "./progress" -export * from "./search" -export * from "./sidebar" -export * from "./source" -export * from "./tabs" -export * from "./toc" -export * from "./top" diff --git a/src/templates/assets/javascripts/components/main/index.ts b/src/templates/assets/javascripts/components/main/index.ts deleted file mode 100644 index 2509f9b9..00000000 --- a/src/templates/assets/javascripts/components/main/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - header$: Observable
    /* 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
    { - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - map(({ height }) => height), - distinctUntilChanged() - ) - - /* Compute the main area's top and bottom borders */ - const border$ = adjust$ - .pipe( - switchMap(() => watchElementSize(el) - .pipe( - map(({ height }) => ({ - top: el.offsetTop, - bottom: el.offsetTop + height - })), - distinctUntilKeyChanged("bottom") - ) - ) - ) - - /* Compute the main area's offset, visible height and if we scrolled past */ - return combineLatest([adjust$, border$, viewport$]) - .pipe( - map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => { - height = Math.max(0, height - - Math.max(0, top - y, header) - - Math.max(0, height + y - bottom) - ) - return { - offset: top - header, - height, - active: top - header <= y - } - }), - distinctUntilChanged((a, b) => ( - a.offset === b.offset && - a.height === b.height && - a.active === b.active - )) - ) -} diff --git a/src/templates/assets/javascripts/components/palette/index.ts b/src/templates/assets/javascripts/components/palette/index.ts deleted file mode 100644 index cf578f60..00000000 --- a/src/templates/assets/javascripts/components/palette/index.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - defer, - finalize, - fromEvent, - map, - mergeMap, - observeOn, - of, - shareReplay, - startWith, - tap -} from "rxjs" - -import { getElements } from "~/browser" -import { h } from "~/utilities" - -import { - Component, - getComponentElement -} from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Palette colors - */ -export interface PaletteColor { - scheme?: string /* Color scheme */ - primary?: string /* Primary color */ - accent?: string /* Accent color */ -} - -/** - * Palette - */ -export interface Palette { - index: number /* Palette index */ - color: PaletteColor /* Palette colors */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch color palette - * - * @param inputs - Color palette element - * - * @returns Color palette observable - */ -export function watchPalette( - inputs: HTMLInputElement[] -): Observable { - const current = __md_get("__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> { - const meta = h("meta", { name: "theme-color" }) - document.head.appendChild(meta) - - // Add color scheme meta tag - const scheme = h("meta", { name: "color-scheme" }) - document.head.appendChild(scheme) - - /* Mount component on subscription */ - return defer(() => { - const push$ = new Subject() - push$.subscribe(palette => { - document.body.setAttribute("data-md-color-switching", "") - - /* Set color palette */ - for (const [key, value] of Object.entries(palette.color)) - document.body.setAttribute(`data-md-color-${key}`, value) - - /* Toggle visibility */ - for (let index = 0; index < inputs.length; index++) { - const label = inputs[index].nextElementSibling - if (label instanceof HTMLElement) - label.hidden = palette.index !== index - } - - /* Persist preference in local storage */ - __md_set("__palette", palette) - }) - - /* Update theme-color meta tag */ - push$ - .pipe( - map(() => { - const header = getComponentElement("header") - const style = window.getComputedStyle(header) - - // Set color scheme - scheme.content = style.colorScheme - - /* Return color in hexadecimal format */ - return style.backgroundColor.match(/\d+/g)! - .map(value => (+value).toString(16).padStart(2, "0")) - .join("") - }) - ) - .subscribe(color => meta.content = `#${color}`) - - /* Revert transition durations after color switch */ - push$.pipe(observeOn(asyncScheduler)) - .subscribe(() => { - document.body.removeAttribute("data-md-color-switching") - }) - - /* Create and return component */ - const inputs = getElements("input", el) - return watchPalette(inputs) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/progress/index.ts b/src/templates/assets/javascripts/components/progress/index.ts deleted file mode 100644 index 30c722b8..00000000 --- a/src/templates/assets/javascripts/components/progress/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - defer, - finalize, - map, - tap -} from "rxjs" - -import { Component } from "../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Progress indicator - */ -export interface Progress { - value: number // Progress value -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - progress$: Subject // Progress subject -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount progress indicator - * - * @param el - Progress indicator element - * @param options - Options - * - * @returns Progress indicator component observable - */ -export function mountProgress( - el: HTMLElement, { progress$ }: MountOptions -): Observable> { - - // Mount component on subscription - return defer(() => { - const push$ = new Subject() - push$.subscribe(({ value }) => { - el.style.setProperty("--md-progress-value", `${value}`) - }) - - // Create and return component - return progress$ - .pipe( - tap(value => push$.next({ value })), - finalize(() => push$.complete()), - map(value => ({ ref: el, value })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/search/_/index.ts b/src/templates/assets/javascripts/components/search/_/index.ts deleted file mode 100644 index aa963b47..00000000 --- a/src/templates/assets/javascripts/components/search/_/index.ts +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - NEVER, - Observable, - ObservableInput, - filter, - fromEvent, - merge, - mergeWith -} from "rxjs" - -import { configuration } from "~/_" -import { - Keyboard, - getActiveElement, - getElements, - setToggle -} from "~/browser" -import { - SearchIndex, - SearchResult, - setupSearchWorker -} from "~/integrations" - -import { - Component, - getComponentElement, - getComponentElements -} from "../../_" -import { - SearchQuery, - mountSearchQuery -} from "../query" -import { mountSearchResult } from "../result" -import { - SearchShare, - mountSearchShare -} from "../share" -import { - SearchSuggest, - mountSearchSuggest -} from "../suggest" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search - */ -export type Search = - | SearchQuery - | SearchResult - | SearchShare - | SearchSuggest - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput /* Search index observable */ - keyboard$: Observable /* 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> { - const config = configuration() - try { - const worker$ = setupSearchWorker(config.search, index$) - - /* Retrieve query and result components */ - const query = getComponentElement("search-query", el) - const result = getComponentElement("search-result", el) - - /* Always close search on result selection */ - fromEvent(el, "click") - .pipe( - filter(({ target }) => ( - target instanceof Element && !!target.closest("a") - )) - ) - .subscribe(() => setToggle("search", false)) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - const active = getActiveElement() - switch (key.type) { - - /* Enter: go to first (best) result */ - case "Enter": - if (active === query) { - const anchors = new Map() - for (const anchor of getElements( - ":first-child [href]", result - )) { - const article = anchor.firstElementChild! - anchors.set(anchor, parseFloat( - article.getAttribute("data-md-score")! - )) - } - - /* Go to result with highest score, if any */ - if (anchors.size) { - const [[best]] = [...anchors].sort(([, a], [, b]) => b - a) - best.click() - } - - /* Otherwise omit form submission */ - key.claim() - } - break - - /* Escape or Tab: close search */ - case "Escape": - case "Tab": - setToggle("search", false) - query.blur() - break - - /* Vertical arrows: select previous or next search result */ - case "ArrowUp": - case "ArrowDown": - if (typeof active === "undefined") { - query.focus() - } else { - const els = [query, ...getElements( - ":not(details) > [href], summary, details[open] [href]", - result - )] - const i = Math.max(0, ( - Math.max(0, els.indexOf(active)) + els.length + ( - key.type === "ArrowUp" ? -1 : +1 - ) - ) % els.length) - els[i].focus() - } - - /* Prevent scrolling of page */ - key.claim() - break - - /* All other keys: hand to search query */ - default: - if (query !== getActiveElement()) - query.focus() - } - }) - - /* Set up global keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "global") - ) - .subscribe(key => { - switch (key.type) { - - /* Open search and select query */ - case "f": - case "s": - case "/": - query.focus() - query.select() - - /* Prevent scrolling of page */ - key.claim() - break - } - }) - - /* Create and return component */ - const query$ = mountSearchQuery(query, { worker$ }) - return merge( - query$, - mountSearchResult(result, { worker$, query$ }) - ) - .pipe( - mergeWith( - - /* Search sharing */ - ...getComponentElements("search-share", el) - .map(child => mountSearchShare(child, { query$ })), - - /* Search suggestions */ - ...getComponentElements("search-suggest", el) - .map(child => mountSearchSuggest(child, { worker$, keyboard$ })) - ) - ) - - /* Gracefully handle broken search */ - } catch (err) { - el.hidden = true - return NEVER - } -} diff --git a/src/templates/assets/javascripts/components/search/highlight/.eslintrc b/src/templates/assets/javascripts/components/search/highlight/.eslintrc deleted file mode 100644 index 38a5714d..00000000 --- a/src/templates/assets/javascripts/components/search/highlight/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/components/search/highlight/index.ts b/src/templates/assets/javascripts/components/search/highlight/index.ts deleted file mode 100644 index bc3f94c9..00000000 --- a/src/templates/assets/javascripts/components/search/highlight/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* Map of replacements */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - index$: ObservableInput /* Search index observable */ - location$: Observable /* 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> { - return combineLatest([ - index$, - location$ - .pipe( - startWith(getLocation()), - filter(url => !!url.searchParams.get("h")) - ) - ]) - .pipe( - map(([index, url]) => setupSearchHighlighter(index.config)( - url.searchParams.get("h")! - )), - map(fn => { - const nodes = new Map() - - /* Traverse text nodes and collect matches */ - const it = document.createNodeIterator(el, NodeFilter.SHOW_TEXT) - for (let node = it.nextNode(); node; node = it.nextNode()) { - if (node.parentElement?.offsetHeight) { - const original = node.textContent! - const replaced = fn(original) - if (replaced.length > original.length) - nodes.set(node as ChildNode, replaced) - } - } - - /* Replace original nodes with matches */ - for (const [node, text] of nodes) { - const { childNodes } = h("span", null, text) - node.replaceWith(...Array.from(childNodes)) - } - - /* Return component */ - return { ref: el, nodes } - }) - ) -} diff --git a/src/templates/assets/javascripts/components/search/index.ts b/src/templates/assets/javascripts/components/search/index.ts deleted file mode 100644 index 846d8685..00000000 --- a/src/templates/assets/javascripts/components/search/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./highlight" -export * from "./query" -export * from "./result" -export * from "./share" -export * from "./suggest" diff --git a/src/templates/assets/javascripts/components/search/query/index.ts b/src/templates/assets/javascripts/components/search/query/index.ts deleted file mode 100644 index 4ce21279..00000000 --- a/src/templates/assets/javascripts/components/search/query/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - first, - fromEvent, - ignoreElements, - map, - merge, - shareReplay, - takeUntil, - tap -} from "rxjs" - -import { - getElement, - getLocation, - setToggle, - watchElementFocus, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchMessageType, - isSearchReadyMessage -} from "~/integrations" - -import { Component } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search query - */ -export interface SearchQuery { - value: string /* Query value */ - focus: boolean /* Query focus */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - worker$: Subject /* Search worker */ -} - -/** - * Mount options - */ -interface MountOptions { - worker$: Subject /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch search query - * - * Note that the focus event which triggers re-reading the current query value - * is delayed by `1ms` so the input's empty state is allowed to propagate. - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query observable - */ -export function watchSearchQuery( - el: HTMLInputElement, { worker$ }: WatchOptions -): Observable { - - /* Support search deep linking */ - const { searchParams } = getLocation() - if (searchParams.has("q")) { - setToggle("search", true) - - /* Set query from parameter */ - el.value = searchParams.get("q")! - el.focus() - - /* Remove query parameter on close */ - watchToggle("search") - .pipe( - first(active => !active) - ) - .subscribe(() => { - const url = getLocation() - url.searchParams.delete("q") - history.replaceState({}, "", `${url}`) - }) - } - - /* Intercept focus and input events */ - const focus$ = watchElementFocus(el) - const value$ = merge( - worker$.pipe(first(isSearchReadyMessage)), - fromEvent(el, "keyup"), - focus$ - ) - .pipe( - map(() => el.value), - distinctUntilChanged() - ) - - /* Combine into single observable */ - return combineLatest([value$, focus$]) - .pipe( - map(([value, focus]) => ({ value, focus })), - shareReplay(1) - ) -} - -/** - * Mount search query - * - * @param el - Search query element - * @param options - Options - * - * @returns Search query component observable - */ -export function mountSearchQuery( - el: HTMLInputElement, { worker$ }: MountOptions -): Observable> { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - - /* Handle value change */ - combineLatest([ - worker$.pipe(first(isSearchReadyMessage)), - push$ - ], (_, query) => query) - .pipe( - distinctUntilKeyChanged("value") - ) - .subscribe(({ value }) => worker$.next({ - type: SearchMessageType.QUERY, - data: value - })) - - /* Handle focus change */ - push$ - .pipe( - distinctUntilKeyChanged("focus") - ) - .subscribe(({ focus }) => { - if (focus) - setToggle("search", focus) - }) - - /* Handle reset */ - fromEvent(el.form!, "reset") - .pipe( - takeUntil(done$) - ) - .subscribe(() => el.focus()) - - // Focus search query on label click - note that this is necessary to bring - // up the keyboard on iOS and other mobile platforms, as the search dialog is - // not visible at first, and programatically focusing an input element must - // be triggered by a user interaction - see https://t.ly/Cb30n - const label = getElement("header [for=__search]") - fromEvent(label, "click") - .subscribe(() => el.focus()) - - /* Create and return component */ - return watchSearchQuery(el, { worker$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })), - shareReplay(1) - ) -} diff --git a/src/templates/assets/javascripts/components/search/result/index.ts b/src/templates/assets/javascripts/components/search/result/index.ts deleted file mode 100644 index c3c9ef20..00000000 --- a/src/templates/assets/javascripts/components/search/result/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - bufferCount, - filter, - finalize, - first, - fromEvent, - map, - merge, - mergeMap, - of, - share, - skipUntil, - switchMap, - takeUntil, - tap, - withLatestFrom, - zipWith -} from "rxjs" - -import { translation } from "~/_" -import { - getElement, - getOptionalElement, - watchElementBoundary, - watchToggle -} from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchReadyMessage, - isSearchResultMessage -} from "~/integrations" -import { renderSearchResultItem } from "~/templates" -import { round } from "~/utilities" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - query$: Observable /* Search query observable */ - worker$: Subject /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search result list - * - * This function performs a lazy rendering of the search results, depending on - * the vertical offset of the search result container. - * - * @param el - Search result list element - * @param options - Options - * - * @returns Search result list component observable - */ -export function mountSearchResult( - el: HTMLElement, { worker$, query$ }: MountOptions -): Observable> { - const push$ = new Subject() - const boundary$ = watchElementBoundary(el.parentElement!) - .pipe( - filter(Boolean) - ) - - /* Retrieve container */ - const container = el.parentElement! - - /* Retrieve nested components */ - const meta = getElement(":scope > :first-child", el) - const list = getElement(":scope > :last-child", el) - - /* Reveal to accessibility tree – see https://bit.ly/3iAA7t8 */ - watchToggle("search") - .subscribe(active => list.setAttribute( - "role", active ? "list" : "presentation" - )) - - /* Update search result metadata */ - push$ - .pipe( - withLatestFrom(query$), - skipUntil(worker$.pipe(first(isSearchReadyMessage))) - ) - .subscribe(([{ items }, { value }]) => { - switch (items.length) { - - /* No results */ - case 0: - meta.textContent = value.length - ? translation("search.result.none") - : translation("search.result.placeholder") - break - - /* One result */ - case 1: - meta.textContent = translation("search.result.one") - break - - /* Multiple result */ - default: - const count = round(items.length) - meta.textContent = translation("search.result.other", count) - } - }) - - /* Render search result item */ - const render$ = push$ - .pipe( - tap(() => list.innerHTML = ""), - switchMap(({ items }) => merge( - of(...items.slice(0, 10)), - of(...items.slice(10)) - .pipe( - bufferCount(4), - zipWith(boundary$), - switchMap(([chunk]) => chunk) - ) - )), - map(renderSearchResultItem), - share() - ) - - /* Update search result list */ - render$.subscribe(item => list.appendChild(item)) - render$ - .pipe( - mergeMap(item => { - const details = getOptionalElement("details", item) - if (typeof details === "undefined") - return EMPTY - - /* Keep position of details element stable */ - return fromEvent(details, "toggle") - .pipe( - takeUntil(push$), - map(() => details) - ) - }) - ) - .subscribe(details => { - if ( - details.open === false && - details.offsetTop <= container.scrollTop - ) - container.scrollTo({ top: details.offsetTop }) - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/search/share/index.ts b/src/templates/assets/javascripts/components/search/share/index.ts deleted file mode 100644 index 3db382c8..00000000 --- a/src/templates/assets/javascripts/components/search/share/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - takeUntil, - tap -} from "rxjs" - -import { getLocation } from "~/browser" - -import { Component } from "../../_" -import { SearchQuery } from "../query" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search sharing - */ -export interface SearchShare { - url: URL /* Deep link for sharing */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - query$: Observable /* Search query observable */ -} - -/** - * Mount options - */ -interface MountOptions { - query$: Observable /* 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 { - return query$ - .pipe( - map(({ value }) => { - const url = getLocation() - url.hash = "" - - /* Compute readable query strings */ - value = value - .replace(/\s+/g, "+") /* Collapse whitespace */ - .replace(/&/g, "%26") /* Escape '&' character */ - .replace(/=/g, "%3D") /* Escape '=' character */ - - /* Replace query string */ - url.search = `q=${value}` - return { url } - }) - ) -} - -/** - * Mount search sharing - * - * @param el - Search sharing element - * @param options - Options - * - * @returns Search sharing component observable - */ -export function mountSearchShare( - el: HTMLAnchorElement, options: MountOptions -): Observable> { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ url }) => { - el.setAttribute("data-clipboard-text", el.href) - el.href = `${url}` - }) - - /* Prevent following of link */ - fromEvent(el, "click") - .pipe( - takeUntil(done$) - ) - .subscribe(ev => ev.preventDefault()) - - /* Create and return component */ - return watchSearchShare(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/components/search/suggest/index.ts b/src/templates/assets/javascripts/components/search/suggest/index.ts deleted file mode 100644 index e7881475..00000000 --- a/src/templates/assets/javascripts/components/search/suggest/index.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - combineLatestWith, - distinctUntilChanged, - filter, - finalize, - fromEvent, - map, - merge, - observeOn, - tap -} from "rxjs" - -import { Keyboard } from "~/browser" -import { - SearchMessage, - SearchResult, - isSearchResultMessage -} from "~/integrations" - -import { Component, getComponentElement } from "../../_" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search suggestions - */ -export interface SearchSuggest {} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Mount options - */ -interface MountOptions { - keyboard$: Observable /* Keyboard observable */ - worker$: Subject /* Search worker */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Mount search suggestions - * - * This function will perform a lazy rendering of the search results, depending - * on the vertical offset of the search result container. - * - * @param el - Search result list element - * @param options - Options - * - * @returns Search result list component observable - */ -export function mountSearchSuggest( - el: HTMLElement, { worker$, keyboard$ }: MountOptions -): Observable> { - const push$ = new Subject() - - /* Retrieve query component and track all changes */ - const query = getComponentElement("search-query") - const query$ = merge( - fromEvent(query, "keydown"), - fromEvent(query, "focus") - ) - .pipe( - observeOn(asyncScheduler), - map(() => query.value), - distinctUntilChanged(), - ) - - /* Update search suggestions */ - push$ - .pipe( - combineLatestWith(query$), - map(([{ suggest }, value]) => { - const words = value.split(/([\s-]+)/) - if (suggest?.length && words[words.length - 1]) { - const last = suggest[suggest.length - 1] - if (last.startsWith(words[words.length - 1])) - words[words.length - 1] = last - } else { - words.length = 0 - } - return words - }) - ) - .subscribe(words => el.innerHTML = words - .join("") - .replace(/\s/g, " ") - ) - - /* Set up search keyboard handlers */ - keyboard$ - .pipe( - filter(({ mode }) => mode === "search") - ) - .subscribe(key => { - switch (key.type) { - - /* Right arrow: accept current suggestion */ - case "ArrowRight": - if ( - el.innerText.length && - query.selectionStart === query.value.length - ) - query.value = el.innerText - break - } - }) - - /* Filter search result message */ - const result$ = worker$ - .pipe( - filter(isSearchResultMessage), - map(({ data }) => data) - ) - - /* Create and return component */ - return result$ - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(() => ({ ref: el })) - ) -} diff --git a/src/templates/assets/javascripts/components/sidebar/index.ts b/src/templates/assets/javascripts/components/sidebar/index.ts deleted file mode 100644 index 82f3d03e..00000000 --- a/src/templates/assets/javascripts/components/sidebar/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - animationFrameScheduler, - asyncScheduler, - auditTime, - combineLatest, - defer, - distinctUntilChanged, - endWith, - finalize, - first, - from, - fromEvent, - ignoreElements, - map, - mergeMap, - observeOn, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { - Viewport, - getElement, - getElementContainer, - getElementOffset, - getElementSize, - getElements -} from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sidebar - */ -export interface Sidebar { - height: number /* Sidebar height */ - locked: boolean /* Sidebar is locked */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable /* Viewport observable */ - main$: Observable
    /* Main area observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* Header observable */ - main$: Observable
    /* 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 { - const parent = el.closest(".md-grid")! - const adjust = - parent.offsetTop - - parent.parentElement!.offsetTop - - /* Compute the sidebar's available height and if it should be locked */ - return combineLatest([main$, viewport$]) - .pipe( - map(([{ offset, height }, { offset: { y } }]) => { - height = height - + Math.min(adjust, Math.max(0, y - offset)) - - adjust - return { - height, - locked: y >= offset + adjust - } - }), - distinctUntilChanged((a, b) => ( - a.height === b.height && - a.locked === b.locked - )) - ) -} - -/** - * Mount sidebar - * - * This function doesn't set the height of the actual sidebar, but of its first - * child – the `.md-sidebar__scrollwrap` element in order to mitigiate jittery - * sidebars when the footer is scrolled into view. At some point we switched - * from `absolute` / `fixed` positioning to `sticky` positioning, significantly - * reducing jitter in some browsers (respectively Firefox and Safari) when - * scrolling from the top. However, top-aligned sticky positioning means that - * the sidebar snaps to the bottom when the end of the container is reached. - * This is what leads to the mentioned jitter, as the sidebar's height may be - * updated too slowly. - * - * This behaviour can be mitigiated by setting the height of the sidebar to `0` - * while preserving the padding, and the height on its first element. - * - * @param el - Sidebar element - * @param options - Options - * - * @returns Sidebar component observable - */ -export function mountSidebar( - el: HTMLElement, { header$, ...options }: MountOptions -): Observable> { - const inner = getElement(".md-sidebar__scrollwrap", el) - const { y } = getElementOffset(inner) - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - const next$ = push$ - .pipe( - auditTime(0, animationFrameScheduler) - ) - - /* Update sidebar height and offset */ - next$.pipe(withLatestFrom(header$)) - .subscribe({ - - /* Handle emission */ - next([{ height }, { height: offset }]) { - inner.style.height = `${height - 2 * y}px` - el.style.top = `${offset}px` - }, - - /* Handle complete */ - complete() { - inner.style.height = "" - el.style.top = "" - } - }) - - /* Bring active item into view on initial load */ - next$.pipe(first()) - .subscribe(() => { - for (const item of getElements(".md-nav__link--active[href]", el)) { - const container = getElementContainer(item) - if (typeof container !== "undefined") { - const offset = item.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2 - }) - } - } - }) - - /* Handle accessibility for expandable items, see https://bit.ly/3jaod9p */ - from(getElements("label[tabindex]", el)) - .pipe( - mergeMap(label => fromEvent(label, "click") - .pipe( - observeOn(asyncScheduler), - map(() => label), - takeUntil(done$) - ) - ) - ) - .subscribe(label => { - const input = getElement(`[id="${label.htmlFor}"]`) - const nav = getElement(`[aria-labelledby="${label.id}"]`) - nav.setAttribute("aria-expanded", `${input.checked}`) - }) - - /* Create and return component */ - return watchSidebar(el, options) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/source/_/index.ts b/src/templates/assets/javascripts/components/source/_/index.ts deleted file mode 100644 index 5f6c4d11..00000000 --- a/src/templates/assets/javascripts/components/source/_/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - catchError, - defer, - filter, - finalize, - map, - of, - shareReplay, - tap -} from "rxjs" - -import { getElement } from "~/browser" -import { ConsentDefaults } from "~/components/consent" -import { renderSourceFacts } from "~/templates" - -import { - Component, - getComponentElements -} from "../../_" -import { - SourceFacts, - fetchSourceFacts -} from "../facts" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Repository information - */ -export interface Source { - facts: SourceFacts /* Repository facts */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Repository information observable - */ -let fetch$: Observable - -/* ---------------------------------------------------------------------------- - * 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 { - return fetch$ ||= defer(() => { - const cached = __md_get("__source", sessionStorage) - if (cached) { - return of(cached) - } else { - - /* Check if consent is configured and was given */ - const els = getComponentElements("consent") - if (els.length) { - const consent = __md_get("__consent") - if (!(consent && consent.github)) - return EMPTY - } - - /* Fetch repository facts */ - return fetchSourceFacts(el.href) - .pipe( - tap(facts => __md_set("__source", facts, sessionStorage)) - ) - } - }) - .pipe( - catchError(() => EMPTY), - filter(facts => Object.keys(facts).length > 0), - map(facts => ({ facts })), - shareReplay(1) - ) -} - -/** - * Mount repository information - * - * @param el - Repository information element - * - * @returns Repository information component observable - */ -export function mountSource( - el: HTMLAnchorElement -): Observable> { - const inner = getElement(":scope > :last-child", el) - return defer(() => { - const push$ = new Subject() - push$.subscribe(({ facts }) => { - inner.appendChild(renderSourceFacts(facts)) - inner.classList.add("md-source__repository--active") - }) - - /* Create and return component */ - return watchSource(el) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/source/facts/_/index.ts b/src/templates/assets/javascripts/components/source/facts/_/index.ts deleted file mode 100644 index 154f229f..00000000 --- a/src/templates/assets/javascripts/components/source/facts/_/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - - /* Try to match GitHub repository */ - let match = url.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i) - if (match) { - const [, user, repo] = match - return fetchSourceFactsFromGitHub(user, repo) - } - - /* Try to match GitLab repository */ - match = url.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i) - if (match) { - const [, base, slug] = match - return fetchSourceFactsFromGitLab(base, slug) - } - - /* Fallback */ - return EMPTY -} diff --git a/src/templates/assets/javascripts/components/source/facts/github/index.ts b/src/templates/assets/javascripts/components/source/facts/github/index.ts deleted file mode 100644 index 12cc55e0..00000000 --- a/src/templates/assets/javascripts/components/source/facts/github/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - if (typeof repo !== "undefined") { - const url = `https://api.github.com/repos/${user}/${repo}` - return zip( - - /* Fetch version */ - requestJSON(`${url}/releases/latest`) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(release => ({ - version: release.tag_name - })), - defaultIfEmpty({}) - ), - - /* Fetch stars and forks */ - requestJSON(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(url) - .pipe( - map(info => ({ - repositories: info.public_repos - })), - defaultIfEmpty({}) - ) - } -} diff --git a/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts b/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts deleted file mode 100644 index d85d4afd..00000000 --- a/src/templates/assets/javascripts/components/source/facts/gitlab/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 { - const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}` - return requestJSON(url) - .pipe( - catchError(() => EMPTY), // @todo refactor instant loading - map(({ star_count, forks_count }) => ({ - stars: star_count, - forks: forks_count - })), - defaultIfEmpty({}) - ) -} diff --git a/src/templates/assets/javascripts/components/source/facts/index.ts b/src/templates/assets/javascripts/components/source/facts/index.ts deleted file mode 100644 index f9bda64d..00000000 --- a/src/templates/assets/javascripts/components/source/facts/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./github" -export * from "./gitlab" diff --git a/src/templates/assets/javascripts/components/source/index.ts b/src/templates/assets/javascripts/components/source/index.ts deleted file mode 100644 index 7fac4813..00000000 --- a/src/templates/assets/javascripts/components/source/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./facts" diff --git a/src/templates/assets/javascripts/components/tabs/index.ts b/src/templates/assets/javascripts/components/tabs/index.ts deleted file mode 100644 index 1e69df28..00000000 --- a/src/templates/assets/javascripts/components/tabs/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - header$: Observable
    /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* 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 { - 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> { - return defer(() => { - const push$ = new Subject() - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - }, - - /* Handle complete */ - complete() { - el.hidden = false - } - }) - - /* Create and return component */ - return ( - feature("navigation.tabs.sticky") - ? of({ hidden: false }) - : watchTabs(el, options) - ) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/toc/index.ts b/src/templates/assets/javascripts/components/toc/index.ts deleted file mode 100644 index 04b8d85f..00000000 --- a/src/templates/assets/javascripts/components/toc/index.ts +++ /dev/null @@ -1,379 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - asyncScheduler, - bufferCount, - combineLatestWith, - debounceTime, - defer, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - filter, - finalize, - ignoreElements, - map, - merge, - observeOn, - of, - repeat, - scan, - share, - skip, - startWith, - switchMap, - takeUntil, - tap, - withLatestFrom -} from "rxjs" - -import { feature } from "~/_" -import { - Viewport, - getElement, - getElementContainer, - getElementSize, - getElements, - getLocation, - getOptionalElement, - watchElementSize -} from "~/browser" - -import { - Component, - getComponentElement -} from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Table of contents - */ -export interface TableOfContents { - prev: HTMLAnchorElement[][] /* Anchors (previous) */ - next: HTMLAnchorElement[][] /* Anchors (next) */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* Header observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* Header observable */ - main$: Observable
    /* Main area observable */ - target$: Observable /* 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 { - const table = new Map() - - /* Compute anchor-to-target mapping */ - const anchors = getElements("[href^=\\#]", el) - for (const anchor of anchors) { - const id = decodeURIComponent(anchor.hash.substring(1)) - const target = getOptionalElement(`[id="${id}"]`) - if (typeof target !== "undefined") - table.set(anchor, target) - } - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - distinctUntilKeyChanged("height"), - map(({ height }) => { - const main = getComponentElement("main") - const grid = getElement(":scope > :first-child", main) - return height + 0.8 * ( - grid.offsetTop - - main.offsetTop - ) - }), - share() - ) - - /* Compute partition of previous and next anchors */ - const partition$ = watchElementSize(document.body) - .pipe( - distinctUntilKeyChanged("height"), - - /* Build index to map anchor paths to vertical offsets */ - switchMap(body => defer(() => { - let path: HTMLAnchorElement[] = [] - return of([...table].reduce((index, [anchor, target]) => { - while (path.length) { - const last = table.get(path[path.length - 1])! - if (last.tagName >= target.tagName) { - path.pop() - } else { - break - } - } - - /* If the current anchor is hidden, continue with its parent */ - let offset = target.offsetTop - while (!offset && target.parentElement) { - target = target.parentElement - offset = target.offsetTop - } - - /* Fix anchor offsets in tables - see https://bit.ly/3CUFOcn */ - let parent = target.offsetParent as HTMLElement - for (; parent; parent = parent.offsetParent as HTMLElement) - offset += parent.offsetTop - - /* Map reversed anchor path to vertical offset */ - return index.set( - [...path = [...path, anchor]].reverse(), - offset - ) - }, new Map())) - }) - .pipe( - - /* Sort index by vertical offset (see https://bit.ly/30z6QSO) */ - map(index => new Map([...index].sort(([, a], [, b]) => a - b))), - combineLatestWith(adjust$), - - /* Re-compute partition when viewport offset changes */ - switchMap(([index, adjust]) => viewport$ - .pipe( - scan(([prev, next], { offset: { y }, size }) => { - const last = y + size.height >= Math.floor(body.height) - - /* Look forward */ - while (next.length) { - const [, offset] = next[0] - if (offset - adjust < y || last) { - prev = [...prev, next.shift()!] - } else { - break - } - } - - /* Look backward */ - while (prev.length) { - const [, offset] = prev[prev.length - 1] - if (offset - adjust >= y && !last) { - next = [prev.pop()!, ...next] - } else { - break - } - } - - /* Return partition */ - return [prev, next] - }, [[], [...index]]), - distinctUntilChanged((a, b) => ( - a[0] === b[0] && - a[1] === b[1] - )) - ) - ) - ) - ) - ) - - /* Compute and return anchor list migrations */ - return partition$ - .pipe( - map(([prev, next]) => ({ - prev: prev.map(([path]) => path), - next: next.map(([path]) => path) - })), - - /* Extract anchor list migrations */ - startWith({ prev: [], next: [] }), - bufferCount(2, 1), - map(([a, b]) => { - - /* Moving down */ - if (a.prev.length < b.prev.length) { - return { - prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length), - next: [] - } - - /* Moving up */ - } else { - return { - prev: b.prev.slice(-1), - next: b.next.slice(0, b.next.length - a.next.length) - } - } - }) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Mount table of contents - * - * @param el - Table of contents element - * @param options - Options - * - * @returns Table of contents component observable - */ -export function mountTableOfContents( - el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions -): Observable> { - return defer(() => { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe(({ prev, next }) => { - - /* Look forward */ - for (const [anchor] of next) { - anchor.classList.remove("md-nav__link--passed") - anchor.classList.remove("md-nav__link--active") - } - - /* Look backward */ - for (const [index, [anchor]] of prev.entries()) { - anchor.classList.add("md-nav__link--passed") - anchor.classList.toggle( - "md-nav__link--active", - index === prev.length - 1 - ) - } - }) - - /* Set up following, if enabled */ - if (feature("toc.follow")) { - - /* Toggle smooth scrolling only for anchor clicks */ - const smooth$ = merge( - viewport$.pipe(debounceTime(1), map(() => undefined)), - viewport$.pipe(debounceTime(250), map(() => "smooth" as const)) - ) - - /* Bring active anchor into view */ // @todo: refactor - push$ - .pipe( - filter(({ prev }) => prev.length > 0), - combineLatestWith(main$.pipe(observeOn(asyncScheduler))), - withLatestFrom(smooth$) - ) - .subscribe(([[{ prev }], behavior]) => { - const [anchor] = prev[prev.length - 1] - if (anchor.offsetHeight) { - - /* Retrieve overflowing container and scroll */ - const container = getElementContainer(anchor) - if (typeof container !== "undefined") { - const offset = anchor.offsetTop - container.offsetTop - const { height } = getElementSize(container) - container.scrollTo({ - top: offset - height / 2, - behavior - }) - } - } - }) - } - - /* Set up anchor tracking, if enabled */ - if (feature("navigation.tracking")) - viewport$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("offset"), - debounceTime(250), - skip(1), - takeUntil(target$.pipe(skip(1))), - repeat({ delay: 250 }), - withLatestFrom(push$) - ) - .subscribe(([, { prev }]) => { - const url = getLocation() - - /* Set hash fragment to active anchor */ - const anchor = prev[prev.length - 1] - if (anchor && anchor.length) { - const [active] = anchor - const { hash } = new URL(active.href) - if (url.hash !== hash) { - url.hash = hash - history.replaceState({}, "", `${url}`) - } - - /* Reset anchor when at the top */ - } else { - url.hash = "" - history.replaceState({}, "", `${url}`) - } - }) - - /* Create and return component */ - return watchTableOfContents(el, { viewport$, header$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) - }) -} diff --git a/src/templates/assets/javascripts/components/top/index.ts b/src/templates/assets/javascripts/components/top/index.ts deleted file mode 100644 index 82e88b61..00000000 --- a/src/templates/assets/javascripts/components/top/index.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - Observable, - Subject, - bufferCount, - combineLatest, - distinctUntilChanged, - distinctUntilKeyChanged, - endWith, - finalize, - fromEvent, - ignoreElements, - map, - repeat, - skip, - takeUntil, - tap -} from "rxjs" - -import { Viewport } from "~/browser" - -import { Component } from "../_" -import { Header } from "../header" -import { Main } from "../main" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Back-to-top button - */ -export interface BackToTop { - hidden: boolean /* Back-to-top button is hidden */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - viewport$: Observable /* Viewport observable */ - main$: Observable
    /* Main area observable */ - target$: Observable /* Location target observable */ -} - -/** - * Mount options - */ -interface MountOptions { - viewport$: Observable /* Viewport observable */ - header$: Observable
    /* Header observable */ - main$: Observable
    /* Main area observable */ - target$: Observable /* 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 { - - /* 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> { - const push$ = new Subject() - const done$ = push$.pipe(ignoreElements(), endWith(true)) - push$.subscribe({ - - /* Handle emission */ - next({ hidden }) { - el.hidden = hidden - if (hidden) { - el.setAttribute("tabindex", "-1") - el.blur() - } else { - el.removeAttribute("tabindex") - } - }, - - /* Handle complete */ - complete() { - el.style.top = "" - el.hidden = true - el.removeAttribute("tabindex") - } - }) - - /* Watch header height */ - header$ - .pipe( - takeUntil(done$), - distinctUntilKeyChanged("height") - ) - .subscribe(({ height }) => { - el.style.top = `${height + 16}px` - }) - - /* Go back to top */ - fromEvent(el, "click") - .subscribe(ev => { - ev.preventDefault() - window.scrollTo({ top: 0 }) - }) - - /* Create and return component */ - return watchBackToTop(el, { viewport$, main$, target$ }) - .pipe( - tap(state => push$.next(state)), - finalize(() => push$.complete()), - map(state => ({ ref: el, ...state })) - ) -} diff --git a/src/templates/assets/javascripts/integrations/clipboard/index.ts b/src/templates/assets/javascripts/integrations/clipboard/index.ts deleted file mode 100644 index cf46f601..00000000 --- a/src/templates/assets/javascripts/integrations/clipboard/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 /* Alert subject */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Extract text to copy - * - * @param el - HTML element - * - * @returns Extracted text - */ -function extract(el: HTMLElement): string { - el.setAttribute("data-md-copying", "") - const copy = el.closest("[data-copy]") - const text = copy - ? copy.getAttribute("data-copy")! - : el.innerText - el.removeAttribute("data-md-copying") - return text -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up Clipboard.js integration - * - * @param options - Options - */ -export function setupClipboardJS( - { alert$ }: SetupOptions -): void { - if (ClipboardJS.isSupported()) { - new Observable(subscriber => { - new ClipboardJS("[data-clipboard-target], [data-clipboard-text]", { - text: el => ( - el.getAttribute("data-clipboard-text")! || - extract(getElement( - el.getAttribute("data-clipboard-target")! - )) - ) - }) - .on("success", ev => subscriber.next(ev)) - }) - .pipe( - tap(ev => { - const trigger = ev.trigger as HTMLElement - trigger.focus() - }), - map(() => translation("clipboard.copied")) - ) - .subscribe(alert$) - } -} diff --git a/src/templates/assets/javascripts/integrations/index.ts b/src/templates/assets/javascripts/integrations/index.ts deleted file mode 100644 index 5d91a9d5..00000000 --- a/src/templates/assets/javascripts/integrations/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./clipboard" -export * from "./instant" -export * from "./search" -export * from "./sitemap" -export * from "./version" diff --git a/src/templates/assets/javascripts/integrations/instant/.eslintrc b/src/templates/assets/javascripts/integrations/instant/.eslintrc deleted file mode 100644 index 5adf108a..00000000 --- a/src/templates/assets/javascripts/integrations/instant/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-self-assign": "off", - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/instant/index.ts b/src/templates/assets/javascripts/integrations/instant/index.ts deleted file mode 100644 index d321b751..00000000 --- a/src/templates/assets/javascripts/integrations/instant/index.ts +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - Subject, - bufferCount, - catchError, - concat, - debounceTime, - distinctUntilKeyChanged, - endWith, - filter, - fromEvent, - ignoreElements, - map, - of, - sample, - share, - skip, - startWith, - switchMap, - take, - withLatestFrom -} from "rxjs" - -import { configuration, feature } from "~/_" -import { - Viewport, - getElement, - getElements, - getLocation, - getOptionalElement, - request, - setLocation, - setLocationHash -} from "~/browser" -import { getComponentElement } from "~/components" - -import { fetchSitemap } from "../sitemap" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Setup options - */ -interface SetupOptions { - location$: Subject // Location subject - viewport$: Observable // Viewport observable - progress$: Subject // Progress suject -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Create a map of head elements for lookup and replacement - * - * @param head - Document head - * - * @returns Element map - */ -function lookup(head: HTMLHeadElement): Map { - - // @todo When resolving URLs, we must make sure to use the correct base for - // resolution. The next time we refactor instant loading, we should use the - // location subject as a source, which is also used for anchor links tracking, - // but for now we just rely on canonical. - const canonical = getElement("[rel=canonical]", head) - canonical.href = canonical.href.replace("//localhost:", "//127.0.0.1") - - // Create tag map and index elements in head - const tags = new Map() - for (const el of getElements(":scope > *", head)) { - let html = el.outerHTML - - // If the current element is a style sheet or script, we must resolve the - // URL relative to the current location and make it absolute, so it's easy - // to deduplicate it later on by comparing the outer HTML of tags. We must - // keep identical style sheets and scripts without replacing them. - for (const key of ["href", "src"]) { - const value = el.getAttribute(key)! - if (value === null) - continue - - // Resolve URL relative to current location - const url = new URL(value, canonical.href) - const ref = el.cloneNode() as HTMLElement - - // Set resolved URL and retrieve HTML for deduplication - ref.setAttribute(key, `${url}`) - html = ref.outerHTML - break - } - - // Index element in tag map - tags.set(html, el) - } - - // Return tag map - return tags -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up instant navigation - * - * This is a heavily orchestrated operation - see inline comments to learn how - * this works with Material for MkDocs, and how you can hook into it. - * - * @param options - Options - * - * @returns Document observable - */ -export function setupInstantNavigation( - { location$, viewport$, progress$ }: SetupOptions -): Observable { - const config = configuration() - if (location.protocol === "file:") - return EMPTY - - // Load sitemap immediately, so we have it available when the user initiates - // the first instant navigation request, and canonicalize URLs to the current - // base URL. The base URL will remain stable in between loads, as it's only - // read at the first initialization of the application. - const sitemap$ = fetchSitemap() - .pipe( - map(paths => paths.map(path => `${new URL(path, config.base)}`)) - ) - - // Intercept inter-site navigation - to keep the number of event listeners - // low we use the fact that uncaptured events bubble up to the body. This also - // has the nice property that we don't need to detach and then again attach - // event listeners when instant navigation occurs. - const instant$ = fromEvent(document.body, "click") - .pipe( - withLatestFrom(sitemap$), - switchMap(([ev, sitemap]) => { - if (!(ev.target instanceof Element)) - return EMPTY - - // Skip, as target is not within a link - clicks on non-link elements - // are also captured, which we need to exclude from processing - const el = ev.target.closest("a") - if (el === null) - return EMPTY - - // Skip, as link opens in new window - we now know we have captured a - // click on a link, but the link either has a `target` property defined, - // or the user pressed the `meta` or `ctrl` key to open it in a new - // window. Thus, we need to filter those events, too. - if (el.target || ev.metaKey || ev.ctrlKey) - return EMPTY - - // Next, we must check if the URL is relevant for us, i.e., if it's an - // internal link to a page that is managed by MkDocs. Only then we can - // be sure that the structure of the page to be loaded adheres to the - // current document structure and can subsequently be injected into it - // without doing a full reload. For this reason, we must canonicalize - // the URL by removing all search parameters and hash fragments. - const url = new URL(el.href) - url.search = url.hash = "" - - // Skip, if URL is not included in the sitemap - this could be the case - // when linking between versions or languages, or to another page that - // the author included as part of the build, but that is not managed by - // MkDocs. In that case we must not continue with instant navigation. - if (!sitemap.includes(`${url}`)) - return EMPTY - - // We now know that we have a link to an internal page, so we prevent - // the browser from navigation and emit the URL for instant navigation. - // Note that this also includes anchor links, which means we need to - // implement anchor positioning ourselves. The reason for this is that - // if we wouldn't manage anchor links as well, scroll restoration will - // not work correctly (e.g. following an anchor link and scrolling). - ev.preventDefault() - return of(new URL(el.href)) - }), - share() - ) - - // Before fetching for the first time, resolve the absolute favicon position, - // as the browser will try to fetch the icon immediately - instant$.pipe(take(1)) - .subscribe(() => { - const favicon = getOptionalElement("link[rel=icon]") - if (typeof favicon !== "undefined") - favicon.href = favicon.href - }) - - // Enable scroll restoration before window unloads - this is essential to - // ensure that full reloads (F5) restore the viewport offset correctly. If - // only popstate events wouldn't reset the scroll position prior to their - // emission, we could just reset this in popstate. Meh. - fromEvent(window, "beforeunload") - .subscribe(() => { - history.scrollRestoration = "auto" - }) - - // When an instant navigation event occurs, disable scroll restoration, since - // we must normalize and synchronize the behavior across all browsers. For - // instance, when the user clicks the back or forward button, the browser - // would immediately jump to the position of the previous document. - instant$.pipe(withLatestFrom(viewport$)) - .subscribe(([url, { offset }]) => { - history.scrollRestoration = "manual" - - // While it would be better UX to defer the history state change until the - // document was fully fetched and parsed, we must schedule it here, since - // popstate events are emitted when history state changes happen. Moreover - // we need to back up the current viewport offset, so we can restore it - // when popstate events occur, e.g., when the browser's back and forward - // buttons are used for navigation. - history.replaceState(offset, "") - history.pushState(null, "", url) - }) - - // Emit URL that should be fetched via instant navigation on location subject, - // which was passed into this function. Instant navigation can be intercepted - // by other parts of the application, which can synchronously back up or - // restore state before instant navigation happens. - instant$.subscribe(location$) - - // Fetch document - when fetching, we could use `responseType: document`, but - // since all MkDocs links are relative, we need to make sure that the current - // location matches the document we just loaded. Otherwise any relative links - // in the document might use the old location. If the request fails for some - // reason, we fall back to regular navigation and set the location explicitly, - // which will force-load the page. Furthermore, we must pre-warm the buffer - // for the duplicate check, or the first click on an anchor link will also - // trigger an instant navigation event, which doesn't make sense. - const response$ = location$ - .pipe( - startWith(getLocation()), - distinctUntilKeyChanged("pathname"), - skip(1), - switchMap(url => request(url, { progress$ }) - .pipe( - catchError(() => { - setLocation(url, true) - return EMPTY - }) - ) - ) - ) - - // Initialize the DOM parser, parse the returned HTML, and replace selected - // components before handing control down to the application - const dom = new DOMParser() - const document$ = response$ - .pipe( - switchMap(res => res.text()), - switchMap(res => { - const next = dom.parseFromString(res, "text/html") - for (const selector of [ - "[data-md-component=announce]", - "[data-md-component=container]", - "[data-md-component=header-topic]", - "[data-md-component=outdated]", - "[data-md-component=logo]", - "[data-md-component=skip]", - ...feature("navigation.tabs.sticky") - ? ["[data-md-component=tabs]"] - : [] - ]) { - const source = getOptionalElement(selector) - const target = getOptionalElement(selector, next) - if ( - typeof source !== "undefined" && - typeof target !== "undefined" - ) { - source.replaceWith(target) - } - } - - // Update meta tags - const source = lookup(document.head) - const target = lookup(next.head) - for (const [html, el] of target) { - - // Hack: skip stylesheets and scripts until we manage to replace them - // entirely in order to omit flashes of white content @todo refactor - if ( - el.getAttribute("rel") === "stylesheet" || - el.hasAttribute("src") - ) - continue - - if (source.has(html)) { - source.delete(html) - } else { - document.head.appendChild(el) - } - } - - // Remove meta tags that are not present in the new document - for (const el of source.values()) - - // Hack: skip stylesheets and scripts until we manage to replace them - // entirely in order to omit flashes of white content @todo refactor - if ( - el.getAttribute("rel") === "stylesheet" || - el.hasAttribute("src") - ) - continue - else - el.remove() - - // After components and meta tags were replaced, re-evaluate scripts - // that were provided by the author as part of Markdown files - const container = getComponentElement("container") - return concat(getElements("script", container)) - .pipe( - switchMap(el => { - const script = next.createElement("script") - if (el.src) { - for (const name of el.getAttributeNames()) - script.setAttribute(name, el.getAttribute(name)!) - el.replaceWith(script) - - // Complete when script is loaded - return new Observable(observer => { - script.onload = () => observer.complete() - }) - - // Complete immediately - } else { - script.textContent = el.textContent - el.replaceWith(script) - return EMPTY - } - }), - ignoreElements(), - endWith(next) - ) - }), - share() - ) - - // Intercept popstate events, e.g. when using the browser's back and forward - // buttons, and emit new location for fetching and parsing - const popstate$ = fromEvent(window, "popstate") - popstate$.pipe(map(getLocation)) - .subscribe(location$) - - // Intercept clicks on anchor links, and scroll document into position - as - // we disabled scroll restoration, we need to do this manually here - location$ - .pipe( - startWith(getLocation()), - bufferCount(2, 1), - filter(([prev, next]) => ( - prev.pathname === next.pathname && - prev.hash !== next.hash - )), - map(([, next]) => next) - ) - .subscribe(url => { - if (history.state !== null || !url.hash) { - window.scrollTo(0, history.state?.y ?? 0) - } else { - history.scrollRestoration = "auto" - setLocationHash(url.hash) - history.scrollRestoration = "manual" - } - }) - - // Intercept clicks on the same anchor link - we must use a distinct pipeline - // for this, or we'd end up in a loop, setting the hash again and again - location$ - .pipe( - sample(instant$), - startWith(getLocation()), - bufferCount(2, 1), - filter(([prev, next]) => ( - prev.pathname === next.pathname && - prev.hash === next.hash - )), - map(([, next]) => next) - ) - .subscribe(url => { - history.scrollRestoration = "auto" - setLocationHash(url.hash) - history.scrollRestoration = "manual" - - // Hack: we need to make sure that we don't end up with multiple history - // entries for the same anchor link, so we just remove the last entry - history.back() - }) - - // After parsing the document, check if the current history entry has a state. - // This may happen when users press the back or forward button to visit a page - // that was already seen. If there's no state, it means a new page was visited - // and we should scroll to the top, unless an anchor is given. - document$.pipe(withLatestFrom(location$)) - .subscribe(([, url]) => { - if (history.state !== null || !url.hash) { - window.scrollTo(0, history.state?.y ?? 0) - } else { - setLocationHash(url.hash) - } - }) - - // If the current history is not empty, register an event listener updating - // the current history state whenever the scroll position changes. This must - // be debounced and cannot be done in popstate, as popstate has already - // removed the entry from the history. - viewport$ - .pipe( - distinctUntilKeyChanged("offset"), - debounceTime(100) - ) - .subscribe(({ offset }) => { - history.replaceState(offset, "") - }) - - // Return document - return document$ -} diff --git a/src/templates/assets/javascripts/integrations/search/_/index.ts b/src/templates/assets/javascripts/integrations/search/_/index.ts deleted file mode 100644 index 0e217fa4..00000000 --- a/src/templates/assets/javascripts/integrations/search/_/index.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - SearchDocument, - SearchIndex, - SearchOptions, - setupSearchDocumentMap -} from "../config" -import { - Position, - PositionTable, - highlight, - highlightAll, - tokenize -} from "../internal" -import { - SearchQueryTerms, - getSearchQueryTerms, - parseSearchQuery, - segment, - transformSearchQuery -} from "../query" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search item - */ -export interface SearchItem - extends SearchDocument -{ - score: number /* Score (relevance) */ - terms: SearchQueryTerms /* Search query terms */ -} - -/** - * Search result - */ -export interface SearchResult { - items: SearchItem[][] /* Search items */ - suggest?: string[] /* Search suggestions */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create field extractor factory - * - * @param table - Position table map - * - * @returns Extractor factory - */ -function extractor(table: Map) { - return (name: keyof SearchDocument) => { - return (doc: SearchDocument) => { - if (typeof doc[name] === "undefined") - return undefined - - /* Compute identifier and initialize table */ - const id = [doc.location, name].join(":") - table.set(id, lunr.tokenizer.table = []) - - /* Return field value */ - return doc[name] - } - } -} - -/** - * Compute the difference of two lists of strings - * - * @param a - 1st list of strings - * @param b - 2nd list of strings - * - * @returns Difference - */ -function difference(a: string[], b: string[]): string[] { - const [x, y] = [new Set(a), new Set(b)] - return [ - ...new Set([...x].filter(value => !y.has(value))) - ] -} - -/* ---------------------------------------------------------------------------- - * Class - * ------------------------------------------------------------------------- */ - -/** - * Search index - */ -export class Search { - - /** - * Search document map - */ - protected map: Map - - /** - * Search options - */ - protected options: SearchOptions - - /** - * The underlying Lunr.js search index - */ - protected index: lunr.Index - - /** - * Internal position table map - */ - protected table: Map - - /** - * Create the search integration - * - * @param data - Search index - */ - public constructor({ config, docs, options }: SearchIndex) { - const field = extractor(this.table = new Map()) - - /* Set up document map and options */ - this.map = setupSearchDocumentMap(docs) - this.options = options - - /* Set up document index */ - this.index = lunr(function () { - this.metadataWhitelist = ["position"] - this.b(0) - - /* Set up (multi-)language support */ - if (config.lang.length === 1 && config.lang[0] !== "en") { - // @ts-expect-error - namespace indexing not supported - this.use(lunr[config.lang[0]]) - } else if (config.lang.length > 1) { - this.use(lunr.multiLanguage(...config.lang)) - } - - /* Set up custom tokenizer (must be after language setup) */ - this.tokenizer = tokenize as typeof lunr.tokenizer - lunr.tokenizer.separator = new RegExp(config.separator) - - /* Set up custom segmenter, if loaded */ - lunr.segmenter = "TinySegmenter" in lunr - ? new lunr.TinySegmenter() - : undefined - - /* Compute functions to be removed from the pipeline */ - const fns = difference([ - "trimmer", "stopWordFilter", "stemmer" - ], config.pipeline) - - /* Remove functions from the pipeline for registered languages */ - for (const lang of config.lang.map(language => ( - // @ts-expect-error - namespace indexing not supported - language === "en" ? lunr : lunr[language] - ))) - for (const fn of fns) { - this.pipeline.remove(lang[fn]) - this.searchPipeline.remove(lang[fn]) - } - - /* Set up index reference */ - this.ref("location") - - /* Set up index fields */ - this.field("title", { boost: 1e3, extractor: field("title") }) - this.field("text", { boost: 1e0, extractor: field("text") }) - this.field("tags", { boost: 1e6, extractor: field("tags") }) - - /* Add documents to index */ - for (const doc of docs) - this.add(doc, { boost: doc.boost }) - }) - } - - /** - * Search for matching documents - * - * @param query - Search query - * - * @returns Search result - */ - public search(query: string): SearchResult { - - // Experimental Chinese segmentation - query = query.replace(/\p{sc=Han}+/gu, value => { - return [...segment(value, this.index.invertedIndex)] - .join("* ") - }) - - // @todo: move segmenter (above) into transformSearchQuery - query = transformSearchQuery(query) - if (!query) - return { items: [] } - - /* Parse query to extract clauses for analysis */ - const clauses = parseSearchQuery(query) - .filter(clause => ( - clause.presence !== lunr.Query.presence.PROHIBITED - )) - - /* Perform search and post-process results */ - const groups = this.index.search(query) - - /* Apply post-query boosts based on title and search query terms */ - .reduce((item, { ref, score, matchData }) => { - let doc = this.map.get(ref) - if (typeof doc !== "undefined") { - - /* Shallow copy document */ - doc = { ...doc } - if (doc.tags) - doc.tags = [...doc.tags] - - /* Compute and analyze search query terms */ - const terms = getSearchQueryTerms( - clauses, - Object.keys(matchData.metadata) - ) - - /* Highlight matches in fields */ - for (const field of this.index.fields) { - if (typeof doc[field] === "undefined") - continue - - /* Collect positions from matches */ - const positions: Position[] = [] - for (const match of Object.values(matchData.metadata)) - if (typeof match[field] !== "undefined") - positions.push(...match[field].position) - - /* Skip highlighting, if no positions were collected */ - if (!positions.length) - continue - - /* Load table and determine highlighting method */ - const table = this.table.get([doc.location, field].join(":"))! - const fn = Array.isArray(doc[field]) - ? highlightAll - : highlight - - // @ts-expect-error - stop moaning, TypeScript! - doc[field] = fn(doc[field], table, positions, field !== "text") - } - - /* Highlight title and text and apply post-query boosts */ - const boost = +!doc.parent + - Object.values(terms) - .filter(t => t).length / - Object.keys(terms).length - - /* Append item */ - item.push({ - ...doc, - score: score * (1 + boost ** 2), - terms - }) - } - return item - }, []) - - /* Sort search results again after applying boosts */ - .sort((a, b) => b.score - a.score) - - /* Group search results by article */ - .reduce((items, result) => { - const doc = this.map.get(result.location) - if (typeof doc !== "undefined") { - const ref = doc.parent - ? doc.parent.location - : doc.location - items.set(ref, [...items.get(ref) || [], result]) - } - return items - }, new Map()) - - /* Ensure that every item set has an article */ - for (const [ref, items] of groups) - if (!items.find(item => item.location === ref)) { - const doc = this.map.get(ref)! - items.push({ ...doc, score: 0, terms: {} }) - } - - /* Generate search suggestions, if desired */ - let suggest: string[] | undefined - if (this.options.suggest) { - const titles = this.index.query(builder => { - for (const clause of clauses) - builder.term(clause.term, { - fields: ["title"], - presence: lunr.Query.presence.REQUIRED, - wildcard: lunr.Query.wildcard.TRAILING - }) - }) - - /* Retrieve suggestions for best match */ - suggest = titles.length - ? Object.keys(titles[0].matchData.metadata) - : [] - } - - /* Return search result */ - return { - items: [...groups.values()], - ...typeof suggest !== "undefined" && { suggest } - } - } -} diff --git a/src/templates/assets/javascripts/integrations/search/config/index.ts b/src/templates/assets/javascripts/integrations/search/config/index.ts deleted file mode 100644 index 3d88d1c6..00000000 --- a/src/templates/assets/javascripts/integrations/search/config/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search configuration - */ -export interface SearchConfig { - lang: string[] /* Search languages */ - separator: string /* Search separator */ - pipeline: SearchPipelineFn[] /* Search pipeline */ -} - -/** - * Search document - */ -export interface SearchDocument { - location: string /* Document location */ - title: string /* Document title */ - text: string /* Document text */ - tags?: string[] /* Document tags */ - boost?: number /* Document boost */ - parent?: SearchDocument /* Document parent */ -} - -/** - * Search options - */ -export interface SearchOptions { - suggest: boolean /* Search suggestions */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search index - */ -export interface SearchIndex { - config: SearchConfig /* Search configuration */ - docs: SearchDocument[] /* Search documents */ - options: SearchOptions /* Search options */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Search pipeline function - */ -type SearchPipelineFn = - | "trimmer" /* Trimmer */ - | "stopWordFilter" /* Stop word filter */ - | "stemmer" /* Stemmer */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a search document map - * - * This function creates a mapping of URLs (including anchors) to the actual - * articles and sections. It relies on the invariant that the search index is - * ordered with the main article appearing before all sections with anchors. - * If this is not the case, the logic music be changed. - * - * @param docs - Search documents - * - * @returns Search document map - */ -export function setupSearchDocumentMap( - docs: SearchDocument[] -): Map { - const map = new Map() - for (const doc of docs) { - const [path] = doc.location.split("#") - - /* Add document article */ - const article = map.get(path) - if (typeof article === "undefined") { - map.set(path, doc) - - /* Add document section */ - } else { - map.set(doc.location, doc) - doc.parent = article - } - } - - /* Return search document map */ - return map -} diff --git a/src/templates/assets/javascripts/integrations/search/highlighter/index.ts b/src/templates/assets/javascripts/integrations/search/highlighter/index.ts deleted file mode 100644 index 0fcbb19e..00000000 --- a/src/templates/assets/javascripts/integrations/search/highlighter/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import escapeHTML from "escape-html" - -import { SearchConfig } from "../config" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search highlight function - * - * @param value - Value - * - * @returns Highlighted value - */ -export type SearchHighlightFn = (value: string) => string - -/** - * Search highlight factory function - * - * @param query - Query value - * - * @returns Search highlight function - */ -export type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create a search highlighter - * - * @param config - Search configuration - * - * @returns Search highlight factory function - */ -export function setupSearchHighlighter( - config: SearchConfig -): SearchHighlightFactoryFn { - // Hack: temporarily remove pure lookaheads and lookbehinds - const regex = config.separator.split("|").map(term => { - const temp = term.replace(/(\(\?[!=<][^)]+\))/g, "") - return temp.length === 0 ? "�" : term - }) - .join("|") - - const separator = new RegExp(regex, "img") - const highlight = (_: unknown, data: string, term: string) => { - return `${data}${term}` - } - - /* Return factory function */ - return (query: string) => { - query = query - .replace(/[\s*+\-:~^]+/g, " ") - .trim() - - /* Create search term match expression */ - const match = new RegExp(`(^|${config.separator}|)(${ - query - .replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") - .replace(separator, "|") - })`, "img") - - /* Highlight string value */ - return value => escapeHTML(value) - .replace(match, highlight) - .replace(/<\/mark>(\s+)]*>/img, "$1") - } -} diff --git a/src/templates/assets/javascripts/integrations/search/index.ts b/src/templates/assets/javascripts/integrations/search/index.ts deleted file mode 100644 index 94c010bb..00000000 --- a/src/templates/assets/javascripts/integrations/search/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./config" -export * from "./highlighter" -export * from "./query" -export * from "./worker" diff --git a/src/templates/assets/javascripts/integrations/search/internal/.eslintrc b/src/templates/assets/javascripts/integrations/search/internal/.eslintrc deleted file mode 100644 index 9368ceb6..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-fallthrough": "off", - "no-underscore-dangle": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/_/index.ts b/src/templates/assets/javascripts/integrations/search/internal/_/index.ts deleted file mode 100644 index ae8f6104..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/_/index.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param start - Start offset - * @param end - End offset - */ -type VisitorFn = ( - start: number, end: number -) => void - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string using the given separator - * - * @param input - Input value - * @param separator - Separator - * @param fn - Visitor function - */ -export function split( - input: string, separator: RegExp, fn: VisitorFn -): void { - separator = new RegExp(separator, "g") - - /* Split string using separator */ - let match: RegExpExecArray | null - let index = 0 - do { - match = separator.exec(input) - - /* Emit non-empty range */ - const until = match?.index ?? input.length - if (index < until) - fn(index, until) - - /* Update last index */ - if (match) { - const [term] = match - index = match.index + term.length - - /* Support zero-length lookaheads */ - if (term.length === 0) - separator.lastIndex = match.index + 1 - } - } while (match) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts b/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts deleted file mode 100644 index 2a98b9e1..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/extract/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Extraction type - * - * This type defines the possible values that are encoded into the first two - * bits of a section that is part of the blocks of a tokenization table. There - * are three types of interest: HTML opening and closing tags, as well as the - * actual text content we need to extract for indexing. - */ -export const enum Extract { - TAG_OPEN = 0, /* HTML opening tag */ - TEXT = 1, /* Text content */ - TAG_CLOSE = 2 /* HTML closing tag */ -} - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param block - Block index - * @param type - Extraction type - * @param start - Start offset - * @param end - End offset - */ -type VisitorFn = ( - block: number, type: Extract, start: number, end: number -) => void - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string into markup and text sections - * - * This function scans a string and divides it up into sections of markup and - * text. For each section, it invokes the given visitor function with the block - * index, extraction type, as well as start and end offsets. Using a visitor - * function (= streaming data) is ideal for minimizing pressure on the GC. - * - * @param input - Input value - * @param fn - Visitor function - */ -export function extract( - input: string, fn: VisitorFn -): void { - - let block = 0 /* Current block */ - let start = 0 /* Current start offset */ - let end = 0 /* Current end offset */ - - /* Split string into sections */ - for (let stack = 0; end < input.length; end++) { - - /* Opening tag after non-empty section */ - if (input.charAt(end) === "<" && end > start) { - fn(block, Extract.TEXT, start, start = end) - - /* Closing tag */ - } else if (input.charAt(end) === ">") { - if (input.charAt(start + 1) === "/") { - if (--stack === 0) - fn(block++, Extract.TAG_CLOSE, start, end + 1) - - /* Tag is not self-closing */ - } else if (input.charAt(end - 1) !== "/") { - if (stack++ === 0) - fn(block, Extract.TAG_OPEN, start, end + 1) - } - - /* New section */ - start = end + 1 - } - } - - /* Add trailing section */ - if (end > start) - fn(block, Extract.TEXT, start, end) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts b/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts deleted file mode 100644 index 7cc3bf1a..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/highlight/index.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Position table - */ -export type PositionTable = number[][] - -/** - * Position - */ -export type Position = number - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Highlight all occurrences in a string - * - * This function receives a field's value (e.g. like `title` or `text`), it's - * position table that was generated during indexing, and the positions found - * when executing the query. It then highlights all occurrences, and returns - * their concatenation. In case of multiple blocks, two are returned. - * - * @param input - Input value - * @param table - Table for indexing - * @param positions - Occurrences - * @param full - Full results - * - * @returns Highlighted string value - */ -export function highlight( - input: string, table: PositionTable, positions: Position[], full = false -): string { - return highlightAll([input], table, positions, full).pop()! -} - -/** - * Highlight all occurrences in a set of strings - * - * @param inputs - Input values - * @param table - Table for indexing - * @param positions - Occurrences - * @param full - Full results - * - * @returns Highlighted string values - */ -export function highlightAll( - inputs: string[], table: PositionTable, positions: Position[], full = false -): string[] { - - /* Map blocks to input values */ - const mapping = [0] - for (let t = 1; t < table.length; t++) { - const prev = table[t - 1] - const next = table[t] - - /* Check if table points to new block */ - const p = prev[prev.length - 1] >>> 2 & 0x3FF - const q = next[0] >>> 12 - - /* Add block to mapping */ - mapping.push(+(p > q) + mapping[mapping.length - 1]) - } - - /* Highlight strings one after another */ - return inputs.map((input, i) => { - let cursor = 0 - - /* Map occurrences to blocks */ - const blocks = new Map() - for (const p of positions.sort((a, b) => a - b)) { - const index = p & 0xFFFFF - const block = p >>> 20 - if (mapping[block] !== i) - continue - - /* Ensure presence of block group */ - let group = blocks.get(block) - if (typeof group === "undefined") - blocks.set(block, group = []) - - /* Add index to group */ - group.push(index) - } - - /* Just return string, if no occurrences */ - if (blocks.size === 0) - return input - - /* Compute slices */ - const slices: string[] = [] - for (const [block, indexes] of blocks) { - const t = table[block] - - /* Extract positions and length */ - const start = t[0] >>> 12 - const end = t[t.length - 1] >>> 12 - const length = t[t.length - 1] >>> 2 & 0x3FF - - /* Add prefix, if full results are desired */ - if (full && start > cursor) - slices.push(input.slice(cursor, start)) - - /* Extract and highlight slice */ - let slice = input.slice(start, end + length) - for (const j of indexes.sort((a, b) => b - a)) { - - /* Retrieve offset and length of match */ - const p = (t[j] >>> 12) - start - const q = (t[j] >>> 2 & 0x3FF) + p - - /* Wrap occurrence */ - slice = [ - slice.slice(0, p), - "", - slice.slice(p, q), - "", - slice.slice(q) - ].join("") - } - - /* Update cursor */ - cursor = end + length - - /* Append slice and abort if we have two */ - if (slices.push(slice) === 2) - break - } - - /* Add suffix, if full results are desired */ - if (full && cursor < input.length) - slices.push(input.slice(cursor)) - - /* Return highlighted slices */ - return slices.join("") - }) -} diff --git a/src/templates/assets/javascripts/integrations/search/internal/index.ts b/src/templates/assets/javascripts/integrations/search/internal/index.ts deleted file mode 100644 index c752329e..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./extract" -export * from "./highlight" -export * from "./tokenize" diff --git a/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts b/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts deleted file mode 100644 index f5089bc9..00000000 --- a/src/templates/assets/javascripts/integrations/search/internal/tokenize/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { split } from "../_" -import { - Extract, - extract -} from "../extract" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Split a string or set of strings into tokens - * - * This tokenizer supersedes the default tokenizer that is provided by Lunr.js, - * as it is aware of HTML tags and allows for multi-character splitting. - * - * It takes the given inputs, splits each of them into markup and text sections, - * tokenizes and segments (if necessary) each of them, and then indexes them in - * a table by using a compact bit representation. Bitwise techniques are used - * to write and read from the table during indexing and querying. - * - * @see https://bit.ly/3W3Xw4J - Search: better, faster, smaller - * - * @param input - Input value(s) - * - * @returns Tokens - */ -export function tokenize( - input?: string | string[] -): lunr.Token[] { - const tokens: lunr.Token[] = [] - if (typeof input === "undefined") - return tokens - - /* Tokenize strings one after another */ - const inputs = Array.isArray(input) ? input : [input] - for (let i = 0; i < inputs.length; i++) { - const table = lunr.tokenizer.table - const total = table.length - - /* Split string into sections and tokenize content blocks */ - extract(inputs[i], (block, type, start, end) => { - table[block += total] ||= [] - switch (type) { - - /* Handle markup */ - case Extract.TAG_OPEN: - case Extract.TAG_CLOSE: - table[block].push( - start << 12 | - end - start << 2 | - type - ) - break - - /* Handle text content */ - case Extract.TEXT: - const section = inputs[i].slice(start, end) - split(section, lunr.tokenizer.separator, (index, until) => { - - /** - * Apply segmenter after tokenization. Note that the segmenter will - * also split words at word boundaries, which is not what we want, - * so we need to check if we can somehow mitigate this behavior. - */ - if (typeof lunr.segmenter !== "undefined") { - const subsection = section.slice(index, until) - if (/^[MHIK]$/.test(lunr.segmenter.ctype_(subsection))) { - const segments = lunr.segmenter.segment(subsection) - for (let s = 0, l = 0; s < segments.length; s++) { - - /* Add block to section */ - table[block] ||= [] - table[block].push( - start + index + l << 12 | - segments[s].length << 2 | - type - ) - - /* Add token with position */ - tokens.push(new lunr.Token( - segments[s].toLowerCase(), { - position: block << 20 | table[block].length - 1 - } - )) - - /* Keep track of length */ - l += segments[s].length - } - return - } - } - - /* Add block to section */ - table[block].push( - start + index << 12 | - until - index << 2 | - type - ) - - /* Add token with position */ - tokens.push(new lunr.Token( - section.slice(index, until).toLowerCase(), { - position: block << 20 | table[block].length - 1 - } - )) - }) - } - }) - } - - /* Return tokens */ - return tokens -} diff --git a/src/templates/assets/javascripts/integrations/search/query/.eslintrc b/src/templates/assets/javascripts/integrations/search/query/.eslintrc deleted file mode 100644 index 3031c7e3..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-control-regex": "off", - "@typescript-eslint/no-explicit-any": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/query/_/index.ts b/src/templates/assets/javascripts/integrations/search/query/_/index.ts deleted file mode 100644 index 14482e43..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/_/index.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { split } from "../../internal" -import { transform } from "../transform" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search query clause - */ -export interface SearchQueryClause { - presence: lunr.Query.presence /* Clause presence */ - term: string /* Clause term */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Search query terms - */ -export type SearchQueryTerms = Record - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Transform search query - * - * This function lexes the given search query and applies the transformation - * function to each term, preserving markup like `+` and `-` modifiers. - * - * @param query - Search query - * - * @returns Search query - */ -export function transformSearchQuery( - query: string -): string { - - /* Split query terms with tokenizer */ - return transform(query, part => { - const terms: string[] = [] - - /* Initialize lexer and analyze part */ - const lexer = new lunr.QueryLexer(part) - lexer.run() - - /* Extract and tokenize term from lexeme */ - for (const { type, str: term, start, end } of lexer.lexemes) - switch (type) { - - /* Hack: remove colon - see https://bit.ly/3wD3T3I */ - case "FIELD": - if (!["title", "text", "tags"].includes(term)) - part = [ - part.slice(0, end), - " ", - part.slice(end + 1) - ].join("") - break - - /* Tokenize term */ - case "TERM": - split(term, lunr.tokenizer.separator, (...range) => { - terms.push([ - part.slice(0, start), - term.slice(...range), - part.slice(end) - ].join("")) - }) - } - - /* Return terms */ - return terms - }) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Parse a search query for analysis - * - * Lunr.js itself has a bug where it doesn't detect or remove wildcards for - * query clauses, so we must do this here. - * - * @see https://bit.ly/3DpTGtz - GitHub issue - * - * @param value - Query value - * - * @returns Search query clauses - */ -export function parseSearchQuery( - value: string -): SearchQueryClause[] { - const query = new lunr.Query(["title", "text", "tags"]) - const parser = new lunr.QueryParser(value, query) - - /* Parse Search query */ - parser.parse() - for (const clause of query.clauses) { - clause.usePipeline = true - - /* Handle leading wildcard */ - if (clause.term.startsWith("*")) { - clause.wildcard = lunr.Query.wildcard.LEADING - clause.term = clause.term.slice(1) - } - - /* Handle trailing wildcard */ - if (clause.term.endsWith("*")) { - clause.wildcard = lunr.Query.wildcard.TRAILING - clause.term = clause.term.slice(0, -1) - } - } - - /* Return query clauses */ - return query.clauses -} - -/** - * Analyze the search query clauses in regard to the search terms found - * - * @param query - Search query clauses - * @param terms - Search terms - * - * @returns Search query terms - */ -export function getSearchQueryTerms( - query: SearchQueryClause[], terms: string[] -): SearchQueryTerms { - const clauses = new Set(query) - - /* Match query clauses against terms */ - const result: SearchQueryTerms = {} - for (let t = 0; t < terms.length; t++) - for (const clause of clauses) - if (terms[t].startsWith(clause.term)) { - result[clause.term] = true - clauses.delete(clause) - } - - /* Annotate unmatched non-stopword query clauses */ - for (const clause of clauses) - if (lunr.stopWordFilter?.(clause.term)) - result[clause.term] = false - - /* Return query terms */ - return result -} diff --git a/src/templates/assets/javascripts/integrations/search/query/index.ts b/src/templates/assets/javascripts/integrations/search/query/index.ts deleted file mode 100644 index 763e2fd4..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./segment" -export * from "./transform" diff --git a/src/templates/assets/javascripts/integrations/search/query/segment/index.ts b/src/templates/assets/javascripts/integrations/search/query/segment/index.ts deleted file mode 100644 index b96796f4..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/segment/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Segment a search query using the inverted index - * - * This function implements a clever approach to text segmentation for Asian - * languages, as it used the information already available in the search index. - * The idea is to greedily segment the search query based on the tokens that are - * already part of the index, as described in the linked issue. - * - * @see https://bit.ly/3lwjrk7 - GitHub issue - * - * @param query - Query value - * @param index - Inverted index - * - * @returns Segmented query value - */ -export function segment( - query: string, index: object -): Iterable { - const segments = new Set() - - /* Segment search query */ - const wordcuts = new Uint16Array(query.length) - for (let i = 0; i < query.length; i++) - for (let j = i + 1; j < query.length; j++) { - const value = query.slice(i, j) - if (value in index) - wordcuts[i] = j - i - } - - /* Compute longest matches with minimum overlap */ - const stack = [0] - for (let s = stack.length; s > 0;) { - const p = stack[--s] - for (let q = 1; q < wordcuts[p]; q++) - if (wordcuts[p + q] > wordcuts[p] - q) { - segments.add(query.slice(p, p + q)) - stack[s++] = p + q - } - - /* Continue at end of query string */ - const q = p + wordcuts[p] - if (wordcuts[q] && q < query.length - 1) - stack[s++] = q - - /* Add current segment */ - segments.add(query.slice(p, q)) - } - - // @todo fix this case in the code block above, this is a hotfix - if (segments.has("")) - return new Set([query]) - - /* Return segmented query value */ - return segments -} diff --git a/src/templates/assets/javascripts/integrations/search/query/transform/index.ts b/src/templates/assets/javascripts/integrations/search/query/transform/index.ts deleted file mode 100644 index 41497786..00000000 --- a/src/templates/assets/javascripts/integrations/search/query/transform/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Visitor function - * - * @param value - String value - * - * @returns String term(s) - */ -type VisitorFn = ( - value: string -) => string | string[] - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Default transformation function - * - * 1. Trim excess whitespace from left and right. - * - * 2. Search for parts in quotation marks and prepend a `+` modifier to denote - * that the resulting document must contain all parts, converting the query - * to an `AND` query (as opposed to the default `OR` behavior). While users - * may expect parts enclosed in quotation marks to map to span queries, i.e. - * for which order is important, Lunr.js doesn't support them, so the best - * we can do is to convert the parts to an `AND` query. - * - * 3. Replace control characters which are not located at the beginning of the - * query or preceded by white space, or are not followed by a non-whitespace - * character or are at the end of the query string. Furthermore, filter - * unmatched quotation marks. - * - * 4. Split the query string at whitespace, then pass each part to the visitor - * function for tokenization, and append a wildcard to every resulting term - * that is not explicitly marked with a `+`, `-`, `~` or `^` modifier, since - * it ensures consistent and stable ranking when multiple terms are entered. - * Also, if a fuzzy or boost modifier are given, but no numeric value has - * been entered, default to 1 to not induce a query error. - * - * @param query - Query value - * @param fn - Visitor function - * - * @returns Transformed query value - */ -export function transform( - query: string, fn: VisitorFn = term => term -): string { - return query - - /* => 1 */ - .trim() - - /* => 2 */ - .split(/"([^"]+)"/g) - .map((parts, index) => index & 1 - ? parts.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g, " +") - : parts - ) - .join("") - - /* => 3 */ - .replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g, "") - - /* => 4 */ - .split(/\s+/g) - .reduce((prev, term) => { - const next = fn(term) - return [...prev, ...Array.isArray(next) ? next : [next]] - }, [] as string[]) - .map(term => /([~^]$)/.test(term) ? `${term}1` : term) - .map(term => /(^[+-]|[~^]\d+$)/.test(term) ? term : `${term}*`) - .join(" ") -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/_/index.ts b/src/templates/assets/javascripts/integrations/search/worker/_/index.ts deleted file mode 100644 index 26713573..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/_/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - ObservableInput, - Subject, - first, - merge, - of, - switchMap -} from "rxjs" - -import { feature } from "~/_" -import { watchToggle, watchWorker } from "~/browser" - -import { SearchIndex } from "../../config" -import { - SearchMessage, - SearchMessageType -} from "../message" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up search worker - * - * This function creates and initializes a web worker that is used for search, - * so that the user interface doesn't freeze. In general, the application does - * not care how search is implemented, as long as the web worker conforms to - * the format expected by the application as defined in `SearchMessage`. This - * allows the author to implement custom search functionality, by providing a - * custom web worker via configuration. - * - * Material for MkDocs' built-in search implementation makes use of Lunr.js, an - * efficient and fast implementation for client-side search. Leveraging a tiny - * iframe-based web worker shim, search is even supported for the `file://` - * protocol, enabling search for local non-hosted builds. - * - * If the protocol is `file://`, search initialization is deferred to mitigate - * freezing, as it's now synchronous by design - see https://bit.ly/3C521EO - * - * @see https://bit.ly/3igvtQv - How to implement custom search - * - * @param url - Worker URL - * @param index$ - Search index observable input - * - * @returns Search worker - */ -export function setupSearchWorker( - url: string, index$: ObservableInput -): Subject { - const worker$ = watchWorker(url) - merge( - of(location.protocol !== "file:"), - watchToggle("search") - ) - .pipe( - first(active => active), - switchMap(() => index$) - ) - .subscribe(({ config, docs }) => worker$.next({ - type: SearchMessageType.SETUP, - data: { - config, - docs, - options: { - suggest: feature("search.suggest") - } - } - })) - - /* Return search worker */ - return worker$ -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/index.ts b/src/templates/assets/javascripts/integrations/search/worker/index.ts deleted file mode 100644 index 7120ad6e..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./_" -export * from "./message" diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc b/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc deleted file mode 100644 index 3df9d551..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/main/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": "off", - "@typescript-eslint/no-misused-promises": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/search/worker/main/index.ts b/src/templates/assets/javascripts/integrations/search/worker/main/index.ts deleted file mode 100644 index 2df38080..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/main/index.ts +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import lunr from "lunr" - -import { getElement } from "~/browser/element/_" -import "~/polyfills" - -import { Search } from "../../_" -import { SearchConfig } from "../../config" -import { - SearchMessage, - SearchMessageType -} from "../message" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Add support for `iframe-worker` shim - * - * While `importScripts` is synchronous when executed inside of a web worker, - * it's not possible to provide a synchronous shim implementation. The cool - * thing is that awaiting a non-Promise will convert it into a Promise, so - * extending the type definition to return a `Promise` shouldn't break anything. - * - * @see https://bit.ly/2PjDnXi - GitHub comment - * - * @param urls - Scripts to load - * - * @returns Promise resolving with no result - */ -declare global { - function importScripts(...urls: string[]): Promise | void -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Search index - */ -let index: Search - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch (= import) multi-language support through `lunr-languages` - * - * This function automatically imports the stemmers necessary to process the - * languages which are defined as part of the search configuration. - * - * If the worker runs inside of an `iframe` (when using `iframe-worker` as - * a shim), the base URL for the stemmers to be loaded must be determined by - * searching for the first `script` element with a `src` attribute, which will - * contain the contents of this script. - * - * @param config - Search configuration - * - * @returns Promise resolving with no result - */ -async function setupSearchLanguages( - config: SearchConfig -): Promise { - let base = "../lunr" - - /* Detect `iframe-worker` and fix base URL */ - if (typeof parent !== "undefined" && "IFrameWorker" in parent) { - const worker = getElement("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 { - switch (message.type) { - - /* Search setup message */ - case SearchMessageType.SETUP: - await setupSearchLanguages(message.data.config) - index = new Search(message.data) - return { - type: SearchMessageType.READY - } - - /* Search query message */ - case SearchMessageType.QUERY: - const query = message.data - try { - return { - type: SearchMessageType.RESULT, - data: index.search(query) - } - - /* Return empty result in case of error */ - } catch (err) { - console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`) - console.warn(err) - return { - type: SearchMessageType.RESULT, - data: { items: [] } - } - } - - /* All other messages */ - default: - throw new TypeError("Invalid message type") - } -} - -/* ---------------------------------------------------------------------------- - * Worker - * ------------------------------------------------------------------------- */ - -/* Expose Lunr.js in global scope, or stemmers won't work */ -self.lunr = lunr - -/* Handle messages */ -addEventListener("message", async ev => { - postMessage(await handler(ev.data)) -}) diff --git a/src/templates/assets/javascripts/integrations/search/worker/message/index.ts b/src/templates/assets/javascripts/integrations/search/worker/message/index.ts deleted file mode 100644 index 54d5001e..00000000 --- a/src/templates/assets/javascripts/integrations/search/worker/message/index.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { SearchResult } from "../../_" -import { SearchIndex } from "../../config" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Search message type - */ -export const enum SearchMessageType { - SETUP, /* Search index setup */ - READY, /* Search index ready */ - QUERY, /* Search query */ - RESULT /* Search results */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Message containing the data necessary to setup the search index - */ -export interface SearchSetupMessage { - type: SearchMessageType.SETUP /* Message type */ - data: SearchIndex /* Message data */ -} - -/** - * Message indicating the search index is ready - */ -export interface SearchReadyMessage { - type: SearchMessageType.READY /* Message type */ -} - -/** - * Message containing a search query - */ -export interface SearchQueryMessage { - type: SearchMessageType.QUERY /* Message type */ - data: string /* Message data */ -} - -/** - * Message containing results for a search query - */ -export interface SearchResultMessage { - type: SearchMessageType.RESULT /* Message type */ - data: SearchResult /* Message data */ -} - -/* ------------------------------------------------------------------------- */ - -/** - * Message exchanged with the search worker - */ -export type SearchMessage = - | SearchSetupMessage - | SearchReadyMessage - | SearchQueryMessage - | SearchResultMessage - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Type guard for search ready messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchReadyMessage( - message: SearchMessage -): message is SearchReadyMessage { - return message.type === SearchMessageType.READY -} - -/** - * Type guard for search result messages - * - * @param message - Search worker message - * - * @returns Test result - */ -export function isSearchResultMessage( - message: SearchMessage -): message is SearchResultMessage { - return message.type === SearchMessageType.RESULT -} diff --git a/src/templates/assets/javascripts/integrations/sitemap/index.ts b/src/templates/assets/javascripts/integrations/sitemap/index.ts deleted file mode 100644 index 08695bad..00000000 --- a/src/templates/assets/javascripts/integrations/sitemap/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { - EMPTY, - Observable, - catchError, - defaultIfEmpty, - map, - of, - tap -} from "rxjs" - -import { configuration } from "~/_" -import { getElements, requestXML } from "~/browser" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sitemap, i.e. a list of URLs - */ -export type Sitemap = string[] - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Preprocess a list of URLs - * - * This function replaces the `site_url` in the sitemap with the actual base - * URL, to allow instant navigation to work in occasions like Netlify previews. - * - * @param urls - URLs - * - * @returns URL path parts - */ -function preprocess(urls: Sitemap): Sitemap { - if (urls.length < 2) - return [""] - - /* Take the first two URLs and remove everything after the last slash */ - const [root, next] = [...urls] - .sort((a, b) => a.length - b.length) - .map(url => url.replace(/[^/]+$/, "")) - - /* Compute common prefix */ - let index = 0 - if (root === next) - index = root.length - else - while (root.charCodeAt(index) === next.charCodeAt(index)) - index++ - - /* Remove common prefix and return in original order */ - return urls.map(url => url.replace(root.slice(0, index), "")) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Fetch the sitemap for the given base URL - * - * @param base - Base URL - * - * @returns Sitemap observable - */ -export function fetchSitemap(base?: URL): Observable { - const cached = __md_get("__sitemap", sessionStorage, base) - if (cached) { - return of(cached) - } else { - const config = configuration() - return requestXML(new URL("sitemap.xml", base || config.base)) - .pipe( - map(sitemap => preprocess(getElements("loc", sitemap) - .map(node => node.textContent!) - )), - catchError(() => EMPTY), // @todo refactor instant loading - defaultIfEmpty([]), - tap(sitemap => __md_set("__sitemap", sitemap, sessionStorage, base)) - ) - } -} diff --git a/src/templates/assets/javascripts/integrations/version/.eslintrc b/src/templates/assets/javascripts/integrations/version/.eslintrc deleted file mode 100644 index 38a5714d..00000000 --- a/src/templates/assets/javascripts/integrations/version/.eslintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "no-null/no-null": "off" - } -} diff --git a/src/templates/assets/javascripts/integrations/version/index.ts b/src/templates/assets/javascripts/integrations/version/index.ts deleted file mode 100644 index 38d78f17..00000000 --- a/src/templates/assets/javascripts/integrations/version/index.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 subject */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set up version selector - * - * @param options - Options - */ -export function setupVersionSelector( - { document$ }: SetupOptions -): void { - const config = configuration() - const versions$ = requestJSON( - 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(document.body, "click") - .pipe( - filter(ev => !ev.metaKey && !ev.ctrlKey), - withLatestFrom(current$), - switchMap(([ev, current]) => { - if (ev.target instanceof Element) { - const el = ev.target.closest("a") - if (el && !el.target && urls.has(el.href)) { - const url = el.href - // This is a temporary hack to detect if a version inside the - // version selector or on another part of the site was clicked. - // If we're inside the version selector, we definitely want to - // find the same page, as we might have different deployments - // due to aliases. However, if we're outside the version - // selector, we must abort here, because we might otherwise - // interfere with instant navigation. We need to refactor this - // at some point together with instant navigation. - // - // See https://github.com/squidfunk/mkdocs-material/issues/4012 - if (!ev.target.closest(".md-version")) { - const version = urls.get(url)! - if (version === current) - return EMPTY - } - ev.preventDefault() - return of(url) - } - } - return EMPTY - }), - switchMap(url => { - const { version } = urls.get(url)! - return fetchSitemap(new URL(url)) - .pipe( - map(sitemap => { - const location = getLocation() - const path = location.href.replace(config.base, "") - return sitemap.includes(path.split("#")[0]) - ? new URL(`../${version}/${path}`, config.base) - : new URL(url) - }) - ) - }) - ) - ) - ) - .subscribe(url => setLocation(url, true)) - - /* Render version selector and warning */ - combineLatest([versions$, current$]) - .subscribe(([versions, current]) => { - const topic = getElement(".md-header__topic") - topic.appendChild(renderVersionSelector(versions, current)) - }) - - /* Integrate outdated version banner with instant navigation */ - document$.pipe(switchMap(() => current$)) - .subscribe(current => { - - /* Check if version state was already determined */ - let outdated = __md_get("__outdated", sessionStorage) - if (outdated === null) { - outdated = true - - /* Obtain and normalize default versions */ - let ignored = config.version?.default || "latest" - if (!Array.isArray(ignored)) - ignored = [ignored] - - /* Check if version is considered a default */ - main: for (const ignore of ignored) - for (const alias of current.aliases) - if (new RegExp(ignore, "i").test(alias)) { - outdated = false - break main - } - - /* Persist version state in session storage */ - __md_set("__outdated", outdated, sessionStorage) - } - - /* Unhide outdated version banner */ - if (outdated) - for (const warning of getComponentElements("outdated")) - warning.hidden = false - }) -} diff --git a/src/templates/assets/javascripts/patches/indeterminate/index.ts b/src/templates/assets/javascripts/patches/indeterminate/index.ts deleted file mode 100644 index 9b7b0d5a..00000000 --- a/src/templates/assets/javascripts/patches/indeterminate/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - tablet$: Observable /* 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( - ".md-toggle--indeterminate" - )), - tap(el => { - el.indeterminate = true - el.checked = false - }), - mergeMap(el => fromEvent(el, "change") - .pipe( - takeWhile(() => el.classList.contains("md-toggle--indeterminate")), - map(() => el) - ) - ), - withLatestFrom(tablet$) - ) - .subscribe(([el, tablet]) => { - el.classList.remove("md-toggle--indeterminate") - if (tablet) - el.checked = false - }) -} diff --git a/src/templates/assets/javascripts/patches/index.ts b/src/templates/assets/javascripts/patches/index.ts deleted file mode 100644 index b6e65fc0..00000000 --- a/src/templates/assets/javascripts/patches/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./indeterminate" -export * from "./scrollfix" -export * from "./scrolllock" diff --git a/src/templates/assets/javascripts/patches/scrollfix/index.ts b/src/templates/assets/javascripts/patches/scrollfix/index.ts deleted file mode 100644 index 607c46a0..00000000 --- a/src/templates/assets/javascripts/patches/scrollfix/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ -} - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Check whether the given device is an Apple device - * - * @returns Test result - */ -function isAppleDevice(): boolean { - return /(iPad|iPhone|iPod)/.test(navigator.userAgent) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Patch all elements with `data-md-scrollfix` attributes - * - * This is a year-old patch which ensures that overflow scrolling works at the - * top and bottom of containers on iOS by ensuring a `1px` scroll offset upon - * the start of a touch event. - * - * @see https://bit.ly/2SCtAOO - Original source - * - * @param options - Options - */ -export function patchScrollfix( - { document$ }: PatchOptions -): void { - document$ - .pipe( - switchMap(() => getElements("[data-md-scrollfix]")), - tap(el => el.removeAttribute("data-md-scrollfix")), - filter(isAppleDevice), - mergeMap(el => fromEvent(el, "touchstart") - .pipe( - map(() => el) - ) - ) - ) - .subscribe(el => { - const top = el.scrollTop - - /* We're at the top of the container */ - if (top === 0) { - el.scrollTop = 1 - - /* We're at the bottom of the container */ - } else if (top + el.offsetHeight === el.scrollHeight) { - el.scrollTop = top - 1 - } - }) -} diff --git a/src/templates/assets/javascripts/patches/scrolllock/index.ts b/src/templates/assets/javascripts/patches/scrolllock/index.ts deleted file mode 100644 index 4ec3e103..00000000 --- a/src/templates/assets/javascripts/patches/scrolllock/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 observable */ - tablet$: Observable /* Media tablet observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Patch the document body to lock when search is open - * - * For mobile and tablet viewports, the search is rendered full screen, which - * leads to scroll leaking when at the top or bottom of the search result. This - * function locks the body when the search is in full screen mode, and restores - * the scroll position when leaving. - * - * @param options - Options - */ -export function patchScrolllock( - { viewport$, tablet$ }: PatchOptions -): void { - combineLatest([watchToggle("search"), tablet$]) - .pipe( - map(([active, tablet]) => active && !tablet), - switchMap(active => of(active) - .pipe( - delay(active ? 400 : 100) - ) - ), - withLatestFrom(viewport$) - ) - .subscribe(([active, { offset: { y }}]) => { - if (active) { - document.body.setAttribute("data-md-scrolllock", "") - document.body.style.top = `-${y}px` - } else { - const value = -1 * parseInt(document.body.style.top, 10) - document.body.removeAttribute("data-md-scrolllock") - document.body.style.top = "" - if (value) - window.scrollTo(0, value) - } - }) -} diff --git a/src/templates/assets/javascripts/polyfills/index.ts b/src/templates/assets/javascripts/polyfills/index.ts deleted file mode 100644 index 2aec8290..00000000 --- a/src/templates/assets/javascripts/polyfills/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 - ): void { - const parent = this.parentNode - if (parent) { - if (nodes.length === 0) - parent.removeChild(this) - - /* Replace children and create text nodes */ - for (let i = nodes.length - 1; i >= 0; i--) { - let node = nodes[i] - if (typeof node === "string") - node = document.createTextNode(node) - else if (node.parentNode) - node.parentNode.removeChild(node) - - /* Replace child or insert before previous sibling */ - if (!i) - parent.replaceChild(node, this) - else - parent.insertBefore(this.previousSibling!, node) - } - } - } -} diff --git a/src/templates/assets/javascripts/templates/annotation/index.tsx b/src/templates/assets/javascripts/templates/annotation/index.tsx deleted file mode 100644 index 9b8f85f5..00000000 --- a/src/templates/assets/javascripts/templates/annotation/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { h } from "~/utilities" - -import { renderTooltip } from "../tooltip" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render an annotation - * - * @param id - Annotation identifier - * @param prefix - Tooltip identifier prefix - * - * @returns Element - */ -export function renderAnnotation( - id: string | number, prefix?: string -): HTMLElement { - prefix = prefix ? `${prefix}_annotation_${id}` : undefined - - /* Render tooltip with anchor, if given */ - if (prefix) { - const anchor = prefix ? `#${prefix}` : undefined - return ( - - ) - } else { - return ( - - ) - } -} diff --git a/src/templates/assets/javascripts/templates/clipboard/index.tsx b/src/templates/assets/javascripts/templates/clipboard/index.tsx deleted file mode 100644 index 95dbf12a..00000000 --- a/src/templates/assets/javascripts/templates/clipboard/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( - - ) -} diff --git a/src/templates/assets/javascripts/templates/index.ts b/src/templates/assets/javascripts/templates/index.ts deleted file mode 100644 index b50b93b8..00000000 --- a/src/templates/assets/javascripts/templates/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./annotation" -export * from "./clipboard" -export * from "./search" -export * from "./source" -export * from "./tabbed" -export * from "./table" -export * from "./version" diff --git a/src/templates/assets/javascripts/templates/search/index.tsx b/src/templates/assets/javascripts/templates/search/index.tsx deleted file mode 100644 index 350c0505..00000000 --- a/src/templates/assets/javascripts/templates/search/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { ComponentChild } from "preact" - -import { configuration, feature, translation } from "~/_" -import { SearchItem } from "~/integrations/search" -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Render flag - */ -const enum Flag { - TEASER = 1, /* Render teaser */ - PARENT = 2 /* Render as parent */ -} - -/* ---------------------------------------------------------------------------- - * Helper function - * ------------------------------------------------------------------------- */ - -/** - * Render a search document - * - * @param document - Search document - * @param flag - Render flags - * - * @returns Element - */ -function renderSearchDocument( - document: SearchItem, flag: Flag -): HTMLElement { - const parent = flag & Flag.PARENT - const teaser = flag & Flag.TEASER - - /* Render missing query terms */ - const missing = Object.keys(document.terms) - .filter(key => !document.terms[key]) - .reduce((list, key) => [ - ...list, {key}, " " - ], []) - .slice(0, -1) - - /* Assemble query string for highlighting */ - const config = configuration() - const url = new URL(document.location, config.base) - if (feature("search.highlight")) - url.searchParams.set("h", Object.entries(document.terms) - .filter(([, match]) => match) - .reduce((highlight, [value]) => `${highlight} ${value}`.trim(), "") - ) - - /* Render article or section, depending on flags */ - const { tags } = configuration() - return ( - -
    - {parent > 0 &&
    } - {parent > 0 &&

    {document.title}

    } - {parent <= 0 &&

    {document.title}

    } - {teaser > 0 && document.text.length > 0 && - document.text - } - {document.tags && document.tags.map(tag => { - const type = tags - ? tag in tags - ? `md-tag-icon md-tag--${tags[tag]}` - : "md-tag-icon" - : "" - return ( - {tag} - ) - })} - {teaser > 0 && missing.length > 0 && -

    - {translation("search.result.term.missing")}: {...missing} -

    - } -
    -
    - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a search result - * - * @param result - Search result - * - * @returns Element - */ -export function renderSearchResultItem( - result: SearchItem[] -): HTMLElement { - const threshold = result[0].score - const docs = [...result] - - const config = configuration() - - /* Find and extract parent article */ - const parent = docs.findIndex(doc => { - const l = `${new URL(doc.location, config.base)}` // @todo hacky - return !l.includes("#") - }) - const [article] = docs.splice(parent, 1) - - /* Determine last index above threshold */ - let index = docs.findIndex(doc => doc.score < threshold) - if (index === -1) - index = docs.length - - /* Partition sections */ - const best = docs.slice(0, index) - const more = docs.slice(index) - - /* Render children */ - const children = [ - renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)), - ...best.map(section => renderSearchDocument(section, Flag.TEASER)), - ...more.length ? [ -
    - -
    - {more.length > 0 && more.length === 1 - ? translation("search.result.more.one") - : translation("search.result.more.other", more.length) - } -
    -
    - {...more.map(section => renderSearchDocument(section, Flag.TEASER))} -
    - ] : [] - ] - - /* Render search result */ - return ( -
  • - {children} -
  • - ) -} diff --git a/src/templates/assets/javascripts/templates/source/index.tsx b/src/templates/assets/javascripts/templates/source/index.tsx deleted file mode 100644 index b59a8f67..00000000 --- a/src/templates/assets/javascripts/templates/source/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( -
      - {Object.entries(facts).map(([key, value]) => ( -
    • - {typeof value === "number" ? round(value) : value} -
    • - ))} -
    - ) -} diff --git a/src/templates/assets/javascripts/templates/tabbed/index.tsx b/src/templates/assets/javascripts/templates/tabbed/index.tsx deleted file mode 100644 index b283ac66..00000000 --- a/src/templates/assets/javascripts/templates/tabbed/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( - - ) -} diff --git a/src/templates/assets/javascripts/templates/table/index.tsx b/src/templates/assets/javascripts/templates/table/index.tsx deleted file mode 100644 index 1fcba152..00000000 --- a/src/templates/assets/javascripts/templates/table/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( -
    -
    - {table} -
    -
    - ) -} diff --git a/src/templates/assets/javascripts/templates/tooltip/index.tsx b/src/templates/assets/javascripts/templates/tooltip/index.tsx deleted file mode 100644 index ec583490..00000000 --- a/src/templates/assets/javascripts/templates/tooltip/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import { h } from "~/utilities" - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a tooltip - * - * @param id - Tooltip identifier - * - * @returns Element - */ -export function renderTooltip(id?: string): HTMLElement { - return ( -
    -
    -
    - ) -} diff --git a/src/templates/assets/javascripts/templates/version/index.tsx b/src/templates/assets/javascripts/templates/version/index.tsx deleted file mode 100644 index 4aff7aa7..00000000 --- a/src/templates/assets/javascripts/templates/version/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 ( -
  • - - {version.title} - -
  • - ) -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Render a version selector - * - * @param versions - Versions - * @param active - Active version - * - * @returns Element - */ -export function renderVersionSelector( - versions: Version[], active: Version -): HTMLElement { - return ( -
    - -
      - {versions.map(renderVersion)} -
    -
    - ) -} diff --git a/src/templates/assets/javascripts/utilities/h/.eslintrc b/src/templates/assets/javascripts/utilities/h/.eslintrc deleted file mode 100644 index d79b45b0..00000000 --- a/src/templates/assets/javascripts/utilities/h/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-namespace": "off", - "jsdoc/require-jsdoc": "off" - } -} diff --git a/src/templates/assets/javascripts/utilities/h/index.ts b/src/templates/assets/javascripts/utilities/h/index.ts deleted file mode 100644 index 08d809f1..00000000 --- a/src/templates/assets/javascripts/utilities/h/index.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION 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 - -/** - * Child element - */ -type Child = - | ChildNode - | HTMLElement - | Text - | string - | number - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Append a child node to an element - * - * @param el - Element - * @param child - Child node(s) - */ -function appendChild(el: HTMLElement, child: Child | Child[]): void { - - /* Handle primitive types (including raw HTML) */ - if (typeof child === "string" || typeof child === "number") { - el.innerHTML += child.toString() - - /* Handle nodes */ - } else if (child instanceof Node) { - el.appendChild(child) - - /* Handle nested children */ - } else if (Array.isArray(child)) { - for (const node of child) - appendChild(el, node) - } -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * JSX factory - * - * @template T - Element type - * - * @param tag - HTML tag - * @param attributes - HTML attributes - * @param children - Child elements - * - * @returns Element - */ -export function h( - tag: T, attributes?: Attributes | null, ...children: Child[] -): HTMLElementTagNameMap[T] - -export function h( - tag: string, attributes?: Attributes | null, ...children: Child[] -): T - -export function h( - tag: string, attributes?: Attributes | null, ...children: Child[] -): T { - const el = document.createElement(tag) - - /* Set attributes, if any */ - if (attributes) - for (const attr of Object.keys(attributes)) { - if (typeof attributes[attr] === "undefined") - continue - - /* Set default attribute or boolean */ - if (typeof attributes[attr] !== "boolean") - el.setAttribute(attr, attributes[attr]) - else - el.setAttribute(attr, "") - } - - /* Append child nodes */ - for (const child of children) - appendChild(el, child) - - /* Return element */ - return el as T -} - -/* ---------------------------------------------------------------------------- - * Namespace - * ------------------------------------------------------------------------- */ - -export declare namespace h { - namespace JSX { - type Element = HTMLElement - type IntrinsicElements = JSXInternal.IntrinsicElements - } -} diff --git a/src/templates/assets/javascripts/utilities/index.ts b/src/templates/assets/javascripts/utilities/index.ts deleted file mode 100644 index 42886e0b..00000000 --- a/src/templates/assets/javascripts/utilities/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -export * from "./h" -export * from "./round" diff --git a/src/templates/assets/javascripts/utilities/round/index.ts b/src/templates/assets/javascripts/utilities/round/index.ts deleted file mode 100644 index 3e6bf91a..00000000 --- a/src/templates/assets/javascripts/utilities/round/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Round a number for display with repository facts - * - * This is a reverse-engineered version of GitHub's weird rounding algorithm - * for stars, forks and all other numbers. While all numbers below `1,000` are - * returned as-is, bigger numbers are converted to fixed numbers: - * - * - `1,049` => `1k` - * - `1,050` => `1.1k` - * - `1,949` => `1.9k` - * - `1,950` => `2k` - * - * @param value - Original value - * - * @returns Rounded value - */ -export function round(value: number): string { - if (value > 999) { - const digits = +((value - 950) % 1000 > 99) - return `${((value + 0.000001) / 1000).toFixed(digits)}k` - } else { - return value.toString() - } -} diff --git a/src/templates/assets/javascripts/workers/search.ts b/src/templates/assets/javascripts/workers/search.ts deleted file mode 100644 index e995b1ff..00000000 --- a/src/templates/assets/javascripts/workers/search.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2016-2023 Martin Donath - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -import "~/integrations/search/worker/main" diff --git a/src/templates/assets/stylesheets/_config.scss b/src/templates/assets/stylesheets/_config.scss deleted file mode 100644 index e64b8e29..00000000 --- a/src/templates/assets/stylesheets/_config.scss +++ /dev/null @@ -1,42 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Variables: breakpoints -// ---------------------------------------------------------------------------- - -// Device-specific breakpoints -$break-devices: ( - mobile: ( - portrait: px2em(220px) px2em(479.75px), - landscape: px2em(480px) px2em(719.75px) - ), - tablet: ( - portrait: px2em(720px) px2em(959.75px), - landscape: px2em(960px) px2em(1219.75px) - ), - screen: ( - small: px2em(1220px) px2em(1599.75px), - medium: px2em(1600px) px2em(1999.75px), - large: px2em(2000px) - ) -); diff --git a/src/templates/assets/stylesheets/main.scss b/src/templates/assets/stylesheets/main.scss deleted file mode 100644 index 2b203d3d..00000000 --- a/src/templates/assets/stylesheets/main.scss +++ /dev/null @@ -1,86 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Dependencies -// ---------------------------------------------------------------------------- - -@import "material-color"; -@import "material-shadows"; - -// ---------------------------------------------------------------------------- -// Local imports -// ---------------------------------------------------------------------------- - -@import "utilities/break"; -@import "utilities/convert"; - -@import "config"; - -@import "main/resets"; -@import "main/colors"; -@import "main/icons"; -@import "main/typeset"; - -@import "main/components/author"; -@import "main/components/banner"; -@import "main/components/base"; -@import "main/components/clipboard"; -@import "main/components/consent"; -@import "main/components/content"; -@import "main/components/dialog"; -@import "main/components/feedback"; -@import "main/components/footer"; -@import "main/components/form"; -@import "main/components/header"; -@import "main/components/meta"; -@import "main/components/nav"; -@import "main/components/pagination"; -@import "main/components/post"; -@import "main/components/progress"; -@import "main/components/search"; -@import "main/components/select"; -@import "main/components/sidebar"; -@import "main/components/source"; -@import "main/components/status"; -@import "main/components/tabs"; -@import "main/components/tag"; -@import "main/components/tooltip"; -@import "main/components/top"; -@import "main/components/version"; - -@import "main/extensions/markdown/admonition"; -@import "main/extensions/markdown/footnotes"; -@import "main/extensions/markdown/toc"; - -@import "main/extensions/pymdownx/arithmatex"; -@import "main/extensions/pymdownx/critic"; -@import "main/extensions/pymdownx/details"; -@import "main/extensions/pymdownx/emoji"; -@import "main/extensions/pymdownx/highlight"; -@import "main/extensions/pymdownx/keys"; -@import "main/extensions/pymdownx/tabbed"; -@import "main/extensions/pymdownx/tasklist"; - -@import "main/integrations/mermaid"; - -@import "main/modifiers"; diff --git a/src/templates/assets/stylesheets/main/_colors.scss b/src/templates/assets/stylesheets/main/_colors.scss deleted file mode 100644 index 68969fe9..00000000 --- a/src/templates/assets/stylesheets/main/_colors.scss +++ /dev/null @@ -1,153 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Color variables -:root { - @extend %root; - - // Primary color shades - --md-primary-fg-color: hsla(#{hex2hsl($clr-indigo-500)}, 1); - --md-primary-fg-color--light: hsla(#{hex2hsl($clr-indigo-400)}, 1); - --md-primary-fg-color--dark: hsla(#{hex2hsl($clr-indigo-700)}, 1); - --md-primary-bg-color: hsla(0, 0%, 100%, 1); - --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); - - // Accent color shades - --md-accent-fg-color: hsla(#{hex2hsl($clr-indigo-a200)}, 1); - --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1); - --md-accent-bg-color: hsla(0, 0%, 100%, 1); - --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); -} - -// ---------------------------------------------------------------------------- - -// Allow to explicitly use color schemes in nested content -[data-md-color-scheme="default"] { - @extend %root; - - // Indicate that the site is rendered with a light color scheme - color-scheme: light; - - // Hide images for dark mode - img[src$="#only-dark"], - img[src$="#gh-dark-mode-only"] { - display: none; - } -} - -// ---------------------------------------------------------------------------- -// Placeholders -// ---------------------------------------------------------------------------- - -// Default theme, i.e. light mode -%root { - - // Color hue in the range [0,360] - change this variable to alter the tone - // of the theme, e.g. to make it more redish or greenish - --md-hue: 225deg; - - // Default color shades - --md-default-fg-color: hsla(0, 0%, 0%, 0.87); - --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54); - --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32); - --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07); - --md-default-bg-color: hsla(0, 0%, 100%, 1); - --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7); - --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3); - --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12); - - // Code color shades - --md-code-fg-color: hsla(200, 18%, 26%, 1); - --md-code-bg-color: hsla(200, 0%, 96%, 1); - - // Code highlighting color shades - --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 1); - --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.1); - --md-code-hl-number-color: hsla(0, 67%, 50%, 1); - --md-code-hl-special-color: hsla(340, 83%, 47%, 1); - --md-code-hl-function-color: hsla(291, 45%, 50%, 1); - --md-code-hl-constant-color: hsla(250, 63%, 60%, 1); - --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1); - --md-code-hl-string-color: hsla(150, 63%, 30%, 1); - --md-code-hl-name-color: var(--md-code-fg-color); - --md-code-hl-operator-color: var(--md-default-fg-color--light); - --md-code-hl-punctuation-color: var(--md-default-fg-color--light); - --md-code-hl-comment-color: var(--md-default-fg-color--light); - --md-code-hl-generic-color: var(--md-default-fg-color--light); - --md-code-hl-variable-color: var(--md-default-fg-color--light); - - // Typeset color shades - --md-typeset-color: var(--md-default-fg-color); - - // Typeset `a` color shades - --md-typeset-a-color: var(--md-primary-fg-color); - - // Typeset `del` and `ins` color shades - --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); - --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); - - // Typeset `kbd` color shades - --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); - --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); - --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); - - // Typeset `mark` color shades - --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); - - // Typeset `table` color shades - --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); - --md-typeset-table-color--light: hsla(0, 0%, 0%, 0.035); - - // Admonition color shades - --md-admonition-fg-color: var(--md-default-fg-color); - --md-admonition-bg-color: var(--md-default-bg-color); - - // Warning color shades - --md-warning-fg-color: hsla(0, 0%, 0%, 0.87); - --md-warning-bg-color: hsla(60, 100%, 80%, 1); - - // Footer color shades - --md-footer-fg-color: hsla(0, 0%, 100%, 1); - --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); - --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.45); - --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); - --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); - - // Shadow depth 1 - --md-shadow-z1: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); - - // Shadow depth 2 - --md-shadow-z2: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.1), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); - - // Shadow depth 3 - --md-shadow-z3: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); -} diff --git a/src/templates/assets/stylesheets/main/_icons.scss b/src/templates/assets/stylesheets/main/_icons.scss deleted file mode 100644 index 9853e93d..00000000 --- a/src/templates/assets/stylesheets/main/_icons.scss +++ /dev/null @@ -1,37 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Icon -.md-icon { - - // SVG defaults - svg { - display: block; - width: px2rem(24px); - height: px2rem(24px); - fill: currentcolor; - } -} diff --git a/src/templates/assets/stylesheets/main/_modifiers.scss b/src/templates/assets/stylesheets/main/_modifiers.scss deleted file mode 100644 index 4b2b046a..00000000 --- a/src/templates/assets/stylesheets/main/_modifiers.scss +++ /dev/null @@ -1,48 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // [tablet +]: Allow for rendering content as sidebars - @include break-from-device(tablet) { - - // Modifier to float block elements - .inline { - float: inline-start; - width: px2rem(234px); - margin-inline-end: px2rem(16px); - margin-top: 0; - margin-bottom: px2rem(16px); - - // Modifier to move to end (ltr: right, rtl: left) - &.end { - float: inline-end; - margin-inline: px2rem(16px) 0; - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/_resets.scss b/src/templates/assets/stylesheets/main/_resets.scss deleted file mode 100644 index c6fc4b28..00000000 --- a/src/templates/assets/stylesheets/main/_resets.scss +++ /dev/null @@ -1,118 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Enforce correct box model and prevent adjustments of font size after -// orientation changes in IE and iOS -html { - box-sizing: border-box; - text-size-adjust: none; -} - -// All elements shall inherit the document default -*, -*::before, -*::after { - box-sizing: inherit; - - // [reduced motion]: Disable all transitions - @media (prefers-reduced-motion) { - transition: none !important; // stylelint-disable-line - } -} - -// Remove margin in all browsers -body { - margin: 0; -} - -// Reset tap outlines on iOS and Android -a, -button, -label, -input { - -webkit-tap-highlight-color: transparent; -} - -// Reset link styles -a { - color: inherit; - text-decoration: none; -} - -// Normalize horizontal separator styles -hr { - box-sizing: content-box; - display: block; - height: px2rem(1px); - padding: 0; - overflow: visible; - border: 0; -} - -// Normalize font-size in all browsers -small { - font-size: 80%; -} - -// Prevent subscript and superscript from affecting line-height -sub, -sup { - line-height: 1em; -} - -// Remove border on image -img { - border-style: none; -} - -// Reset table styles -table { - border-spacing: 0; - border-collapse: separate; -} - -// Reset table cell styles -td, -th { - font-weight: 400; - vertical-align: top; -} - -// Reset button styles -button { - padding: 0; - margin: 0; - font-family: inherit; - font-size: inherit; - background: transparent; - border: 0; -} - -// Reset input styles -input { - border: 0; - outline: none; -} diff --git a/src/templates/assets/stylesheets/main/_typeset.scss b/src/templates/assets/stylesheets/main/_typeset.scss deleted file mode 100644 index 1c322859..00000000 --- a/src/templates/assets/stylesheets/main/_typeset.scss +++ /dev/null @@ -1,603 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules: font definitions -// ---------------------------------------------------------------------------- - -// Enable font-smoothing in Webkit and FF -body { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - // Font with fallback for body copy - --md-text-font-family: - var(--md-text-font, _), - -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; - - // Font with fallback for code - --md-code-font-family: - var(--md-code-font, _), - SFMono-Regular, Consolas, Menlo, monospace; -} - -// Define default fonts -body, -input, -aside { - font-family: var(--md-text-font-family); - font-feature-settings: "kern", "liga"; - color: var(--md-typeset-color); -} - -// Define monospaced fonts -code, -pre, -kbd { - font-family: var(--md-code-font-family); - font-feature-settings: "kern"; -} - -// ---------------------------------------------------------------------------- -// Rules: typesetted content -// ---------------------------------------------------------------------------- - -// General variables -:root { - --md-typeset-table-sort-icon: svg-load("material/sort.svg"); - --md-typeset-table-sort-icon--asc: svg-load("material/sort-ascending.svg"); - --md-typeset-table-sort-icon--desc: svg-load("material/sort-descending.svg"); -} - -// ---------------------------------------------------------------------------- - -// Content that is typeset - if possible, all margins, paddings and font sizes -// should be set in ems, so nested blocks (e.g. admonitions) render correctly. -.md-typeset { - font-size: px2rem(16px); - line-height: 1.6; - color-adjust: exact; - - // [print]: We'll use a smaller `font-size` for printing, so code examples - // don't break too early, and `16px` looks too big anyway. - @media print { - font-size: px2rem(13.6px); - } - - // Default spacing - ul, - ol, - dl, - figure, - blockquote, - pre { - margin-block: 1em; - } - - // Headline on level 1 - h1 { - margin: 0 0 px2em(40px, 32px); - font-size: px2em(32px); - font-weight: 300; - line-height: 1.3; - color: var(--md-default-fg-color--light); - letter-spacing: -0.01em; - } - - // Headline on level 2 - h2 { - margin: px2em(40px, 25px) 0 px2em(16px, 25px); - font-size: px2em(25px); - font-weight: 300; - line-height: 1.4; - letter-spacing: -0.01em; - } - - // Headline on level 3 - h3 { - margin: px2em(32px, 20px) 0 px2em(16px, 20px); - font-size: px2em(20px); - font-weight: 400; - line-height: 1.5; - letter-spacing: -0.01em; - } - - // Headline on level 3 following level 2 - h2 + h3 { - margin-top: px2em(16px, 20px); - } - - // Headline on level 4 - h4 { - margin: px2em(16px) 0; - font-weight: 700; - letter-spacing: -0.01em; - } - - // Headline on level 5-6 - h5, - h6 { - margin: px2em(16px, 12.8px) 0; - font-size: px2em(12.8px); - font-weight: 700; - color: var(--md-default-fg-color--light); - letter-spacing: -0.01em; - } - - // Headline on level 5 - h5 { - text-transform: uppercase; - } - - // Horizontal separator - hr { - display: flow-root; - margin: 1.5em 0; - border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); - } - - // Text link - a { - color: var(--md-typeset-a-color); - word-break: break-word; - - // Also enable color transition on pseudo elements - &, - &::before { - transition: color 125ms; - } - - // Text link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - - // Inline code block - code { - background-color: var(--md-accent-fg-color--transparent); - } - } - - // Inline code block - code { - color: currentcolor; - transition: background-color 125ms; - } - - // Show outline for keyboard devices - &.focus-visible { - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(4px); - } - } - - // Code block - code, - pre, - kbd { - font-variant-ligatures: none; - color: var(--md-code-fg-color); - direction: ltr; - - // [print]: Wrap text and hide scollbars - @media print { - white-space: pre-wrap; - } - } - - // Inline code block - code { - padding: 0 px2em(4px, 13.6px); - font-size: px2em(13.6px); - word-break: break-word; - background-color: var(--md-code-bg-color); - border-radius: px2rem(2px); - box-decoration-break: clone; - - // Hide outline for pointer devices - &:not(.focus-visible) { - outline: none; - -webkit-tap-highlight-color: transparent; - } - } - - // Unformatted content - pre { - position: relative; - display: flow-root; - line-height: 1.4; - - // Code block - > code { - display: block; - padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); - margin: 0; - overflow: auto; - word-break: normal; - touch-action: auto; - outline-color: var(--md-accent-fg-color); - box-shadow: none; - box-decoration-break: slice; - scrollbar-width: thin; - scrollbar-color: var(--md-default-fg-color--lighter) transparent; - - // Code block on hover - &:hover { - scrollbar-color: var(--md-accent-fg-color) transparent; - } - - // Webkit scrollbar - &::-webkit-scrollbar { - width: px2rem(4px); - height: px2rem(4px); - } - - // Webkit scrollbar thumb - &::-webkit-scrollbar-thumb { - background-color: var(--md-default-fg-color--lighter); - - // Webkit scrollbar thumb on hover - &:hover { - background-color: var(--md-accent-fg-color); - } - } - } - } - - // Keyboard key - kbd { - display: inline-block; - padding: 0 px2em(8px, 12px); - font-size: px2em(12px); - color: var(--md-default-fg-color); - word-break: break-word; - vertical-align: text-top; - background-color: var(--md-typeset-kbd-color); - border-radius: px2rem(2px); - box-shadow: - 0 px2rem(2px) 0 px2rem(1px) var(--md-typeset-kbd-border-color), - 0 px2rem(2px) 0 var(--md-typeset-kbd-border-color), - 0 px2rem(-2px) px2rem(4px) var(--md-typeset-kbd-accent-color) inset; - } - - // Text highlighting marker - mark { - color: inherit; - word-break: break-word; - background-color: var(--md-typeset-mark-color); - box-decoration-break: clone; - } - - // Abbreviation - abbr { - text-decoration: none; - cursor: help; - border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light); - - // Show tooltip for touch devices - @media (hover: none) { - - // Tooltip - &[title]:is(:focus, :hover)::after { - position: absolute; - inset-inline: px2rem(16px); - padding: px2rem(4px) px2rem(6px); - margin-top: 2em; - font-size: px2rem(14px); - color: var(--md-default-bg-color); - content: attr(title); - background-color: var(--md-default-fg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z3); - } - } - } - - // Small text - small { - opacity: 0.75; - } - - // Superscript and subscript - sup, - sub { - margin-inline-start: px2em(1px, 12.8px); - } - - // Blockquotes, possibly nested - blockquote { - padding-inline-start: px2rem(12px); - margin-inline: 0; - color: var(--md-default-fg-color--light); - border-inline-start: px2rem(4px) solid var(--md-default-fg-color--lighter); - } - - // Unordered list - ul { - list-style-type: disc; - } - - // Unordered and ordered list - ul, - ol { - padding: 0; - margin-inline-start: px2em(10px); - - // Adjust display mode if not hidden - &:not([hidden]) { - display: flow-root; - } - - // Nested ordered list - ol { - list-style-type: lower-alpha; - - // Triply nested ordered list - ol { - list-style-type: lower-roman; - } - } - - // List element - li { - margin-inline-start: px2em(20px); - margin-bottom: 0.5em; - - // Adjust spacing - p, - blockquote { - margin: 0.5em 0; - } - - // Adjust spacing on last child - &:last-child { - margin-bottom: 0; - } - - // Nested list - :is(ul, ol) { - margin-block: 0.5em; - margin-inline-start: px2em(10px); - } - } - } - - // Definition list - dd { - margin-block: 1em 1.5em; - margin-inline-start: px2em(30px); - } - - // Image or video - img, - svg, - video { - max-width: 100%; - height: auto; - } - - // Image - img { - - // Adjust spacing when left-aligned - &[align="left"] { - margin: 1em; - margin-left: 0; - } - - // Adjust spacing when right-aligned - &[align="right"] { - margin: 1em; - margin-right: 0; - } - - // Adjust spacing when sole children - &[align]:only-child { - margin-top: 0; - } - } - - // Figure - figure { - display: flow-root; - width: fit-content; - max-width: 100%; - margin: 1em auto; - text-align: center; - - // Figure images - img { - display: block; - } - } - - // Figure caption - figcaption { - max-width: px2rem(480px); - margin: 1em auto; - font-style: italic; - } - - // Limit width to container - iframe { - max-width: 100%; - } - - // Data table - table:not([class]) { - display: inline-block; - max-width: 100%; - overflow: auto; - font-size: px2rem(12.8px); - touch-action: auto; - background-color: var(--md-default-bg-color); - border: px2rem(1px) solid var(--md-typeset-table-color); - border-radius: px2rem(2px); - - // [print]: Reset display mode so table header wraps when printing - @media print { - display: table; - } - - // Due to margin collapse because of the necessary inline-block hack, we - // cannot increase the bottom margin on the table, so we just increase the - // top margin on the following element - + * { - margin-top: 1.5em; - } - - // Elements in table heading and cell - :is(th, td) > * { - - // Adjust spacing on first child - &:first-child { - margin-top: 0; - } - - // Adjust spacing on last child - &:last-child { - margin-bottom: 0; - } - } - - // Table heading and cell - :is(th, td):not([align]) { - text-align: left; - - // Adjust for right-to-left languages - [dir="rtl"] & { - text-align: right; - } - } - - // Table heading - th { - min-width: px2rem(100px); - padding: px2em(12px, 12.8px) px2em(16px, 12.8px); - font-weight: 700; - vertical-align: top; - } - - // Table cell - td { - padding: px2em(12px, 12.8px) px2em(16px, 12.8px); - vertical-align: top; - border-top: px2rem(1px) solid var(--md-typeset-table-color); - } - - // Table body row - tbody tr { - transition: background-color 125ms; - - // Table row on hover - &:hover { - background-color: var(--md-typeset-table-color--light); - box-shadow: 0 px2rem(1px) 0 var(--md-default-bg-color) inset; - } - } - - // Text link in table - a { - word-break: normal; - } - } - - // Sortable table - table th[role="columnheader"] { - cursor: pointer; - - // Sort icon - &::after { - display: inline-block; - width: 1.2em; - height: 1.2em; - margin-inline-start: 0.5em; - vertical-align: text-bottom; - content: ""; - transition: background-color 125ms; - mask-image: var(--md-typeset-table-sort-icon); - mask-repeat: no-repeat; - mask-size: contain; - } - - // Show sort icon on hover - &:hover::after { - background-color: var(--md-default-fg-color--lighter); - } - - // Sort ascending icon - &[aria-sort="ascending"]::after { - background-color: var(--md-default-fg-color--light); - mask-image: var(--md-typeset-table-sort-icon--asc); - } - - // Sort descending icon - &[aria-sort="descending"]::after { - background-color: var(--md-default-fg-color--light); - mask-image: var(--md-typeset-table-sort-icon--desc); - } - } - - // Data table scroll wrapper - &__scrollwrap { - margin: 1em px2rem(-16px); - overflow-x: auto; - touch-action: auto; - } - - // Data table wrapper - &__table { - display: inline-block; - padding: 0 px2rem(16px); - margin-bottom: 0.5em; - - // [print]: Reset display mode so table header wraps when printing - @media print { - display: block; - } - - // Data table - html & table { - display: table; - width: 100%; - margin: 0; - overflow: hidden; - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: top-level -// ---------------------------------------------------------------------------- - -// [mobile -]: Align with body copy -@include break-to-device(mobile) { - - // Top-level unformatted content - .md-content__inner > pre { - margin: 1em px2rem(-16px); - - // Code block - code { - border-radius: 0; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_author.scss b/src/templates/assets/stylesheets/main/components/_author.scss deleted file mode 100644 index 111baf40..00000000 --- a/src/templates/assets/stylesheets/main/components/_author.scss +++ /dev/null @@ -1,86 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Author, i.e., GitHub user - .md-author { - position: relative; - display: block; - flex-shrink: 0; - width: px2rem(32px); - height: px2rem(32px); - overflow: hidden; - transition: - color 125ms, - transform 125ms; - - // Author image - img { - display: block; - border-radius: 100%; - } - - // More authors - &--more { - font-size: px2rem(12px); - font-weight: 700; - line-height: px2rem(32px); - color: var(--md-default-fg-color--lighter); - text-align: center; - background: var(--md-default-fg-color--lightest); - } - - // Enlarge image - &--long { - width: px2rem(48px); - height: px2rem(48px); - } - } - - // Author link - a.md-author { - transform: scale(1); - - // Author image - img { - filter: grayscale(100%) opacity(75%); - transition: filter 125ms; - } - - // Author on focus/hover - &:is(:focus, :hover) { - z-index: 1; - transform: scale(1.1); - - // Author image - img { - filter: grayscale(0%); - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_banner.scss b/src/templates/assets/stylesheets/main/components/_banner.scss deleted file mode 100644 index 8fe08c0f..00000000 --- a/src/templates/assets/stylesheets/main/components/_banner.scss +++ /dev/null @@ -1,68 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Banner for announcements and warnings -.md-banner { - overflow: auto; - color: var(--md-footer-fg-color); - background-color: var(--md-footer-bg-color); - - // [print]: Hide banner - @media print { - display: none; - } - - // Banner with warning - &--warning { - color: var(--md-warning-fg-color); - background-color: var(--md-warning-bg-color); - } - - // Banner wrapper - &__inner { - padding: 0 px2rem(16px); - margin: px2rem(12px) auto; - font-size: px2rem(14px); - } - - // Banner button - &__button { - float: inline-end; - color: inherit; - cursor: pointer; - transition: opacity 250ms; - - // [no-js]: Hide button - .no-js & { - display: none; - } - - // Button on hover - &:hover { - opacity: 0.7; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_base.scss b/src/templates/assets/stylesheets/main/components/_base.scss deleted file mode 100644 index 33f834ed..00000000 --- a/src/templates/assets/stylesheets/main/components/_base.scss +++ /dev/null @@ -1,182 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules: base grid and containers -// ---------------------------------------------------------------------------- - -// Stretch container to viewport and set base `font-size` -html { - height: 100%; - overflow-x: hidden; - // Hack: normally, we would set the base `font-size` to `62.5%`, so we can - // base all calculations on `10px`, but Chromium and Chrome define a minimal - // `font-size` of `12px` if the system language is set to Chinese. For this - // reason we just double the `font-size` and set it to `20px`. - // - // See https://github.com/squidfunk/mkdocs-material/issues/911 - font-size: 125%; - - // [screen medium +]: Set base `font-size` to `11px` - @include break-from-device(screen medium) { - font-size: 137.5%; - } - - // [screen large +]: Set base `font-size` to `12px` - @include break-from-device(screen large) { - font-size: 150%; - } -} - -// Stretch body to container - flexbox is used, so the footer will always be -// aligned to the bottom of the viewport -body { - position: relative; - display: flex; - flex-direction: column; - width: 100%; - min-height: 100%; - // Hack: reset `font-size` to `10px`, so the spacing for all inline elements - // is correct again. Otherwise the spacing would be based on `20px`. - font-size: px2rem(10px); - background-color: var(--md-default-bg-color); - - // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) - @media print { - display: block; - } - - // Body in locked state - &[data-md-scrolllock] { - - // [tablet portrait -]: Omit scroll bubbling - @include break-to-device(tablet portrait) { - position: fixed; - } - } -} - -// ---------------------------------------------------------------------------- - -// Grid container - this class is applied to wrapper elements within the -// header, content area and footer, and makes sure that their width is limited -// to `1220px`, and they are rendered centered if the screen is larger. -.md-grid { - max-width: px2rem(1220px); - margin-inline: auto; -} - -// Main container -.md-container { - display: flex; - flex-direction: column; - flex-grow: 1; - - // [print]: Omit flexbox layout due to a Firefox bug (https://mzl.la/39DgR3m) - @media print { - display: block; - } -} - -// Main area - stretch to remaining space of container -.md-main { - flex-grow: 1; - - // Main area wrapper - &__inner { - display: flex; - height: 100%; - margin-top: px2rem(24px + 6px); - } -} - -// Add ellipsis in case of overflowing text -.md-ellipsis { - overflow: hidden; - text-overflow: ellipsis; -} - -// ---------------------------------------------------------------------------- -// Rules: navigational elements -// ---------------------------------------------------------------------------- - -// Toggle - this class is applied to checkbox elements, which are used to -// implement the CSS-only drawer and navigation, as well as the search -.md-toggle { - display: none; -} - -// Option - this class is applied to radio elements, which are used to -// implement the color palette toggle -.md-option { - position: absolute; - width: 0; - height: 0; - opacity: 0; - - // Option label for checked radio button - &:checked + label:not([hidden]) { - display: block; - } - - // Show outline for keyboard devices - &.focus-visible + label { - outline-style: auto; - outline-color: var(--md-accent-fg-color); - } -} - -// Skip link -.md-skip { - position: fixed; - // Hack: if we don't set the negative `z-index`, the skip link will force the - // creation of new layers when code blocks are near the header on scrolling - z-index: -1; - padding: px2rem(6px) px2rem(10px); - margin: px2rem(10px); - font-size: px2rem(12.8px); - color: var(--md-default-bg-color); - background-color: var(--md-default-fg-color); - border-radius: px2rem(2px); - outline-color: var(--md-accent-fg-color); - opacity: 0; - transform: translateY(px2rem(8px)); - - // Show skip link on focus - &:focus { - z-index: 10; - opacity: 1; - transition: - transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 175ms 75ms; - transform: translateY(0); - } -} - -// ---------------------------------------------------------------------------- -// Rules: print styles -// ---------------------------------------------------------------------------- - -// Add margins to page -@page { - margin: 25mm; -} diff --git a/src/templates/assets/stylesheets/main/components/_clipboard.scss b/src/templates/assets/stylesheets/main/components/_clipboard.scss deleted file mode 100644 index c07c9c67..00000000 --- a/src/templates/assets/stylesheets/main/components/_clipboard.scss +++ /dev/null @@ -1,102 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Clipboard button variables -:root { - --md-clipboard-icon: svg-load("material/content-copy.svg"); -} - -// ---------------------------------------------------------------------------- - -// Clipboard button -.md-clipboard { - position: absolute; - top: px2em(8px); - right: px2em(8px); - z-index: 1; - width: px2em(24px); - height: px2em(24px); - color: var(--md-default-fg-color--lightest); - cursor: pointer; - border-radius: px2rem(2px); - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(2px); - transition: color 250ms; - - // [print]: Hide button - @media print { - display: none; - } - - // Hide outline for pointer devices - &:not(.focus-visible) { - outline: none; - -webkit-tap-highlight-color: transparent; - } - - // Darken color on code block hover - :hover > & { - color: var(--md-default-fg-color--light); - } - - // Button on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - - // Button icon - the width and height are defined in `em`, so the size is - // automatically adjusted for nested code blocks (e.g. in admonitions) - &::after { - display: block; - width: px2em(18px); - height: px2em(18px); - margin: 0 auto; - content: ""; - background-color: currentcolor; - mask-image: var(--md-clipboard-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Inline clipboard button - &--inline { - cursor: pointer; - - // Code block - code { - transition: - color 250ms, - background-color 250ms; - } - - // Code block on focus/hover - &:is(:focus, :hover) code { - color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_consent.scss b/src/templates/assets/stylesheets/main/components/_consent.scss deleted file mode 100644 index 5502460c..00000000 --- a/src/templates/assets/stylesheets/main/components/_consent.scss +++ /dev/null @@ -1,127 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Keyframes -// ---------------------------------------------------------------------------- - -// Show consent -@keyframes consent { - 0% { - opacity: 0; - transform: translateY(100%); - } - - 100% { - opacity: 1; - transform: translateY(0); - } -} - -// Show consent overlay -@keyframes overlay { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Consent -.md-consent { - - // Consent overlay - &__overlay { - position: fixed; - top: 0; - z-index: 5; - width: 100%; - height: 100%; - background-color: hsla(0, 0%, 0%, 0.54); - opacity: 1; - backdrop-filter: blur(px2rem(2px)); - animation: overlay 250ms both; - } - - // Consent wrapper - &__inner { - position: fixed; - bottom: 0; - z-index: 5; - width: 100%; - max-height: 100%; - padding: 0; - overflow: auto; - background-color: var(--md-default-bg-color); - border: 0; - border-radius: px2rem(2px); - box-shadow: - 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), - 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); - animation: consent 500ms cubic-bezier(0.1, 0.7, 0.1, 1) both; - } - - // Consent form - &__form { - padding: px2rem(16px); - } - - // Consent settings - &__settings { - display: none; - margin: 1em 0; - - // Show settings - input:checked + & { - display: block; - } - } - - // Consent controls - &__controls { - margin-bottom: px2rem(16px); - - // Consent control button - .md-typeset & .md-button { - display: inline; - - // [tablet +]: Align buttons horizontally - @include break-to-device(mobile) { - display: block; - width: 100%; - margin-top: px2rem(8px); - text-align: center; - } - } - } - - // Ensure users realize that labels are clickaböe - label { - cursor: pointer; - } -} diff --git a/src/templates/assets/stylesheets/main/components/_content.scss b/src/templates/assets/stylesheets/main/components/_content.scss deleted file mode 100644 index 7c945749..00000000 --- a/src/templates/assets/stylesheets/main/components/_content.scss +++ /dev/null @@ -1,97 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Content area -.md-content { - flex-grow: 1; - // Hack: we must use `min-width: 0`, so the content area is capped by the - // dimensions of its parent. Otherwise, long code blocks might lead to a - // wider content area which will overflow. See https://bit.ly/3bP3f8k - min-width: 0; - - // Content wrapper - &__inner { - padding-top: px2rem(12px); - margin: 0 px2rem(16px) px2rem(24px); - - // [screen +]: Adjust spacing between content area and sidebars - @include break-from-device(screen) { - - // Sidebar with navigation is visible - .md-sidebar--primary:not([hidden]) ~ .md-content > & { - margin-inline-start: px2rem(24px); - } - - // Sidebar with table of contents is visible - .md-sidebar--secondary:not([hidden]) ~ .md-content > & { - margin-inline-end: px2rem(24px); - } - } - - // Hack: add pseudo element for spacing, as the overflow of the content - // container may not be hidden due to an imminent offset error on targets - &::before { - display: block; - height: px2rem(8px); - content: ""; - } - - // Adjust spacing on last child - > :last-child { - margin-bottom: 0; - } - } - - // Button inside of the content area - these buttons are meant for actions on - // a document-level, i.e. linking to related source code files, printing etc. - &__button { - float: inline-end; - padding: 0; - margin: px2rem(8px) 0; - margin-inline-start: px2rem(8px); - - // [print]: Hide buttons - @media print { - display: none; - } - - // Adjust default link color for icons - .md-typeset & { - color: var(--md-default-fg-color--lighter); - } - - // Align with body copy located next to icon - svg { - display: inline; - vertical-align: top; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: scaleX(-1); - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_dialog.scss b/src/templates/assets/stylesheets/main/components/_dialog.scss deleted file mode 100644 index 16782ede..00000000 --- a/src/templates/assets/stylesheets/main/components/_dialog.scss +++ /dev/null @@ -1,65 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Dialog -.md-dialog { - position: fixed; - bottom: px2rem(16px); - z-index: 4; - min-width: px2rem(222px); - padding: px2rem(8px) px2rem(12px); - pointer-events: none; - background-color: var(--md-default-fg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z3); - opacity: 0; - transition: - transform 0ms 400ms, - opacity 400ms; - transform: translateY(100%); - inset-inline-end: px2rem(16px); - - // [print]: Hide dialog - @media print { - display: none; - } - - // Active dialog - &--active { - pointer-events: initial; - opacity: 1; - transition: - transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1), - opacity 400ms; - transform: translateY(0); - } - - // Dialog wrapper - &__inner { - font-size: px2rem(14px); - color: var(--md-default-bg-color); - } -} diff --git a/src/templates/assets/stylesheets/main/components/_feedback.scss b/src/templates/assets/stylesheets/main/components/_feedback.scss deleted file mode 100644 index bbcd00e9..00000000 --- a/src/templates/assets/stylesheets/main/components/_feedback.scss +++ /dev/null @@ -1,110 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Was this page helpful? -.md-feedback { - margin: 2em 0 1em; - text-align: center; - - // Feedback fieldset - fieldset { - padding: 0; - margin: 0; - border: none; - } - - // Feedback title - &__title { - margin: 1em auto; - font-weight: 700; - } - - // Feedback wrapper - &__inner { - position: relative; - } - - // Feedback list - &__list { - position: relative; - display: flex; - flex-wrap: wrap; - align-content: baseline; - justify-content: center; - - // Feedback icon on hover - &:hover .md-icon:not(:disabled) { - color: var(--md-default-fg-color--lighter); - } - - // Adjust height after submission - :disabled & { - min-height: px2rem(36px); - } - } - - // Feedback icon - &__icon { - flex-shrink: 0; - margin: 0 px2rem(2px); - color: var(--md-default-fg-color--light); - cursor: pointer; - transition: color 125ms; - - // Feedback icon on hover - &:not(:disabled).md-icon:hover { - color: var(--md-accent-fg-color); - } - - // Feedback icon after submit - &:disabled { - color: var(--md-default-fg-color--lightest); - pointer-events: none; - } - } - - // Feedback note - &__note { - position: relative; - opacity: 0; - transition: - transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 150ms; - transform: translateY(px2rem(8px)); - - // Feedback note value - > * { - max-width: px2rem(320px); - margin: 0 auto; - } - - // Show after submission - :disabled & { - opacity: 1; - transform: translateY(0); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_footer.scss b/src/templates/assets/stylesheets/main/components/_footer.scss deleted file mode 100644 index 9fabc05b..00000000 --- a/src/templates/assets/stylesheets/main/components/_footer.scss +++ /dev/null @@ -1,201 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Footer -.md-footer { - color: var(--md-footer-fg-color); - background-color: var(--md-footer-bg-color); - - // [print]: Hide footer - @media print { - display: none; - } - - // Footer wrapper - &__inner { - justify-content: space-between; - padding: px2rem(4px); - overflow: auto; - - // Footer is visible - &:not([hidden]) { - display: flex; - } - } - - // Footer link to previous and next page - &__link { - display: flex; - // Hack: some browsers induce ellipsis on flex children that are set to - // `overflow: hidden` and `text-overflow: ellipsis`. Enforcing growth by - // a tiny factor seems to get rid of the ellipsis and renders the text as - // it should - see https://bit.ly/2ZUCXQ8 - flex-grow: 0.01; - align-items: end; - max-width: 100%; - margin-block: px2rem(20px) px2rem(8px); - overflow: hidden; - outline-color: var(--md-accent-fg-color); - transition: opacity 250ms; - - // Footer link on focus/hover - &:is(:focus, :hover) { - opacity: 0.7; - } - - // Adjust for right-to-left languages - [dir="rtl"] & svg { - transform: scaleX(-1); - } - - // [mobile -]: Adjust width to 25/75 and hide title - @include break-to-device(mobile) { - - // Footer link to previous page - &--prev { - flex-shrink: 0; - - // Hide footer title - .md-footer__title { - display: none; - } - } - } - - // Footer link to next page - &--next { - margin-inline-start: auto; - text-align: right; - - // Adjust for right-to-left languages - [dir="rtl"] & { - text-align: left; - } - } - } - - // Footer title - &__title { - flex-grow: 1; - max-width: calc(100% - #{px2rem(48px)}); - padding: 0 px2rem(20px); - margin-bottom: px2rem(14px); - font-size: px2rem(18px); - white-space: nowrap; - } - - // Footer link button - &__button { - padding: px2rem(8px); - margin: px2rem(4px); - } - - // Footer link direction (i.e. prev and next) - &__direction { - font-size: px2rem(12.8px); - opacity: 0.7; - } -} - -// Footer metadata -.md-footer-meta { - background-color: var(--md-footer-bg-color--dark); - - // Footer metadata wrapper - &__inner { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - padding: px2rem(4px); - } - - // Lighten color for non-hovered text links - html &.md-typeset a { - color: var(--md-footer-fg-color--light); - - // Text link on focus/hover - &:is(:focus, :hover) { - color: var(--md-footer-fg-color); - } - } -} - -// ---------------------------------------------------------------------------- - -// Copyright and theme information -.md-copyright { - width: 100%; - padding: px2rem(8px) 0; - margin: auto px2rem(12px); - font-size: px2rem(12.8px); - color: var(--md-footer-fg-color--lighter); - - // [tablet portrait +]: Show copyright and social links in one line - @include break-from-device(tablet portrait) { - width: auto; - } - - // Footer copyright highlight - this is the upper part of the copyright and - // theme information, which will include a darker color than the theme link - &__highlight { - color: var(--md-footer-fg-color--light); - } -} - -// ---------------------------------------------------------------------------- - -// Social links -.md-social { - display: inline-flex; - gap: px2rem(4px); - padding: px2rem(4px) 0 px2rem(12px); - margin: 0 px2rem(8px); - - // [tablet portrait +]: Show copyright and social links in one line - @include break-from-device(tablet portrait) { - padding: px2rem(12px) 0; - } - - // Footer social link - &__link { - display: inline-block; - width: px2rem(32px); - height: px2rem(32px); - text-align: center; - - // Adjust line-height to match height for correct alignment - &::before { - line-height: 1.9; - } - - // Fill icon with current color - svg { - max-height: px2rem(16px); - vertical-align: -25%; - fill: currentcolor; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_form.scss b/src/templates/assets/stylesheets/main/components/_form.scss deleted file mode 100644 index 49b59e42..00000000 --- a/src/templates/assets/stylesheets/main/components/_form.scss +++ /dev/null @@ -1,83 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Form button - .md-button { - display: inline-block; - padding: px2em(10px) px2em(32px); - font-weight: 700; - color: var(--md-primary-fg-color); - cursor: pointer; - border: px2rem(2px) solid currentcolor; - border-radius: px2rem(2px); - transition: - color 125ms, - background-color 125ms, - border-color 125ms; - - // Primary button - &--primary { - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color); - border-color: var(--md-primary-fg-color); - } - - // Button on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-bg-color); - background-color: var(--md-accent-fg-color); - border-color: var(--md-accent-fg-color); - } - } - - // Form input - .md-input { - height: px2rem(36px); - padding: 0 px2rem(12px); - font-size: px2rem(16px); - border-bottom: px2rem(2px) solid var(--md-default-fg-color--lighter); - border-start-start-radius: px2rem(2px); - border-start-end-radius: px2rem(2px); - box-shadow: var(--md-shadow-z1); - transition: - border 250ms, - box-shadow 250ms; - - // Input on focus/hover - &:is(:focus, :hover) { - border-bottom-color: var(--md-accent-fg-color); - box-shadow: var(--md-shadow-z2); - } - - // Stretch to full width - &--stretch { - width: 100%; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_header.scss b/src/templates/assets/stylesheets/main/components/_header.scss deleted file mode 100644 index e51f3f99..00000000 --- a/src/templates/assets/stylesheets/main/components/_header.scss +++ /dev/null @@ -1,270 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Header - by default, the header will be sticky and stay always on top of the -// viewport. If this behavior is not desired, just set `position: static`. -.md-header { - position: sticky; - inset-inline: 0; - top: 0; - z-index: 4; - display: block; - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color); - // Hack: reduce jitter by adding a transparent box shadow of the same size - // so the size of the layer doesn't change during animation - box-shadow: - 0 0 px2rem(4px) rgba(0, 0, 0, 0), - 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0); - - // [print]: Hide header - @media print { - display: none; - } - - // Header is hidden - &[hidden] { - transition: - transform 250ms cubic-bezier(0.8, 0, 0.6, 1), - box-shadow 250ms; - transform: translateY(-100%); - } - - // Header in shadow state, i.e. shadow is visible - &--shadow { - box-shadow: - 0 0 px2rem(4px) rgba(0, 0, 0, 0.1), - 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2); - transition: - transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), - box-shadow 250ms; - } - - // Header wrapper - &__inner { - display: flex; - align-items: center; - padding: 0 px2rem(4px); - } - - // Header button - &__button { - position: relative; - z-index: 1; - padding: px2rem(8px); - margin: px2rem(4px); - color: currentcolor; - vertical-align: middle; - cursor: pointer; - outline-color: var(--md-accent-fg-color); - transition: opacity 250ms; - - // Button on hover - &:hover { - opacity: 0.7; - } - - // Header button is visible - &:not([hidden]) { - display: inline-block; - } - - // Hide outline for pointer devices - &:not(.focus-visible) { - outline: none; - -webkit-tap-highlight-color: transparent; - } - - // Button with logo, pointing to `config.site_url` - &.md-logo { - padding: px2rem(8px); - margin: px2rem(4px); - - // [tablet -]: Hide button - @include break-to-device(tablet) { - display: none; - } - - // Image or icon - :is(img, svg) { - display: block; - width: auto; - height: px2rem(24px); - fill: currentcolor; - } - } - - // Button for search - &[for="__search"] { - - // [tablet landscape +]: Hide button - @include break-from-device(tablet landscape) { - display: none; - } - - // [no-js]: Hide button - .no-js & { - display: none; - } - - // Adjust for right-to-left languages - [dir="rtl"] & svg { - transform: scaleX(-1); - } - } - - // Button for drawer - &[for="__drawer"] { - - // [screen +]: Hide button - @include break-from-device(screen) { - display: none; - } - } - } - - // Header topic - &__topic { - position: absolute; - display: flex; - max-width: 100%; - white-space: nowrap; - transition: - transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 150ms; - - // Second header topic - title of the current page - & + & { - z-index: -1; - pointer-events: none; - opacity: 0; - transition: - transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), - opacity 150ms; - transform: translateX(px2rem(25px)); - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(px2rem(-25px)); - } - } - - // Adjust font weight of site title - &:first-child { - font-weight: 700; - } - } - - // Header title - &__title { - flex-grow: 1; - height: px2rem(48px); - margin-inline-start: px2rem(20px); - margin-inline-end: px2rem(8px); - font-size: px2rem(18px); - line-height: px2rem(48px); - - // Header title in active state, i.e. page title is visible - &--active .md-header__topic { - z-index: -1; - pointer-events: none; - opacity: 0; - transition: - transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1), - opacity 150ms; - transform: translateX(px2rem(-25px)); - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(px2rem(25px)); - } - - // Second header topic - title of the current page - + .md-header__topic { - z-index: 0; - pointer-events: initial; - opacity: 1; - transition: - transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 150ms; - transform: translateX(0); - } - } - - // Add ellipsis in case of overflowing text - > .md-header__ellipsis { - position: relative; - width: 100%; - height: 100%; - } - } - - // Header option - &__option { - display: flex; - flex-shrink: 0; - max-width: 100%; - white-space: nowrap; - transition: - max-width 0ms 250ms, - opacity 250ms 250ms; - - // Hide toggle when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - max-width: 0; - opacity: 0; - transition: - max-width 0ms, - opacity 0ms; - } - - // Hack: Firefox 117 introduces a bug where the browser scrolls the page by - // a small amount to the top every time the header button is focused. After - // investigating, we're confident that it seems to be caused by the input - // field being too close to the border - see https://t.ly/APO8l - > input { - bottom: 0; - } - } - - // Repository information container - &__source { - display: none; - - // [tablet landscape +]: Show repository information - @include break-from-device(tablet landscape) { - display: block; - width: px2rem(234px); - max-width: px2rem(234px); - margin-inline-start: px2rem(20px); - } - - // [screen +]: Adjust spacing of search bar - @include break-from-device(screen) { - margin-inline-start: px2rem(28px); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_meta.scss b/src/templates/assets/stylesheets/main/components/_meta.scss deleted file mode 100644 index aaeae8df..00000000 --- a/src/templates/assets/stylesheets/main/components/_meta.scss +++ /dev/null @@ -1,67 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Metadata -.md-meta { - font-size: px2rem(14px); - line-height: 1.3; - color: var(--md-default-fg-color--light); - - // Metadata list - &__list { - display: inline-flex; - flex-wrap: wrap; - padding: 0; - margin: 0; - list-style: none; - } - - // Metadata item separator - &__item:not(:last-child)::after { - margin-inline: px2rem(4px); - content: "·"; - } - - // Metadata link - &__link { - color: var(--md-typeset-a-color); - - // Metadata link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - } -} - -// Draft -.md-draft { - display: inline-block; - padding-inline: px2em(8px, 14px); - font-weight: 700; - color: hsla(255, 100%, 100%); - background-color: $clr-red-a400; - border-radius: px2em(2px); -} diff --git a/src/templates/assets/stylesheets/main/components/_nav.scss b/src/templates/assets/stylesheets/main/components/_nav.scss deleted file mode 100644 index 673918af..00000000 --- a/src/templates/assets/stylesheets/main/components/_nav.scss +++ /dev/null @@ -1,761 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Navigation variables -:root { - --md-nav-icon--prev: svg-load("material/arrow-left.svg"); - --md-nav-icon--next: svg-load("material/chevron-right.svg"); - --md-toc-icon: svg-load("material/table-of-contents.svg"); -} - -// ---------------------------------------------------------------------------- - -// Navigation -.md-nav { - font-size: px2rem(14px); - line-height: 1.3; - - // Navigation title - &__title { - display: block; - padding: 0 px2rem(12px); - overflow: hidden; - font-weight: 700; - color: var(--md-default-fg-color--light); - text-overflow: ellipsis; - - // Navigaton button - .md-nav__button { - display: none; - - // Stretch images based on height, as it's the smaller dimension - img { - width: auto; - height: 100%; - } - - // Button with logo, pointing to `config.site_url` - &.md-logo { - - // Image or icon - :is(img, svg) { - display: block; - width: auto; - max-width: 100%; - height: px2rem(48px); - object-fit: contain; - fill: currentcolor; - } - } - } - } - - // Navigation list - &__list { - padding: 0; - margin: 0; - list-style: none; - } - - // Navigation link - &__link { - display: flex; - gap: px2rem(8px); - align-items: flex-start; - margin-top: 0.625em; - transition: color 125ms; - scroll-snap-align: start; - - // Navigation link that was passed - &--passed { - color: var(--md-default-fg-color--light); - } - - // Active link - .md-nav__item &--active { - - // Also enable color transitions on inline code blocks - &, - code { - color: var(--md-typeset-a-color); - } - } - - // Navigation link title - .md-ellipsis { - // Hack: Safari exhibits a bug where the text will sometimes disappear - // and the element will become unclickable. Setting `position: relative` - // seems to fix the issue - see https://bit.ly/3HljM1T - position: relative; - } - - // Always align navigation icons to the end - .md-icon:last-child { - margin-inline-start: auto; - } - - // Navigation link icon - svg { - flex-shrink: 0; - height: 1.3em; - fill: currentcolor; - } - - // Navigation link on focus/hover - &:is([href], [for]):is(:focus, :hover) { - color: var(--md-accent-fg-color); - cursor: pointer; - } - - // Show outline for keyboard devices - &.focus-visible { - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(4px); - } - - // Navigation link for table of contents - .md-nav--primary &[for="__toc"] { - display: none; - - // Table of contents icon - .md-icon::after { - display: block; - width: 100%; - height: 100%; - mask-image: var(--md-toc-icon); - background-color: currentcolor; - } - - // Hide table of contents - ~ .md-nav { - display: none; - } - } - } - - // Navigation container (for section index pages) - &__container > .md-nav__link { - margin-top: 0; - - // Stretch first child - &:first-child { - flex-grow: 1; - // Hack: if a very long word is used, it can push the arrow out of sight. - // Setting this property contains the text - see https://t.ly/E02vp - min-width: 0; - } - } - - // Navigation icon - &__icon { - flex-shrink: 0; - } - - // Repository information container - &__source { - display: none; - } - - // [tablet -]: Layered navigation - @include break-to-device(tablet) { - - // Primary and nested navigation - &--primary, - &--primary & { - position: absolute; - inset-inline: 0; - top: 0; - z-index: 1; - display: flex; - flex-direction: column; - height: 100%; - background-color: var(--md-default-bg-color); - } - - // Primary navigation - &--primary { - - // Navigation title and item - :is(.md-nav__title, .md-nav__item) { - font-size: px2rem(16px); - line-height: 1.5; - } - - // Navigation title - .md-nav__title { - position: relative; - height: px2rem(112px); - padding: px2rem(60px) px2rem(16px) px2rem(4px); - line-height: px2rem(48px); - color: var(--md-default-fg-color--light); - white-space: nowrap; - cursor: pointer; - background-color: var(--md-default-fg-color--lightest); - - // Navigation icon - .md-nav__icon { - position: absolute; - top: px2rem(8px); - inset-inline-start: px2rem(8px); - display: block; - width: px2rem(24px); - height: px2rem(24px); - margin: px2rem(4px); - - // Navigation icon in link to previous level - &::after { - display: block; - width: 100%; - height: 100%; - content: ""; - background-color: currentcolor; - mask-image: var(--md-nav-icon--prev); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - } - - // Navigation list - ~ .md-nav__list { - overflow-y: auto; - touch-action: pan-y; - background-color: var(--md-default-bg-color); - box-shadow: - 0 px2rem(1px) 0 var(--md-default-fg-color--lightest) inset; - scroll-snap-type: y mandatory; - - // Omit border on first child - > :first-child { - border-top: 0; - } - } - - // Top-level navigation title - &[for="__drawer"] { - font-weight: 700; - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color); - } - - // Button with logo, pointing to `config.site_url` - .md-logo { - position: absolute; - inset-inline: px2rem(4px); - top: px2rem(4px); - display: block; - padding: px2rem(8px); - margin: px2rem(4px); - } - } - - // Navigation list - .md-nav__list { - flex: 1; - } - - // Navigation item - .md-nav__item { - border-top: px2rem(1px) solid var(--md-default-fg-color--lightest); - - // Navigation link in active navigation - &--active > .md-nav__link { - color: var(--md-typeset-a-color); - - // Navigation link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - } - } - - // Navigation link - .md-nav__link { - padding: px2rem(12px) px2rem(16px); - margin-top: 0; - - // Navigation link icon - svg { - margin-top: 0.1em; - } - - // Adjust spacing on nested link - > .md-nav__link { - padding: 0; - } - - // Navigation icon - .md-nav__icon { - width: px2rem(24px); - height: px2rem(24px); - margin-inline-end: px2rem(-4px); - font-size: px2rem(24px); - - // Navigation icon in link to next level - &::after { - display: block; - width: 100%; - height: 100%; - content: ""; - background-color: currentcolor; - mask-image: var(--md-nav-icon--next); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - } - } - - // Flip icon vertically - .md-nav__icon { - - // Adjust for right-to-left languages - [dir="rtl"] &::after { - transform: scale(-1); - } - } - - // Table of contents contained in primary navigation - .md-nav--secondary { - - // Navigation on level 2-6 - .md-nav { - position: static; - background-color: transparent; - - // Navigation link on level 3 - .md-nav__link { - padding-inline-start: px2rem(28px); - } - - // Navigation link on level 4 - .md-nav .md-nav__link { - padding-inline-start: px2rem(40px); - } - - // Navigation link on level 5 - .md-nav .md-nav .md-nav__link { - padding-inline-start: px2rem(52px); - } - - // Navigation link on level 6 - .md-nav .md-nav .md-nav .md-nav__link { - padding-inline-start: px2rem(64px); - } - } - } - } - - // Table of contents - &--secondary { - background-color: transparent; - } - - // Hide nested navigation - &__toggle ~ & { - display: flex; - opacity: 0; - transition: - transform 250ms cubic-bezier(0.8, 0, 0.6, 1), - opacity 125ms 50ms; - transform: translateX(100%); - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(-100%); - } - } - - // Show nested navigation when toggle is active - &__toggle:checked ~ & { - opacity: 1; - transition: - transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 125ms 125ms; - transform: translateX(0); - - // Navigation list - > .md-nav__list { - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - } - } - } - - // [tablet portrait -]: Layered navigation with table of contents - @include break-to-device(tablet portrait) { - - // Show link to table of contents - &--primary &__link[for="__toc"] { - display: flex; - - // Show table of contents icon - .md-icon::after { - content: ""; - } - - // Hide navigation link to current page - + .md-nav__link { - display: none; - } - - // Show table of contents - ~ .md-nav { - display: flex; - } - } - - // Repository information container - &__source { - display: block; - padding: 0 px2rem(4px); - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color--dark); - } - } - - // [tablet landscape]: Layered navigation with table of contents - @include break-at-device(tablet landscape) { - - // Show link to integrated table of contents - &--integrated &__link[for="__toc"] { - display: flex; - - // Show table of contents icon - .md-icon::after { - content: ""; - } - - // Hide navigation link to current page - + .md-nav__link { - display: none; - } - - // Show table of contents - ~ .md-nav { - display: flex; - } - } - } - - // [tablet landscape +]: Tree-like table of contents - @include break-from-device(tablet landscape) { - margin-bottom: px2rem(-8px); - - // Table of contents - &--secondary { - - // Navigation title - .md-nav__title { - position: sticky; - top: 0; - // Hack: because of the hack that we need to make .md-ellipsis work in - // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm - z-index: 1; - background: var(--md-default-bg-color); - box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); - - // Adjust snapping behavior - &[for="__toc"] { - scroll-snap-align: start; - } - - // Hide navigation icon - .md-nav__icon { - display: none; - } - } - - // Adjust spacing for navigation list - same reason as below - .md-nav__list { - padding-inline-start: px2rem(12px); - padding-bottom: px2rem(8px); - } - - // Adjust spacing for navigation link - before this change, we set spacing - // on the left and right of a navigation item, but this led to the problem - // of cropped focus outlines, because we must set `overflow: hidden` on - // the navigation list for smooth expand and collapse transitions. - .md-nav__item > .md-nav__link { - margin-inline-end: px2rem(8px); - } - } - } - - // [screen +]: Tree-like navigation - @include break-from-device(screen) { - margin-bottom: px2rem(-8px); - transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1); - - // Primary navigation - &--primary { - - // Navigation title - .md-nav__title { - position: sticky; - top: 0; - // Hack: because of the hack that we need to make .md-ellipsis work in - // Safari, we need to set `z-index` here as - see https://bit.ly/3s5M2jm - z-index: 1; - background: var(--md-default-bg-color); - box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); - - // Adjust snapping behavior - &[for="__drawer"] { - scroll-snap-align: start; - } - - // Hide navigation icon - .md-nav__icon { - display: none; - } - } - - // Adjust spacing for navigation list - same reason as below - .md-nav__list { - padding-inline-start: px2rem(12px); - padding-bottom: px2rem(8px); - } - - // Adjust spacing for navigation link - before this change, we set spacing - // on the left and right of a navigation item, but this led to the problem - // of cropped focus outlines, because we must set `overflow: hidden` on - // the navigation list for smooth expand and collapse transitions. - .md-nav__item > .md-nav__link { - margin-inline-end: px2rem(8px); - } - } - - // Hide nested navigation - &__toggle ~ & { - display: grid; - grid-template-rows: 0fr; - visibility: collapse; - opacity: 0; - transition: - grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), - opacity 250ms, - visibility 0ms 250ms; - - // Navigation list - > .md-nav__list { - overflow: hidden; - } - } - - // Show nested navigation when toggle is active or indeterminate - &__toggle:is(:checked, :indeterminate) ~ & { - grid-template-rows: 1fr; - visibility: visible; - opacity: 1; - transition: - grid-template-rows 250ms cubic-bezier(0.86, 0, 0.07, 1), - opacity 150ms 100ms, - visibility 0ms; - } - - // Hide navigation title in nested navigation - &__item--nested > & > &__title { - display: none; - } - - // Navigation section - &__item--section { - display: block; - margin: 1.25em 0; - - // Adjust spacing on last child - &:last-child { - margin-bottom: 0; - } - - // Show navigation link as title - > .md-nav__link { - font-weight: 700; - - // Make labels discernable from links - &[for] { - color: var(--md-default-fg-color--light); - } - - // Omit clicks if not a section index page - &:not(.md-nav__container) { - pointer-events: none; - } - - // Hide navigation icon - > [for], - .md-icon { - display: none; - } - } - - // Navigation - > .md-nav { - display: block; - margin-inline-start: px2rem(-12px); - visibility: visible; - opacity: 1; - - // Adjust spacing on next level item - > .md-nav__list > .md-nav__item { - padding: 0; - } - } - } - - // Navigation icon - &__icon { - width: px2rem(18px); - height: px2rem(18px); - border-radius: 100%; - transition: background-color 250ms; - - // Navigation icon on hover - &:hover { - background-color: var(--md-accent-fg-color--transparent); - } - - // Navigation icon content - &::after { - display: inline-block; - width: 100%; - height: 100%; - vertical-align: px2rem(-2px); - content: ""; - background-color: currentcolor; - border-radius: 100%; - transition: transform 250ms; - mask-image: var(--md-nav-icon--next); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: rotate(180deg); - } - - // Navigation icon - rotate icon when toggle is active or indeterminate - .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link &, - .md-nav__item--nested .md-nav__toggle:indeterminate ~ .md-nav__link & { - transform: rotate(90deg); - } - } - } - - // Modifier for when navigation tabs are rendered - &--lifted { - - // Hide site title - > .md-nav__title { - display: none; - } - - // Hide level 0 navigation items - > .md-nav__list > .md-nav__item { - display: none; - - // Active parent navigation item - &--active { - display: block; - - // Show navigation link as title - > .md-nav__link { - position: sticky; - top: 0; - z-index: 1; - margin-top: 0; - background: var(--md-default-bg-color); - box-shadow: 0 0 px2rem(8px) px2rem(8px) var(--md-default-bg-color); - - // Omit clicks if not a section index page - &:not(.md-nav__container) { - pointer-events: none; - } - } - - // Adjust spacing for navigation section - &.md-nav__item--section { - margin: 0; - } - } - - // Adjust spacing for nested navigation - > .md-nav { - margin-inline-start: px2rem(-12px); - } - - // Make labels discernable from links - > [for] { - color: var(--md-default-fg-color--light); - } - } - - // Hack: Always show active navigation tab on breakpoint screen, despite - // of checkbox being checked or not - see https://t.ly/Qc311 - .md-nav[data-md-level="1"] { - grid-template-rows: 1fr; - visibility: visible; - opacity: 1; - } - } - - // Modifier for when table of contents is rendered in primary navigation - &--integrated > .md-nav__list > .md-nav__item--active { - - // Add spacing to container for non-nested navigation items - &:not(.md-nav__item--nested) { - padding: 0 px2rem(12px); - - // Remove padding as it's given by container - > .md-nav__link { - padding: 0; - } - } - - // Show integrated table of contents - .md-nav--secondary { - display: block; - margin-bottom: 1.25em; - visibility: visible; - border-inline-start: px2rem(1px) solid var(--md-primary-fg-color); - opacity: 1; - - // Navigation list - > .md-nav__list { - padding-bottom: 0; - overflow: visible; - } - - // Hide table of contents title - > .md-nav__title { - display: none; - } - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_pagination.scss b/src/templates/assets/stylesheets/main/components/_pagination.scss deleted file mode 100644 index a010bf43..00000000 --- a/src/templates/assets/stylesheets/main/components/_pagination.scss +++ /dev/null @@ -1,85 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Pagination -.md-pagination { - display: flex; - gap: px2rem(8px); - align-items: center; - justify-content: center; - font-size: px2rem(16px); - font-weight: 700; - - // Pagination item - > * { - display: flex; - align-items: center; - justify-content: center; - min-width: px2rem(36px); - height: px2rem(36px); - text-align: center; - border-radius: px2rem(4px); - } - - // Active pagination item - &__current { - color: var(--md-default-fg-color--light); - background-color: var(--md-default-fg-color--lightest); - } - - // Pagination link - &__link { - transition: - color 125ms, - background-color 125ms; - - // Pagination link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - - // Pagination icon - svg { - color: var(--md-accent-fg-color); - } - } - - // Show outline for keyboard devices - &.focus-visible { - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(4px); - } - - // Pagination icon - svg { - display: block; - width: px2rem(24px); - max-height: 100%; - color: var(--md-default-fg-color--lighter); - fill: currentcolor; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_post.scss b/src/templates/assets/stylesheets/main/components/_post.scss deleted file mode 100644 index cf6ce019..00000000 --- a/src/templates/assets/stylesheets/main/components/_post.scss +++ /dev/null @@ -1,196 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Post -.md-post { - - // Post backlink - &__back { - padding-bottom: px2rem(24px); - margin-bottom: px2rem(24px); - border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); - - // [tablet -]: Hide post backlink - @include break-to-device(tablet) { - display: none; - } - - // Adjust for right-to-left languages - [dir="rtl"] & { - - // Flip icon vertically - svg { - transform: scaleX(-1); - } - } - } - - // Post authors - &__authors { - display: flex; - flex-direction: column; - gap: px2rem(12px); - margin: 0 px2rem(12px) px2rem(24px); - } - - // Post metadata - .md-post__meta { - - // Navigation link - a { - transition: color 125ms; - - // Navigation link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - } - } - - // Post navigation title @todo - generalize - &__title { - font-weight: 700; - color: var(--md-default-fg-color--light); - } - - // Post excerpt - &--excerpt { - margin-bottom: px2rem(64px); - - // Post excerpt header - .md-post__header { - display: flex; - gap: px2rem(12px); - align-items: center; - min-height: px2rem(32px); - } - - // Post excerpt authors - .md-post__authors { - display: inline-flex; - flex-direction: row; - gap: px2rem(4px); - align-items: center; - min-height: px2rem(48px); - margin: 0; - } - - // Post excerpt metadata - .md-post__meta .md-meta__list { - margin-inline-end: px2rem(8px); - } - - // Post excerpt content - .md-post__content > :first-child { - --md-scroll-margin: #{px2rem(120px)}; - - margin-top: 0; - } - } - - // Add margin to table of contents - > .md-nav--secondary { - margin: 1em 0; - } -} - -// ---------------------------------------------------------------------------- - -// Post author profile -.md-profile { - display: flex; - gap: px2rem(12px); - align-items: center; - width: 100%; - font-size: px2rem(14px); - line-height: 1.4; - - // Post author description - &__description { - flex-grow: 1; - } -} - -// ---------------------------------------------------------------------------- - -// Content area for post -.md-content--post { - display: flex; - - // [tablet -]: Switch to inverted column layout - @include break-to-device(tablet) { - flex-flow: column-reverse; - } - - // Content wrapper - > .md-content__inner { - min-width: 0; - - // [screen +]: Adjust spacing between content area and sidebars - @include break-from-device(screen) { - margin-inline-start: px2rem(24px); - } - } -} - -// Sidebar for post -.md-sidebar.md-sidebar--post { - - // [tablet -]: Adjust spacing - @include break-to-device(tablet) { - position: initial; - width: 100%; - padding: 0; - - .md-sidebar__inner { - padding: 0; - } - - .md-post__meta { - margin-inline: px2rem(12px); - } - - .md-nav__item { - display: inline; - border: none; - } - - .md-nav__list { - display: inline-flex; - flex-wrap: wrap; - gap: px2rem(12px); - padding-block: px2rem(12px); - } - - .md-nav__link { - padding: 0; - } - - .md-nav { - position: initial; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_progress.scss b/src/templates/assets/stylesheets/main/components/_progress.scss deleted file mode 100644 index 7386ae33..00000000 --- a/src/templates/assets/stylesheets/main/components/_progress.scss +++ /dev/null @@ -1,53 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Progress variables -:root { - --md-progress-value: 0; - --md-progress-delay: 400ms; -} - -// ---------------------------------------------------------------------------- - -// Progress indicator -.md-progress { - position: fixed; - top: 0; - z-index: 4; - width: 100%; - height: px2rem(1.5px); - background: var(--md-primary-bg-color); - opacity: - min( - clamp(0, var(--md-progress-value), 1), - clamp(0, 100 - var(--md-progress-value), 1) - ); - transition: - transform 500ms cubic-bezier(0.19, 1, 0.22, 1), - opacity 250ms var(--md-progress-delay); - transform: scaleX(calc(var(--md-progress-value) * 1%)); - transform-origin: left; -} diff --git a/src/templates/assets/stylesheets/main/components/_search.scss b/src/templates/assets/stylesheets/main/components/_search.scss deleted file mode 100644 index e0f36b0c..00000000 --- a/src/templates/assets/stylesheets/main/components/_search.scss +++ /dev/null @@ -1,707 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Search variables -:root { - --md-search-result-icon: svg-load("material/file-search-outline.svg"); -} - -// ---------------------------------------------------------------------------- - -// Search -.md-search { - position: relative; - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - padding: px2rem(4px) 0; - } - - // [no-js]: Hide search - .no-js & { - display: none; - } - - // Search overlay - &__overlay { - z-index: 1; - opacity: 0; - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - position: absolute; - top: px2rem(-20px); - width: px2rem(40px); - height: px2rem(40px); - overflow: hidden; - pointer-events: none; - background-color: var(--md-default-bg-color); - border-radius: px2rem(20px); - transition: - transform 300ms 100ms, - opacity 200ms 200ms; - transform-origin: center; - inset-inline-start: px2rem(-44px); - - // Show overlay when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - opacity: 1; - transition: - transform 400ms, - opacity 100ms; - } - } - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - position: fixed; - top: 0; - width: 0; - height: 0; - cursor: pointer; - background-color: hsla(0, 0%, 0%, 0.54); - transition: - width 0ms 250ms, - height 0ms 250ms, - opacity 250ms; - inset-inline-start: 0; - - // Show overlay when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - width: 100%; - // Hack: when the header is translated upon scrolling, a new layer is - // induced, which means that the height will now refer to the height of - // the header, albeit positioning is fixed. This should be mitigated - // in all cases when setting the height to 2x the viewport. - height: 200vh; - opacity: 1; - transition: - width 0ms, - height 0ms, - opacity 250ms; - } - } - - // Adjust appearance when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - - // [mobile portrait -]: Scale up 45 times - @include break-to-device(mobile portrait) { - transform: scale(45); - } - - // [mobile landscape]: Scale up 60 times - @include break-at-device(mobile landscape) { - transform: scale(60); - } - - // [tablet portrait]: Scale up 75 times - @include break-at-device(tablet portrait) { - transform: scale(75); - } - } - } - - // Search wrapper - &__inner { - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - position: fixed; - top: 0; - z-index: 2; - width: 0; - height: 0; - overflow: hidden; - opacity: 0; - transition: - width 0ms 300ms, - height 0ms 300ms, - transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 150ms 150ms; - transform: translateX(5%); - inset-inline-start: 0; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(-5%); - } - - // Adjust appearance when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - width: 100%; - height: 100%; - opacity: 1; - transition: - width 0ms 0ms, - height 0ms 0ms, - transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 150ms 150ms; - transform: translateX(0); - } - } - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - position: relative; - float: inline-end; - width: px2rem(234px); - padding: px2rem(2px) 0; - transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1); - } - - // Adjust appearance when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - - // [tablet landscape]: Omit overlaying header title - @include break-at-device(tablet landscape) { - width: px2rem(468px); - } - - // [screen +]: Match width of content area - @include break-from-device(screen) { - width: px2rem(688px); - } - } - } - - // Search form - &__form { - position: relative; - z-index: 2; - height: px2rem(48px); - background-color: var(--md-default-bg-color); - box-shadow: 0 0 px2rem(12px) transparent; - transition: - color 250ms, - background-color 250ms; - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - height: px2rem(36px); - background-color: hsla(0, 0%, 0%, 0.26); - border-radius: px2rem(2px); - - // Search form on hover - &:hover { - background-color: hsla(0, 0%, 100%, 0.12); - } - } - - // Adjust appearance when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - color: var(--md-default-fg-color); - background-color: var(--md-default-bg-color); - border-radius: px2rem(2px) px2rem(2px) 0 0; - box-shadow: 0 0 px2rem(12px) hsla(0, 0%, 0%, 0.07); - } - } - - // Search input - &__input { - position: relative; - z-index: 2; - width: 100%; - height: 100%; - padding-inline: px2rem(72px) px2rem(44px); - font-size: px2rem(18px); - text-overflow: ellipsis; - background: transparent; - - // Search placeholder - &::placeholder { - transition: color 250ms; - } - - // Search icon and placeholder - ~ .md-search__icon, - &::placeholder { - color: var(--md-default-fg-color--light); - } - - // Remove the "x" rendered by Internet Explorer - &::-ms-clear { - display: none; - } - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - width: 100%; - height: px2rem(48px); - font-size: px2rem(18px); - } - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - font-size: px2rem(16px); - color: inherit; - - // Search placeholder - &::placeholder { - color: var(--md-primary-bg-color--light); - } - - // Search icon - + .md-search__icon { - color: var(--md-primary-bg-color); - } - - // Adjust appearance when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - text-overflow: clip; - - // Search icon and placeholder - + .md-search__icon { - color: var(--md-default-fg-color--light); - } - - // Search placeholder - &::placeholder { - color: transparent; - } - } - } - } - - // Search icon - &__icon { - display: inline-block; - width: px2rem(24px); - height: px2rem(24px); - cursor: pointer; - transition: - color 250ms, - opacity 250ms; - - // Search icon on hover - &:hover { - opacity: 0.7; - } - - // Search focus button - &[for="__search"] { - position: absolute; - top: px2rem(6px); - inset-inline-start: px2rem(10px); - z-index: 2; - - // Adjust for right-to-left languages - [dir="rtl"] & svg { - transform: scaleX(-1); - } - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - top: px2rem(12px); - inset-inline-start: px2rem(16px); - - // Hide the magnifying glass - svg:first-child { - display: none; - } - } - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - pointer-events: none; - - // Hide the back arrow - svg:last-child { - display: none; - } - } - } - } - - // Search options - &__options { - position: absolute; - top: px2rem(6px); - inset-inline-end: px2rem(10px); - z-index: 2; - pointer-events: none; - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - top: px2rem(12px); - inset-inline-end: px2rem(16px); - } - - // Search option buttons - > .md-icon { - margin-inline-start: px2rem(4px); - color: var(--md-default-fg-color--light); - opacity: 0; - transition: - transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 150ms; - transform: scale(0.75); - - // Hide outline for pointer devices - &:not(.focus-visible) { - outline: none; - -webkit-tap-highlight-color: transparent; - } - - // Show buttons when search is active and input non-empty - [data-md-toggle="search"]:checked ~ .md-header // stylelint-disable-line - .md-search__input:valid ~ & { - pointer-events: initial; - opacity: 1; - transform: scale(1); - - // Search focus icon - &:hover { - opacity: 0.7; - } - } - } - } - - // Search suggestions - &__suggest { - position: absolute; - top: 0; - display: flex; - align-items: center; - width: 100%; - height: 100%; - padding-inline: px2rem(72px) px2rem(44px); - font-size: px2rem(18px); - color: var(--md-default-fg-color--lighter); - white-space: nowrap; - opacity: 0; - transition: opacity 50ms; - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - font-size: px2rem(16px); - } - - // Show suggestions when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - opacity: 1; - transition: opacity 300ms 100ms; - } - } - - // Search output - &__output { - position: absolute; - z-index: 1; - width: 100%; - overflow: hidden; - border-end-start-radius: px2rem(2px); - border-end-end-radius: px2rem(2px); - - // [tablet portrait -]: Search modal - @include break-to-device(tablet portrait) { - top: px2rem(48px); - bottom: 0; - } - - // [tablet landscape +]: Header-embedded search - @include break-from-device(tablet landscape) { - top: px2rem(38px); - opacity: 0; - transition: opacity 400ms; - - // Show output when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - box-shadow: var(--md-shadow-z3); - opacity: 1; - } - } - } - - // Search scroll wrapper - &__scrollwrap { - height: 100%; - overflow-y: auto; - // Hack: Chrome 88+ has weird overscroll behavior. Overall, scroll snapping - // seems to be something that is not ready for prime time on some browsers. - // scroll-snap-type: y mandatory; - touch-action: pan-y; - background-color: var(--md-default-bg-color); - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - - // Mitigiate excessive repaints on non-retina devices - @media (max-resolution: 1dppx) { - transform: translateZ(0); - } - - // [tablet landscape]: Set fixed width to omit unnecessary reflow - @include break-at-device(tablet landscape) { - width: px2rem(468px); - } - - // [screen +]: Set fixed width to omit unnecessary reflow - @include break-from-device(screen) { - width: px2rem(688px); - } - - // [tablet landscape +]: Limit height to viewport - @include break-from-device(tablet landscape) { - max-height: 0; - scrollbar-width: thin; - scrollbar-color: var(--md-default-fg-color--lighter) transparent; - - // Show scroll wrapper when search is active - [data-md-toggle="search"]:checked ~ .md-header & { - max-height: 75vh; - } - - // Search scroll wrapper on hover - &:hover { - scrollbar-color: var(--md-accent-fg-color) transparent; - } - - // Webkit scrollbar - &::-webkit-scrollbar { - width: px2rem(4px); - height: px2rem(4px); - } - - // Webkit scrollbar thumb - &::-webkit-scrollbar-thumb { - background-color: var(--md-default-fg-color--lighter); - - // Webkit scrollbar thumb on hover - &:hover { - background-color: var(--md-accent-fg-color); - } - } - } - } -} - -// Search result -.md-search-result { - color: var(--md-default-fg-color); - word-break: break-word; - - // Search result metadata - &__meta { - padding: 0 px2rem(16px); - font-size: px2rem(12.8px); - line-height: px2rem(36px); - color: var(--md-default-fg-color--light); - background-color: var(--md-default-fg-color--lightest); - scroll-snap-align: start; - - // [tablet landscape +]: Adjust spacing - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - } - } - - // Search result list - &__list { - padding: 0; - margin: 0; - list-style: none; - // Hack: omit accidental text selection on fast toggle of more button - user-select: none; - } - - // Search result item - &__item { - box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest); - - // Omit border on first child - &:first-child { - box-shadow: none; - } - } - - // Search result link - &__link { - display: block; - outline: none; - transition: background-color 250ms; - scroll-snap-align: start; - - // Search result link on focus/hover - &:is(:focus, :hover) { - background-color: var(--md-accent-fg-color--transparent); - } - - // Adjust spacing on last child of last link - &:last-child p:last-child { - margin-bottom: px2rem(12px); - } - } - - // Search result more container - &__more > summary { - position: sticky; - top: 0; - z-index: 1; - display: block; - cursor: pointer; - outline: none; - scroll-snap-align: start; - - // Hide native details marker - &::marker { - display: none; - } - - // Hide native details marker - legacy, must be split into a seprate rule, - // so older browsers don't consider the selector list as invalid - &::-webkit-details-marker { - display: none; - } - - // Search result more button - > div { - padding: px2em(12px) px2rem(16px); - font-size: px2rem(12.8px); - color: var(--md-typeset-a-color); - transition: - color 250ms, - background-color 250ms; - - // [tablet landscape +]: Adjust spacing - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - } - } - - // Search result more link on focus/hover - &:is(:focus, :hover) > div { - color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - } - } - - // Adjust background for more container in open state - &__more[open] > summary { - background-color: var(--md-default-bg-color); - // box-shadow: 0 px2rem(-1px) hsla(0, 0%, 0%, 0.07) inset; - } - - // Search result article - &__article { - position: relative; - padding: 0 px2rem(16px); - overflow: hidden; - - // [tablet landscape +]: Adjust spacing - @include break-from-device(tablet landscape) { - padding-inline-start: px2rem(44px); - } - } - - // Search result icon - &__icon { - position: absolute; - inset-inline-start: 0; - width: px2rem(24px); - height: px2rem(24px); - margin: px2rem(10px); - color: var(--md-default-fg-color--light); - - // [tablet portrait -]: Hide icon - @include break-to-device(tablet portrait) { - display: none; - } - - // Search result icon content - &::after { - display: inline-block; - width: 100%; - height: 100%; - content: ""; - background-color: currentcolor; - mask-image: var(--md-search-result-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: scaleX(-1); - } - } - } - - // Typesetted content - .md-typeset { - font-size: px2rem(12.8px); - line-height: 1.6; - color: var(--md-default-fg-color--light); - - // Search result article title - h1 { - margin: px2rem(11px) 0; - font-size: px2rem(16px); - font-weight: 400; - line-height: 1.4; - color: var(--md-default-fg-color); - - // Search term highlighting - mark { - text-decoration: none; - } - } - - // Search result section title - h2 { - margin: 0.5em 0; - font-size: px2rem(12.8px); - font-weight: 700; - line-height: 1.6; - color: var(--md-default-fg-color); - - // Search term highlighting - mark { - text-decoration: none; - } - } - } - - // Search result terms - &__terms { - display: block; - margin: 0.5em 0; - font-size: px2rem(12.8px); - font-style: italic; - color: var(--md-default-fg-color); - } - - // Search term highlighting - mark { - color: var(--md-accent-fg-color); - text-decoration: underline; - background-color: transparent; - } -} diff --git a/src/templates/assets/stylesheets/main/components/_select.scss b/src/templates/assets/stylesheets/main/components/_select.scss deleted file mode 100644 index ed597a39..00000000 --- a/src/templates/assets/stylesheets/main/components/_select.scss +++ /dev/null @@ -1,115 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Selection -.md-select { - position: relative; - z-index: 1; - - // Selection tooltip - &__inner { - position: absolute; - top: calc(100% - #{px2rem(4px)}); - left: 50%; - max-height: 0; - margin-top: px2rem(4px); - color: var(--md-default-fg-color); - background-color: var(--md-default-bg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z2); - opacity: 0; - transition: - transform 250ms 375ms, - opacity 250ms 250ms, - max-height 0ms 500ms; - transform: translate3d(-50%, px2rem(6px), 0); - - // Selection bubble on parent focus/hover - .md-select:is(:focus-within, :hover) & { - max-height: px2rem(200px); - opacity: 1; - transition: - transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 250ms, - max-height 0ms; - transform: translate3d(-50%, 0, 0); - } - - // Selection bubble handle - &::after { - position: absolute; - top: 0; - left: 50%; - width: 0; - height: 0; - margin-top: px2rem(-4px); - margin-left: px2rem(-4px); - content: ""; - border: px2rem(4px) solid transparent; - border-top: 0; - border-bottom-color: var(--md-default-bg-color); - } - } - - // Selection list - &__list { - max-height: inherit; - padding: 0; - margin: 0; - overflow: auto; - font-size: px2rem(16px); - list-style-type: none; - border-radius: px2rem(2px); - } - - // Selection item - &__item { - line-height: px2rem(36px); - } - - // Selection link - &__link { - display: block; - width: 100%; - padding-inline: px2rem(12px) px2rem(24px); - cursor: pointer; - outline: none; - transition: - background-color 250ms, - color 250ms; - scroll-snap-align: start; - - // Link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - - // Link on focus - &:focus { - background-color: var(--md-default-fg-color--lightest); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_sidebar.scss b/src/templates/assets/stylesheets/main/components/_sidebar.scss deleted file mode 100644 index 8a320c04..00000000 --- a/src/templates/assets/stylesheets/main/components/_sidebar.scss +++ /dev/null @@ -1,209 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Sidebar -.md-sidebar { - position: sticky; - top: px2rem(48px); - flex-shrink: 0; - align-self: flex-start; - width: px2rem(242px); - padding: px2rem(24px) 0; - - // [print]: Hide sidebar - @media print { - display: none; - } - - // Primary sidebar with navigation - &--primary { - - // [tablet -]: Show navigation as drawer - @include break-to-device(tablet) { - position: fixed; - top: 0; - z-index: 5; - display: block; - width: px2rem(242px); - height: 100%; - background-color: var(--md-default-bg-color); - transition: - transform 250ms cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 250ms; - transform: translateX(0); - inset-inline-start: px2rem(-242px); - - // Show sidebar when drawer is active - [data-md-toggle="drawer"]:checked ~ .md-container & { - box-shadow: var(--md-shadow-z3); - transform: translateX(px2rem(242px)); - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(px2rem(-242px)); - } - } - - // Stretch scroll wrapper for primary sidebar - .md-sidebar__scrollwrap { - position: absolute; - inset: 0; - margin: 0; - scroll-snap-type: none; - overflow: hidden; - } - } - } - - // [screen +]: Show navigation as sidebar - @include break-from-device(screen) { - height: 0; - - // [no-js]: Switch to native sticky behavior - .no-js & { - height: auto; - } - - // Adjust spacing for sticky navigation tabs - .md-header--lifted ~ .md-container & { - top: px2rem(96px); - } - } - - // Secondary sidebar with table of contents - &--secondary { - display: none; - order: 2; - - // [tablet landscape +]: Show table of contents as sidebar - @include break-from-device(tablet landscape) { - height: 0; - - // [no-js]: Switch to native sticky behavior - .no-js & { - height: auto; - } - - // Sidebar is visible - &:not([hidden]) { - display: block; - } - - // Ensure smooth scrolling on iOS - .md-sidebar__scrollwrap { - touch-action: pan-y; - } - } - } - - // Sidebar scroll wrapper - &__scrollwrap { - margin: 0 px2rem(4px); - overflow-y: auto; - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - // Hack: Chrome 81+ exhibits a strange bug, where it scrolls the container - // to the bottom if `scroll-snap-type` is set on the initial render. For - // this reason, we disable scroll snapping until this is resolved (#1667). - // scroll-snap-type: y mandatory; - scrollbar-width: thin; - scrollbar-gutter: stable; - scrollbar-color: var(--md-default-fg-color--lighter) transparent; - - // Webkit scrollbar - &::-webkit-scrollbar { - width: px2rem(4px); - height: px2rem(4px); - } - - // Sidebar scroll wrapper on focus/hover - &:is(:focus-within, :hover) { - scrollbar-color: var(--md-accent-fg-color) transparent; - - // Webkit scrollbar thumb - &::-webkit-scrollbar-thumb { - background-color: var(--md-default-fg-color--lighter); - - // Webkit scrollbar thumb on hover - &:hover { - background-color: var(--md-accent-fg-color); - } - } - } - } - - // Hack: the scrollbar is only visible when the sidebar's contents overflow, - // which is nice, but leads to the problem where the chevrons of expandable - // sections will jump by `4px` when the sidebar is shown. We wanted to fix - // this problem for so long, but haven't found a clean way of doing it. - // Until now. The following declaration is only applied to Webkit browsers - // (e.g. Chrome and Safari), which support styling of scrollbars. The trick - // is to add conditional padding on the side of the scrollbar only if the - // sidebar's content doesn't overflow. This hack is inspired and adapted - // from Ayke van Laëthem's year old trick – see https://bit.ly/3Sb1qql - @supports selector(::-webkit-scrollbar) { - - // Sidebar scroll wrapper - &__scrollwrap { - scrollbar-gutter: auto; - } - - // Sidebar wrapper - &__inner { - padding-inline-end: calc(100% - #{px2rem(230px)}); - } - } -} - -// [tablet -]: Show overlay on active drawer -@include break-to-device(tablet) { - - // Drawer overlay - .md-overlay { - position: fixed; - top: 0; - z-index: 5; - width: 0; - height: 0; - background-color: hsla(0, 0%, 0%, 0.54); - opacity: 0; - transition: - width 0ms 250ms, - height 0ms 250ms, - opacity 250ms; - - // Show overlay when drawer is active - [data-md-toggle="drawer"]:checked ~ & { - width: 100%; - height: 100%; - opacity: 1; - transition: - width 0ms, - height 0ms, - opacity 250ms; - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_source.scss b/src/templates/assets/stylesheets/main/components/_source.scss deleted file mode 100644 index a2b72009..00000000 --- a/src/templates/assets/stylesheets/main/components/_source.scss +++ /dev/null @@ -1,182 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Keyframes -// ---------------------------------------------------------------------------- - -// Show repository facts -@keyframes facts { - 0% { - height: 0; - } - - 100% { - height: px2rem(13px); - } -} - -// Show repository fact -@keyframes fact { - 0% { - opacity: 0; - transform: translateY(100%); - } - - 50% { - opacity: 0; - } - - 100% { - opacity: 1; - transform: translateY(0%); - } -} - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Repository information variables -:root { - --md-source-forks-icon: svg-load("octicons/repo-forked-16.svg"); - --md-source-repositories-icon: svg-load("octicons/repo-16.svg"); - --md-source-stars-icon: svg-load("octicons/star-16.svg"); - --md-source-version-icon: svg-load("octicons/tag-16.svg"); -} - -// ---------------------------------------------------------------------------- - -// Repository information -.md-source { - display: block; - font-size: px2rem(13px); - line-height: 1.2; - white-space: nowrap; - outline-color: var(--md-accent-fg-color); - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - transition: opacity 250ms; - - // Repository information on hover - &:hover { - opacity: 0.7; - } - - // Repository icon - &__icon { - display: inline-block; - width: px2rem(40px); - height: px2rem(48px); - vertical-align: middle; - - // Align with margin only (as opposed to normal button alignment) - svg { - margin-inline-start: px2rem(12px); - margin-top: px2rem(12px); - } - - // Adjust spacing if icon is present - + .md-source__repository { - padding-inline-start: px2rem(40px); - margin-inline-start: px2rem(-40px); - } - } - - // Repository name - &__repository { - display: inline-block; - max-width: calc(100% - #{px2rem(24px)}); - margin-inline-start: px2rem(12px); - overflow: hidden; - text-overflow: ellipsis; - vertical-align: middle; - } - - // Repository facts - &__facts { - display: flex; - gap: px2rem(8px); - width: 100%; - padding: 0; - margin: px2rem(2px) 0 0; - overflow: hidden; - font-size: px2rem(11px); - list-style-type: none; - opacity: 0.75; - - // Show after the data was loaded - .md-source__repository--active & { - animation: facts 250ms ease-in; - } - } - - // Repository fact - &__fact { - overflow: hidden; - text-overflow: ellipsis; - - // Show after the data was loaded - .md-source__repository--active & { - animation: fact 400ms ease-out; - } - - // Repository fact icon - &::before { - display: inline-block; - width: px2rem(12px); - height: px2rem(12px); - margin-inline-end: px2rem(2px); - vertical-align: text-top; - content: ""; - background-color: currentcolor; - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Adjust spacing for 2nd+ fact - &:nth-child(1n+2) { - flex-shrink: 0; - } - - // Repository fact: version - &--version::before { - mask-image: var(--md-source-version-icon); - } - - // Repository fact: stars - &--stars::before { - mask-image: var(--md-source-stars-icon); - } - - // Repository fact: forks - &--forks::before { - mask-image: var(--md-source-forks-icon); - } - - // Repository fact: repositories - &--repositories::before { - mask-image: var(--md-source-repositories-icon); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_status.scss b/src/templates/assets/stylesheets/main/components/_status.scss deleted file mode 100644 index 9e096021..00000000 --- a/src/templates/assets/stylesheets/main/components/_status.scss +++ /dev/null @@ -1,73 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Status variables -:root { - --md-status: svg-load("material/information-outline.svg"); - --md-status--new: svg-load("material/alert-decagram.svg"); - --md-status--deprecated: svg-load("material/trash-can.svg"); - --md-status--encrypted: svg-load("material/shield-lock.svg"); -} - -// ---------------------------------------------------------------------------- - -// Status -.md-status { - - // Status icon - &::after { - display: inline-block; - width: px2em(18px); - height: px2em(18px); - vertical-align: text-bottom; - content: ""; - background-color: var(--md-default-fg-color--light); - mask-image: var(--md-status); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Status icon on hover - &:hover::after { - background-color: currentcolor; - } - - // Status: new - &--new::after { - mask-image: var(--md-status--new); - } - - // Status: deprecated - &--deprecated::after { - mask-image: var(--md-status--deprecated); - } - - // Status: encrypted - &--encrypted::after { - mask-image: var(--md-status--encrypted); - } -} diff --git a/src/templates/assets/stylesheets/main/components/_tabs.scss b/src/templates/assets/stylesheets/main/components/_tabs.scss deleted file mode 100644 index 0da3384b..00000000 --- a/src/templates/assets/stylesheets/main/components/_tabs.scss +++ /dev/null @@ -1,133 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Navigation tabs -.md-tabs { - // Must be higher than the z-index of the back-to-top button, or the button - // will overlay the navigation tabs bar when scrolling up fast. - z-index: 3; - display: block; - width: 100%; - overflow: auto; - line-height: 1.3; - color: var(--md-primary-bg-color); - background-color: var(--md-primary-fg-color); - - // [print]: Hide tabs - @media print { - display: none; - } - - // [tablet -]: Hide tabs - @include break-to-device(tablet) { - display: none; - } - - // Navigation tabs are hidden - &[hidden] { - pointer-events: none; - } - - // Navigation tabs list - &__list { - display: flex; - padding: 0; - margin: 0; - margin-inline-start: px2rem(4px); - overflow: auto; - white-space: nowrap; - list-style: none; - contain: content; - // Hack: don't show scrollbar when navigation tabs overflow, which should - // only happen in rare occasions, as adding too many top level sections is - // discouraged, since hiding content on horitontal axis doesn't lead to a - // good user experience. It's just harder to discover. - scrollbar-width: none; - - // Hack: see above - &::-webkit-scrollbar { - display: none; - } - } - - // Navigation tabs item - &__item { - height: px2rem(48px); - padding-inline: px2rem(12px); - - // Navigation tabs link in active navigation - &--active .md-tabs__link { - color: inherit; - opacity: 1; - } - } - - // Navigation tabs link - could be defined as block elements and aligned via - // line height, but this would imply more repaints when scrolling - &__link { - display: flex; - margin-top: px2rem(16px); - font-size: px2rem(14px); - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(4px); - // Hack: save a repaint when tabs are appearing on scrolling up - backface-visibility: hidden; - opacity: 0.7; - transition: - transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 250ms; - - // Navigation tabs link on focus/hover - &:is(:focus, :hover) { - color: inherit; - opacity: 1; - } - - // Navigation tabs link icon - svg { - height: 1.3em; - margin-inline-end: px2rem(8px); - fill: currentcolor; - } - - // Delay transitions by a small amount - @for $i from 2 through 16 { - .md-tabs__item:nth-child(#{$i}) & { - transition-delay: 20ms * ($i - 1); - } - } - - // Hide tabs upon scrolling - disable transition to minimizes repaints - // while scrolling down, while scrolling up seems to be okay - .md-tabs[hidden] & { - opacity: 0; - transition: - transform 0ms 100ms, - opacity 100ms; - transform: translateY(50%); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_tag.scss b/src/templates/assets/stylesheets/main/components/_tag.scss deleted file mode 100644 index 9f31829d..00000000 --- a/src/templates/assets/stylesheets/main/components/_tag.scss +++ /dev/null @@ -1,105 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Tag variables -:root { - --md-tag-icon: svg-load("material/pound.svg"); -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Tag list - .md-tags { - display: inline-flex; - flex-wrap: wrap; - gap: px2em(8px); - margin-top: px2em(-2px); - margin-bottom: px2em(12px); - } - - // Tag - .md-tag { - display: inline-flex; - gap: px2em(8px); - align-items: center; - padding: px2em(4px, 12.8px) px2em(10px, 12.8px); - font-size: px2rem(12.8px); // Fallback - font-size: min(px2em(12.8px), px2rem(12.8px)); - font-weight: 700; - line-height: 1.6; - letter-spacing: initial; - background: var(--md-default-fg-color--lightest); - border-radius: px2rem(48px); - - // Linked tag - &[href] { - color: inherit; - outline: none; - -webkit-tap-highlight-color: transparent; - transition: - color 125ms, - background-color 125ms; - - // Linked tag on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-bg-color); - background-color: var(--md-accent-fg-color); - } - } - - // Tag inside headline - [id] > & { - vertical-align: text-top; - } - } - - // Tag icon - .md-tag-icon { - - // Tag icon content - &::before { - display: inline-block; - width: 1.2em; - height: 1.2em; - vertical-align: text-bottom; - content: ""; - background-color: var(--md-default-fg-color--lighter); - transition: background-color 125ms; - mask-image: var(--md-tag-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Linked tag on focus/hover - &[href]:is(:focus, :hover)::before { - background-color: var(--md-accent-bg-color); - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_tooltip.scss b/src/templates/assets/stylesheets/main/components/_tooltip.scss deleted file mode 100644 index 421e5858..00000000 --- a/src/templates/assets/stylesheets/main/components/_tooltip.scss +++ /dev/null @@ -1,292 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Keyframes -// ---------------------------------------------------------------------------- - -// Continuous pulse animation -@keyframes pulse { - 0% { - transform: scale(0.95); - } - - 75% { - transform: scale(1); - } - - 100% { - transform: scale(0.95); - } -} - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Tooltip variables -:root { - --md-annotation-bg-icon: svg-load("material/circle.svg"); - --md-annotation-icon: svg-load("material/plus-circle.svg"); - --md-tooltip-width: #{px2rem(400px)}; -} - -// ---------------------------------------------------------------------------- - -// Tooltip -.md-tooltip { - position: absolute; - top: var(--md-tooltip-y); - left: - clamp( - var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)}, - var(--md-tooltip-x), - 100vw + - var(--md-tooltip-0, #{px2rem(0px)}) + #{px2rem(16px)} - - var(--md-tooltip-width) - - 2 * #{px2rem(16px)} - ); - // Hack: set an explicit `z-index` so we can transition it to ensure that any - // following elements are not overlaying the tooltip during the transition. - z-index: 0; - width: var(--md-tooltip-width); - max-width: calc(100vw - 2 * #{px2rem(16px)}); - font-family: var(--md-text-font-family); - color: var(--md-default-fg-color); - background-color: var(--md-default-bg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z2); - opacity: 0; - transition: - transform 0ms 250ms, - opacity 250ms, - z-index 250ms; - transform: translateY(px2rem(-8px)); - // Hack: promote to own layer to reduce jitter - backface-visibility: hidden; - - // Active tooltip - &--active { - z-index: 2; - opacity: 1; - transition: - transform 250ms cubic-bezier(0.1, 0.7, 0.1, 1), - opacity 250ms, - z-index 0ms; - transform: translateY(0); - } - - // Show outline on target and for keyboard devices - :is(.focus-visible > &, &:target) { - outline: var(--md-accent-fg-color) auto; - } - - // Tooltip wrapper - &__inner { - padding: px2rem(16px); - font-size: px2rem(12.8px); - - // Adjust spacing on first child - &.md-typeset > :first-child { - margin-top: 0; - } - - // Adjust spacing on last child - &.md-typeset > :last-child { - margin-bottom: 0; - } - } -} - -// ---------------------------------------------------------------------------- - -// Annotation -.md-annotation { - font-weight: 400; - white-space: normal; - vertical-align: text-bottom; - outline: none; - - // Adjust for right-to-left languages - [dir="rtl"] & { - direction: rtl; - } - - // Annotation index in code block - code & { - font-family: var(--md-code-font-family); - font-size: inherit; - } - - // Annotation is not hidden (e.g. when copying) - &:not([hidden]) { - display: inline-block; - // Hack: ensure that the line height doesn't exceed the line height of the - // hosting line, because it will lead to dancing pixels. - line-height: 1.25; - } - - // Annotation index - &__index { - position: relative; - z-index: 0; - display: inline-block; - margin-inline: 0.4ch; - vertical-align: text-top; - cursor: pointer; - user-select: none; - outline: none; - - // Hack: increase specificity to override default for anchors in typesetted - // content, because transitions are defined on anchor elements - .md-annotation & { - transition: z-index 250ms; - } - - // Hack: Work around Firefox bug that renders a subpixel outline when - // rotating a mask image element. - // https://bugzilla.mozilla.org/show_bug.cgi?id=1671784 - overflow: hidden; // stylelint-disable-line order/properties-order - border-radius: 0.01px; - - // [screen]: Render annotation markers as icons - @media screen { - width: 2.2ch; - - // Annotation is visible - [data-md-visible] > & { - animation: pulse 2000ms infinite; - } - - // Annotation marker background - &::before { - position: absolute; - top: -0.1ch; - z-index: -1; - width: 2.2ch; - height: 2.2ch; - content: ""; - background: var(--md-default-bg-color); - mask-image: var(--md-annotation-bg-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Annotation marker – the marker must be positioned absolutely behind - // the index, because it shouldn't impact the rendering of a code block. - // Otherwise, small rounding differences in browsers can sometimes mess up - // alignment of text following an annotation. - &::after { - position: absolute; - top: -0.1ch; - z-index: -1; - width: 2.2ch; - height: 2.2ch; - content: ""; - background-color: var(--md-default-fg-color--lighter); - transition: - background-color 250ms, - transform 250ms; - // Hack: promote to own layer to reduce jitter - transform: scale(1.0001); - mask-image: var(--md-annotation-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - // Annotation marker for active tooltip - .md-tooltip--active + & { - transform: rotate(45deg); - } - - // Annotation marker for active tooltip or on hover - :is(.md-tooltip--active + &, :hover > &) { - background-color: var(--md-accent-fg-color); - } - } - } - - // Annotation index for active tooltip - .md-tooltip--active + & { - z-index: 2; - transition-duration: 0ms; - animation-play-state: paused; - } - - // Annotation marker - [data-md-annotation-id] { - display: inline-block; - - // [print]: Render annotation markers as numbers - @media print { - padding: 0 0.6ch; - font-weight: 700; - color: var(--md-default-bg-color); - white-space: nowrap; - background: var(--md-default-fg-color--lighter); - border-radius: 2ch; - - // Annotation marker content - &::after { - content: attr(data-md-annotation-id); - } - } - } - } -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Annotation list - .md-annotation-list { - list-style: none; - counter-reset: xxx; - - // Annotation list item - li { - position: relative; - - // Annotation list marker - &::before { - position: absolute; - top: px2em(4px); - inset-inline-start: px2em(-34px); - min-width: 2ch; - height: 2ch; - padding: 0 0.6ch; - font-size: px2em(14.2px); - font-weight: 700; - line-height: 1.25; - color: var(--md-default-bg-color); - text-align: center; - content: counter(xxx); - counter-increment: xxx; - background: var(--md-default-fg-color--lighter); - border-radius: 2ch; - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/components/_top.scss b/src/templates/assets/stylesheets/main/components/_top.scss deleted file mode 100644 index c24d44d1..00000000 --- a/src/templates/assets/stylesheets/main/components/_top.scss +++ /dev/null @@ -1,83 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Back-to-top button -.md-top { - position: fixed; - top: px2rem(48px + 16px); - z-index: 2; - display: block; - padding: px2rem(8px) px2rem(16px); - margin-inline-start: 50%; - font-size: px2rem(14px); - color: var(--md-default-fg-color--light); - cursor: pointer; - background-color: var(--md-default-bg-color); - border-radius: px2rem(32px); - outline: none; - box-shadow: var(--md-shadow-z2); - transition: - color 125ms, - background-color 125ms, - transform 125ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 125ms; - transform: translate(-50%, 0); - - // [print]: Hide back-to-top button - @media print { - display: none; - } - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translate(50%, 0); - } - - // Back-to-top button is hidden - &[hidden] { - pointer-events: none; - opacity: 0; - transition-duration: 0ms; - transform: translate(-50%, px2rem(4px)); - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translate(50%, px2rem(4px)); - } - } - - // Back-to-top button on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-bg-color); - background-color: var(--md-accent-fg-color); - } - - // Inline icon - svg { - display: inline-block; - vertical-align: -0.5em; - } -} diff --git a/src/templates/assets/stylesheets/main/components/_version.scss b/src/templates/assets/stylesheets/main/components/_version.scss deleted file mode 100644 index 3f85d6cd..00000000 --- a/src/templates/assets/stylesheets/main/components/_version.scss +++ /dev/null @@ -1,150 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Keyframes -// ---------------------------------------------------------------------------- - -// See https://github.com/squidfunk/mkdocs-material/issues/2429 -@keyframes hoverfix { - 0% { - pointer-events: none; - } -} - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Version selection variables -:root { - --md-version-icon: svg-load("fontawesome/solid/caret-down.svg"); -} - -// ---------------------------------------------------------------------------- - -// Version selection -.md-version { - flex-shrink: 0; - height: px2rem(48px); - font-size: px2rem(16px); - - // Current selection - &__current { - position: relative; - // Hack: in general, we would use `vertical-align` to align the version at - // the bottom with the title, but since the list uses absolute positioning, - // this won't work consistently. Furthermore, we would need to use inline - // positioning to align the links, which looks jagged. - top: px2rem(1px); - margin-inline: px2rem(28px) px2rem(8px); - color: inherit; - cursor: pointer; - outline: none; - - // Version selection icon - &::after { - display: inline-block; - width: px2rem(8px); - height: px2rem(12px); - margin-inline-start: px2rem(8px); - content: ""; - background-color: currentcolor; - mask-image: var(--md-version-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - } - - // Version selection list - &__list { - position: absolute; - top: px2rem(3px); - z-index: 3; - max-height: 0; - padding: 0; - margin: px2rem(4px) px2rem(16px); - overflow: auto; - color: var(--md-default-fg-color); - list-style-type: none; - background-color: var(--md-default-bg-color); - border-radius: px2rem(2px); - box-shadow: var(--md-shadow-z2); - opacity: 0; - transition: - max-height 0ms 500ms, - opacity 250ms 250ms; - scroll-snap-type: y mandatory; - - // Version selection list on parent focus/hover - .md-version:is(:focus-within, :hover) & { - max-height: px2rem(200px); - opacity: 1; - transition: - max-height 0ms, - opacity 250ms; - } - - // Fix hover on touch devices - @media (pointer: coarse), (hover: none) { - // Switch off on hover - .md-version:hover & { - animation: hoverfix 250ms forwards; - } - - // Enable on focus - .md-version:focus-within & { - animation: none; - } - } - } - - // Version selection item - &__item { - line-height: px2rem(36px); - } - - // Version selection link - &__link { - display: block; - width: 100%; - padding-inline: px2rem(12px) px2rem(24px); - white-space: nowrap; - cursor: pointer; - outline: none; - transition: - color 250ms, - background-color 250ms; - scroll-snap-align: start; - - // Link on focus/hover - &:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - - // Link on focus - &:focus { - background-color: var(--md-default-fg-color--lightest); - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss deleted file mode 100644 index bf517989..00000000 --- a/src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss +++ /dev/null @@ -1,195 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -@use "sass:color"; -@use "sass:list"; - -// ---------------------------------------------------------------------------- -// Variables -// ---------------------------------------------------------------------------- - -/// Admonition flavours -$admonitions: ( - "note": pencil-circle $clr-blue-a200, - "abstract": clipboard-text $clr-light-blue-a400, - "info": information $clr-cyan-a700, - "tip": fire $clr-teal-a700, - "success": check $clr-green-a700, - "question": help-circle $clr-light-green-a700, - "warning": alert $clr-orange-a400, - "failure": close $clr-red-a200, - "danger": lightning-bolt-circle $clr-red-a400, - "bug": shield-bug $clr-pink-a400, - "example": test-tube $clr-deep-purple-a200, - "quote": format-quote-close $clr-grey -) !default; - -// ---------------------------------------------------------------------------- -// Rules: layout -// ---------------------------------------------------------------------------- - -// Admonition variables -:root { - @each $name, $props in $admonitions { - --md-admonition-icon--#{$name}: - svg-load("material/#{list.nth($props, 1)}.svg"); - } -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Admonition - note that all styles also apply to details tags, which are - // rendered as collapsible admonitions with summary elements as titles. - .admonition { - display: flow-root; - padding: 0 px2rem(12px); - margin: px2em(20px, 12.8px) 0; - font-size: px2rem(12.8px); - color: var(--md-admonition-fg-color); - background-color: var(--md-admonition-bg-color); - border: px2rem(1.5px) solid $clr-blue-a200; - border-radius: px2rem(4px); - box-shadow: var(--md-shadow-z1); - transition: box-shadow 125ms; - page-break-inside: avoid; - - // [print]: Omit shadow as it may lead to rendering errors - @media print { - box-shadow: none; - } - - // Admonition on focus - &:focus-within { - box-shadow: 0 0 0 px2rem(4px) color.adjust($clr-blue-a200, $alpha: -0.9); - } - - // Hack: Chrome exhibits a weird issue where it will set nested elements to - // content-box. Doesn't happen in other browsers, so looks like a bug. - > * { - box-sizing: border-box; - } - - // Adjust vertical spacing for nested admonitions - .admonition { - margin-top: 1em; - margin-bottom: 1em; - } - - // Adjust spacing for contained table wrappers - .md-typeset__scrollwrap { - margin: 1em px2rem(-12px); - } - - // Adjust spacing for contained tables - .md-typeset__table { - padding: 0 px2rem(12px); - } - - // Adjust spacing for single-child tabbed block container - > .tabbed-set:only-child { - margin-top: 0; - } - - // Adjust spacing on last child - html & > :last-child { - margin-bottom: px2rem(12px); - } - } - - // Admonition title - .admonition-title { - position: relative; - padding-block: px2rem(8px); - padding-inline: px2rem(40px) px2rem(12px); - margin-block: 0; - margin-inline: px2rem(-12px); - font-weight: 700; - background-color: color.adjust($clr-blue-a200, $alpha: -0.9); - border: none; - border-inline-start-width: px2rem(4px); - border-start-start-radius: px2rem(2px); - border-start-end-radius: px2rem(2px); - - // Adjust spacing for title-only admonitions - html &:last-child { - margin-bottom: 0; - } - - // Admonition icon - &::before { - position: absolute; - top: px2em(10px); - width: px2rem(20px); - height: px2rem(20px); - content: ""; - background-color: $clr-blue-a200; - inset-inline-start: px2rem(12px); - mask-image: var(--md-admonition-icon--note); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Inline code block - code { - box-shadow: 0 0 0 px2rem(1px) var(--md-default-fg-color--lightest); - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: flavours -// ---------------------------------------------------------------------------- - -// Define admonition flavors -@each $name, $props in $admonitions { - $tint: list.nth($props, 2); - - // Admonition flavour - .md-typeset .admonition.#{$name} { - border-color: $tint; - - // Admonition on focus - &:focus-within { - box-shadow: 0 0 0 px2rem(4px) color.adjust($tint, $alpha: -0.9); - } - } - - // Admonition flavour title - .md-typeset .#{$name} > .admonition-title { - background-color: color.adjust($tint, $alpha: -0.9); - - // Admonition icon - &::before { - background-color: $tint; - mask-image: var(--md-admonition-icon--#{$name}); - } - - // Details marker - &::after { - color: $tint; - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss deleted file mode 100644 index 59447d89..00000000 --- a/src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss +++ /dev/null @@ -1,146 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Footnotes variables -:root { - --md-footnotes-icon: svg-load("material/keyboard-return.svg"); -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Footnote container - .footnote { - font-size: px2rem(12.8px); - color: var(--md-default-fg-color--light); - - // Footnote list - omit left indentation - > ol { - margin-inline-start: 0; - - // Footnote item - footnote items can contain lists, so we need to scope - // the spacing adjustments to the top-level footnote item. - > li { - transition: color 125ms; - - // Darken color on target - &:target { - color: var(--md-default-fg-color); - } - - // Show backreferences on footnote focus without transition - &:focus-within .footnote-backref { - opacity: 1; - transition: none; - transform: translateX(0); - } - - // Show backreferences on footnote hover/target - &:is(:hover, :target) .footnote-backref { - opacity: 1; - transform: translateX(0); - } - - // Adjust spacing on first child - > :first-child { - margin-top: 0; - } - } - } - } - - // Footnote reference - .footnote-ref { - font-size: px2em(12px, 16px); - font-weight: 700; - - // Hack: increase specificity to override default - html & { - outline-offset: px2rem(2px); - } - } - - // Show outline for all devices - [id^="fnref:"]:target > .footnote-ref { - outline: auto; - } - - // Footnote backreference - .footnote-backref { - display: inline-block; - // Hack: omit Unicode arrow for replacement with icon - font-size: 0; - color: var(--md-typeset-a-color); - vertical-align: text-bottom; - opacity: 0; - transition: - color 250ms, - transform 250ms 250ms, - opacity 125ms 250ms; - transform: translateX(px2rem(5px)); - - // [print]: Show footnote backreferences - @media print { - color: var(--md-typeset-a-color); - opacity: 1; - transform: translateX(0); - } - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: translateX(px2rem(-5px)); - } - - // Adjust color on hover - &:hover { - color: var(--md-accent-fg-color); - } - - // Footnote backreference icon - &::before { - display: inline-block; - width: px2rem(16px); - height: px2rem(16px); - content: ""; - background-color: currentcolor; - mask-image: var(--md-footnotes-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - // Adjust for right-to-left languages - [dir="rtl"] & { - - // Flip icon vertically - svg { - transform: scaleX(-1); - } - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss b/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss deleted file mode 100644 index 8284a5c0..00000000 --- a/src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss +++ /dev/null @@ -1,92 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Headerlink - .headerlink { - display: inline-block; - margin-inline-start: px2rem(10px); - color: var(--md-default-fg-color--lighter); - opacity: 0; - transition: - color 250ms, - opacity 125ms; - - // [print]: Hide headerlinks - @media print { - display: none; - } - } - - // Show headerlinks on parent hover - :is(:hover, :target) > .headerlink, - .headerlink:focus { - opacity: 1; - transition: - color 250ms, - opacity 125ms; - } - - // Adjust color on parent target or focus/hover - :target > .headerlink, - .headerlink:is(:focus, :hover) { - color: var(--md-accent-fg-color); - } - - // Adjust scroll margin for all elements with `id` attributes - :target { - --md-scroll-margin: #{px2rem(48px + 24px)}; - --md-scroll-offset: #{px2rem(0px)}; - // Scroll margin is finally ready for prime time - before, we used a hack - // for anchor correction based on pseudo elements but those times are gone. - scroll-margin-top: - calc( - var(--md-scroll-margin) - - var(--md-scroll-offset) - ); - - // [screen +]: Sticky navigation tabs - @include break-from-device(screen) { - - // Adjust scroll margin for sticky navigation tabs - .md-header--lifted ~ .md-container & { - --md-scroll-margin: #{px2rem(96px + 24px)}; - } - } - } - - // Adjust scroll offset for headlines of level 1-3 - :is(h1, h2, h3):target { - --md-scroll-offset: #{px2rem(4px)}; - } - - // Adjust scroll offset for headlines of level 4 - h4:target { - --md-scroll-offset: #{px2rem(3px)}; - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss deleted file mode 100644 index fe8ffd62..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss +++ /dev/null @@ -1,52 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Arithmatex container - div.arithmatex { - overflow: auto; - - // [mobile -]: Align with body copy - @include break-to-device(mobile) { - margin: 0 px2rem(-16px); - } - - // Arithmatex content - > * { - width: min-content; - padding: 0 px2rem(16px); - margin-inline: auto !important; // stylelint-disable-line - touch-action: auto; - - // MathJax container - see https://bit.ly/3HR8YJ5 - mjx-container { - margin: 0 !important; // stylelint-disable-line - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss deleted file mode 100644 index 683705ce..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss +++ /dev/null @@ -1,76 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Deletion - del.critic { - background-color: var(--md-typeset-del-color); - box-decoration-break: clone; - } - - // Addition - ins.critic { - background-color: var(--md-typeset-ins-color); - box-decoration-break: clone; - } - - // Comment - .critic.comment { - color: var(--md-code-hl-comment-color); - box-decoration-break: clone; - - // Comment opening mark - &::before { - content: "/* "; - } - - // Comment closing mark - &::after { - content: " */"; - } - } - - // Critic block - .critic.block { - display: block; - padding-inline: px2rem(16px); - margin: 1em 0; - overflow: auto; - box-shadow: none; - - // Adjust spacing on first child - > :first-child { - margin-top: 0.5em; - } - - // Adjust spacing on last child - > :last-child { - margin-bottom: 0.5em; - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss deleted file mode 100644 index 8eea678a..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss +++ /dev/null @@ -1,121 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Details variables -:root { - --md-details-icon: svg-load("material/chevron-right.svg"); -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Details - details { - @extend .admonition; - - display: flow-root; - padding-top: 0; - overflow: visible; - - // Details title icon - rotate icon on transition to open state - &[open] > summary::after { - transform: rotate(90deg); - } - - // Adjust spacing for details in closed state - &:not([open]) { - padding-bottom: 0; - box-shadow: none; - - // Hack: we cannot set `overflow: hidden` on the `details` element (which - // is why we set it to `overflow: visible`, as the outline would not be - // visible when focusing. Therefore, we must set the border radius on the - // summary explicitly. - > summary { - border-radius: px2rem(2px); - } - } - } - - // Details title - summary { - @extend .admonition-title; - - display: block; - min-height: px2rem(20px); - padding-inline-end: px2rem(36px); - cursor: pointer; - border-start-start-radius: px2rem(2px); - border-start-end-radius: px2rem(2px); - - // Show outline for keyboard devices - &.focus-visible { - outline-color: var(--md-accent-fg-color); - outline-offset: px2rem(4px); - } - - // Hide outline for pointer devices - &:not(.focus-visible) { - outline: none; - -webkit-tap-highlight-color: transparent; - } - - // Details marker - &::after { - position: absolute; - top: px2em(10px); - width: px2rem(20px); - height: px2rem(20px); - content: ""; - background-color: currentcolor; - transition: transform 250ms; - transform: rotate(0deg); - inset-inline-end: px2rem(8px); - mask-image: var(--md-details-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: rotate(180deg); - } - } - - // Hide native details marker - modern - &::marker { - display: none; - } - - // Hide native details marker - legacy, must be split into a seprate rule, - // so older browsers don't consider the selector list as invalid - &::-webkit-details-marker { - display: none; - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss deleted file mode 100644 index 8b351013..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss +++ /dev/null @@ -1,43 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Emoji and icon container - :is(.emojione, .twemoji, .gemoji) { - display: inline-flex; - height: px2em(18px); - vertical-align: text-top; - - // Icon - inlined via mkdocs-material-extensions - svg { - width: px2em(18px); - max-height: 100%; - fill: currentcolor; - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss deleted file mode 100644 index 7d297677..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss +++ /dev/null @@ -1,382 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules: syntax highlighting -// ---------------------------------------------------------------------------- - -// Code block -.highlight { - - // .o = Operator - // .ow = Operator, word - :is(.o, .ow) { - color: var(--md-code-hl-operator-color); - } - - .p { // Punctuation - color: var(--md-code-hl-punctuation-color); - } - - // .cpf = Comment, preprocessor file - // .l = Literal - // .s = Literal, string - // .sb = Literal, string backticks - // .sc = Literal, string char - // .s2 = Literal, string double - // .si = Literal, string interpol - // .s1 = Literal, string single - // .ss = Literal, string symbol - :is(.cpf, .l, .s, .sb, .sc, .s2, .si, .s1, .ss) { - color: var(--md-code-hl-string-color); - } - - // .cp = Comment, pre-processor - // .se = Literal, string escape - // .sh = Literal, string heredoc - // .sr = Literal, string regex - // .sx = Literal, string other - :is(.cp, .se, .sh, .sr, .sx) { - color: var(--md-code-hl-special-color); - } - - // .m = Number - // .mb = Number, binary - // .mf = Number, float - // .mh = Number, hex - // .mi = Number, integer - // .il = Number, integer long - // .mo = Number, octal - :is(.m, .mb, .mf, .mh, .mi, .il, .mo) { - color: var(--md-code-hl-number-color); - } - - // .k = Keyword, - // .kd = Keyword, declaration - // .kn = Keyword, namespace - // .kp = Keyword, pseudo - // .kr = Keyword, reserved - // .kt = Keyword, type - :is(.k, .kd, .kn, .kp, .kr, .kt) { - color: var(--md-code-hl-keyword-color); - } - - // .kc = Keyword, constant - // .n = Name - :is(.kc, .n) { - color: var(--md-code-hl-name-color); - } - - // .no = Name, constant - // .nb = Name, builtin - // .bp = Name, builtin pseudo - :is(.no, .nb, .bp) { - color: var(--md-code-hl-constant-color); - } - - // .nc = Name, class - // .ne = Name, exception - // .nf = Name, function - // .nn = Name, namespace - :is(.nc, .ne, .nf, .nn) { - color: var(--md-code-hl-function-color); - } - - // .nd = Name, decorator - // .ni = Name, entity - // .nl = Name, label - // .nt = Name, tag - :is(.nd, .ni, .nl, .nt) { - color: var(--md-code-hl-keyword-color); - } - - // .c = Comment - // .cm = Comment, multiline - // .c1 = Comment, single - // .ch = Comment, shebang - // .cs = Comment, special - // .sd = Literal, string doc - :is(.c, .cm, .c1, .ch, .cs, .sd) { - color: var(--md-code-hl-comment-color); - } - - // .na = Name, attribute - // .nv = Variable, - // .vc = Variable, class - // .vg = Variable, global - // .vi = Variable, instance - :is(.na, .nv, .vc, .vg, .vi) { - color: var(--md-code-hl-variable-color); - } - - // .ge = Generic, emph - // .gr = Generic, error - // .gh = Generic, heading - // .go = Generic, output - // .gp = Generic, prompt - // .gs = Generic, strong - // .gu = Generic, subheading - // .gt = Generic, traceback - :is(.ge, .gr, .gh, .go, .gp, .gs, .gu, .gt) { - color: var(--md-code-hl-generic-color); - } - - // .gd = Diff, delete - // .gi = Diff, insert - :is(.gd, .gi) { - padding: 0 px2em(2px); - margin: 0 px2em(-2px); - border-radius: px2rem(2px); - } - - .gd { // Diff, delete - background-color: var(--md-typeset-del-color); - } - - .gi { // Diff, insert - background-color: var(--md-typeset-ins-color); - } - - // Highlighted line - .hll { - display: block; - padding: 0 px2em(16px, 13.6px); - margin: 0 px2em(-16px, 13.6px); - background-color: var(--md-code-hl-color--light); - box-shadow: 2px 0 0 0 var(--md-code-hl-color) inset; - } - - // Code block title - span.filename { - position: relative; - display: flow-root; - padding: px2em(9px, 13.6px) px2em(16px, 13.6px); - margin-top: 1em; - font-size: px2em(13.6px); - font-weight: 700; - background-color: var(--md-code-bg-color); - border-bottom: px2rem(1px) solid var(--md-default-fg-color--lightest); - border-top-left-radius: px2rem(2px); - border-top-right-radius: px2rem(2px); - - // Adjust spacing for code block - + pre { - margin-top: 0; - - // Remove rounded border on top side - > code { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - } - } - - // Code block line numbers (pymdownx-inline) - [data-linenos]::before { - position: sticky; - left: px2em(-16px, 13.6px); - // A `z-index` of 3 is necessary for ensuring that code block annotations - // don't overlay line numbers, as active annotations have a `z-index` of 2. - z-index: 3; - float: left; - padding-left: px2em(16px, 13.6px); - margin-right: px2em(16px, 13.6px); - margin-left: px2em(-16px, 13.6px); - color: var(--md-default-fg-color--light); - content: attr(data-linenos); - user-select: none; - background-color: var(--md-code-bg-color); - box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; - } - - // Code block line anchors - Chrome and Safari seem to have a strange bug - // where scroll margin is not applied to anchors inside code blocks. Setting - // positioning to absolute seems to fix the problem. Interestingly, this does - // not happen in Firefox. Furthermore we must set `visibility: hidden` or - // the copy to clipboard functionality will include an empty line between - // each set of lines. - code a[id] { - position: absolute; - visibility: hidden; - } - - // Copying in progress - this class is set before the content is copied and - // removed after copying is done to mitigate whitespace-related issues. - code[data-md-copying] { - - // Temporarily remove highlighted lines - see https://bit.ly/32iVGWh - .hll { - display: contents; - } - - // Temporarily remove annotations - .md-annotation { - display: none; - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: layout -// ---------------------------------------------------------------------------- - -// Code block with line numbers -.highlighttable { - display: flow-root; - - // Set table elements to block layout, because otherwise the whole flexbox - // hacking won't work correctly - :is(tbody, td) { - display: block; - padding: 0; - } - - // We need to use flexbox layout, because otherwise it's not possible to - // make the code container scroll while keeping the line numbers static - tr { - display: flex; - } - - // The pre tags are nested inside a table, so we need to omit the margin - // because it collapses below all the overflows - pre { - margin: 0; - } - - // Code block title container - th.filename { - flex-grow: 1; - padding: 0; - text-align: left; - - // Adjust spacing - span.filename { - margin-top: 0; - } - } - - // Code block line numbers - disable user selection, so code can be easily - // copied without accidentally also copying the line numbers - .linenos { - padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px); - padding-right: 0; - font-size: px2em(13.6px); - user-select: none; - background-color: var(--md-code-bg-color); - border-top-left-radius: px2rem(2px); - border-bottom-left-radius: px2rem(2px); - } - - // Code block line numbers container - .linenodiv { - padding-right: px2em(8px, 13.6px); - box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lightest) inset; - - // Adjust colors and alignment - pre { - color: var(--md-default-fg-color--light); - text-align: right; - } - } - - // Code block container - stretch to remaining space - .code { - flex: 1; - min-width: 0; - } -} - -// Code block line numbers container -.linenodiv a { - color: inherit; -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Code block with line numbers - unfortunately, these selectors need to be - // overly specific so they don't bleed into code blocks in annotations. - .highlighttable { - margin: 1em 0; - direction: ltr; - - // Remove rounded borders on code blocks - > tbody > tr > .code > div > pre > code { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - } - - // Code block result container - .highlight + .result { - padding: 0 px2em(16px); - margin-top: calc(-1em + #{px2em(-2px)}); - overflow: visible; - border: px2rem(1px) solid var(--md-code-bg-color); - border-top-width: px2rem(2px); - border-bottom-right-radius: px2rem(2px); - border-bottom-left-radius: px2rem(2px); - - // Clearfix, because we can't use overflow: auto - &::after { - display: block; - clear: both; - content: ""; - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: top-level -// ---------------------------------------------------------------------------- - -// [mobile -]: Align with body copy -@include break-to-device(mobile) { - - // Top-level code block - .md-content__inner > .highlight { - margin: 1em px2rem(-16px); - - // Remove rounded borders - > .filename, - > pre > code { - border-radius: 0; - } - - // Code block with line numbers - unfortunately, these selectors need to be - // overly specific so they don't bleed into code blocks in annotations. - > .highlighttable > tbody > tr > .filename span.filename, - > .highlighttable > tbody > tr > .linenos, - > .highlighttable > tbody > tr > .code > div > pre > code { - border-radius: 0; - } - - // Code block result container - + .result { - margin-inline: px2rem(-16px); - border-inline-width: 0; - border-radius: 0; - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss deleted file mode 100644 index 8749f08c..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss +++ /dev/null @@ -1,115 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Keyboard key - .keys { - - // Keyboard key icon - kbd:is(::before, ::after) { - position: relative; - margin: 0; - color: inherit; - -moz-osx-font-smoothing: initial; - -webkit-font-smoothing: initial; - } - - // Surrounding text - span { - padding: 0 px2em(3.2px); - color: var(--md-default-fg-color--light); - } - - // Define keyboard keys with left icon - @each $name, $code in ( - - // Modifiers - "alt": "\2387", - "left-alt": "\2387", - "right-alt": "\2387", - "command": "\2318", - "left-command": "\2318", - "right-command": "\2318", - "control": "\2303", - "left-control": "\2303", - "right-control": "\2303", - "meta": "\25C6", - "left-meta": "\25C6", - "right-meta": "\25C6", - "option": "\2325", - "left-option": "\2325", - "right-option": "\2325", - "shift": "\21E7", - "left-shift": "\21E7", - "right-shift": "\21E7", - "super": "\2756", - "left-super": "\2756", - "right-super": "\2756", - "windows": "\229E", - "left-windows": "\229E", - "right-windows": "\229E", - - // Other keys - "arrow-down": "\2193", - "arrow-left": "\2190", - "arrow-right": "\2192", - "arrow-up": "\2191", - "backspace": "\232B", - "backtab": "\21E4", - "caps-lock": "\21EA", - "clear": "\2327", - "context-menu": "\2630", - "delete": "\2326", - "eject": "\23CF", - "end": "\2913", - "escape": "\238B", - "home": "\2912", - "insert": "\2380", - "page-down": "\21DF", - "page-up": "\21DE", - "print-screen": "\2399" - ) { - .key-#{$name}::before { - padding-right: px2em(6.4px); - content: $code; - } - } - - // Define keyboard keys with right icon - @each $name, $code in ( - "tab": "\21E5", - "num-enter": "\2324", - "enter": "\23CE" - ) { - .key-#{$name}::after { - padding-left: px2em(6.4px); - content: $code; - } - } - } -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss deleted file mode 100644 index 9df91bfc..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss +++ /dev/null @@ -1,400 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Tabbed variables -:root { - --md-tabbed-icon--prev: svg-load("material/chevron-left.svg"); - --md-tabbed-icon--next: svg-load("material/chevron-right.svg"); -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Tabbed container - .tabbed-set { - position: relative; - display: flex; - flex-flow: column wrap; - margin: 1em 0; - border-radius: px2rem(2px); - - // Tab radio button - the Tabbed extension will generate radio buttons with - // labels, so tabs can be triggered without the necessity for JavaScript. - // This is pretty cool, as it has great accessibility out-of-the box, so - // we just hide the radio button and toggle the label color for indication. - > input { - position: absolute; - width: 0; - height: 0; - opacity: 0; - - // Adjust scroll margin - &:target { - --md-scroll-offset: #{px2em(10px, 16px)}; - } - - // Tab label states - @for $i from 20 through 1 { - &:nth-child(#{$i}) { - - // Tab is active - &:checked { - - // Tab label - ~ .tabbed-labels > :nth-child(#{$i}) { - @extend %tabbed-label; - } - - // Tab content - ~ .tabbed-content > :nth-child(#{$i}) { - @extend %tabbed-content; - } - } - - // Tab label on keyboard focus - &.focus-visible ~ .tabbed-labels > :nth-child(#{$i}) { - @extend %tabbed-label-focus-visible; - } - } - } - - // Tab indicator on keyboard focus - &.focus-visible ~ .tabbed-labels::before { - background-color: var(--md-accent-fg-color); - } - } - } - - // Tabbed labels - .tabbed-labels { - display: flex; - max-width: 100%; - overflow: auto; - box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest) inset; - -ms-overflow-style: none; // IE, Edge - scrollbar-width: none; // Firefox - - // [print]: Move one layer up for ordering - @media print { - display: contents; - } - - // [screen and no reduced motion]: Disable animation - @media screen { - - // [js]: Show animated tab indicator - .js & { - position: relative; - - // Tab indicator - &::before { - position: absolute; - bottom: 0; - left: 0; - display: block; - width: var(--md-indicator-width); - height: 2px; - content: ""; - background: var(--md-default-fg-color); - transition: - width 225ms, - background-color 250ms, - transform 250ms; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transform: translateX(var(--md-indicator-x)); - } - } - } - - // Webkit scrollbar - &::-webkit-scrollbar { - display: none; // Chrome, Safari - } - - // Tab label - > label { - flex-shrink: 0; - width: auto; - padding: px2em(10px, 12.8px) 1.25em px2em(8px, 12.8px); - font-size: px2rem(12.8px); - font-weight: 700; - color: var(--md-default-fg-color--light); - white-space: nowrap; - cursor: pointer; - border-bottom: px2rem(2px) solid transparent; - border-radius: px2rem(2px) px2rem(2px) 0 0; - transition: - background-color 250ms, - color 250ms; - scroll-margin-inline-start: px2rem(20px); - - // [print]: Intersperse labels with containers - @media print { - - // Ensure correct order of labels - @for $i from 1 through 20 { - &:nth-child(#{$i}) { - order: $i; - } - } - } - - // Tab label on hover - &:hover { - color: var(--md-default-fg-color); - } - } - } - - // Tabbed content - .tabbed-content { - width: 100%; - - // [print]: Move one layer up for ordering - @media print { - display: contents; - } - } - - // Tabbed block - .tabbed-block { - display: none; - - // [print]: Intersperse labels with containers - @media print { - display: block; - - // Ensure correct order of containers - @for $i from 1 through 20 { - &:nth-child(#{$i}) { - order: $i; - } - } - } - - // Code block is the first child of a tab - remove margin and mirror - // previous (now deprecated) SuperFences code block grouping behavior - > pre:first-child, - > .highlight:first-child > pre { - margin: 0; - - // Remove rounded borders on code block - > code { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - } - - // Code block is the first child of a tab - remove margin and mirror - // previous (now deprecated) SuperFences code block grouping behavior - > .highlight:first-child { - - // Code block title - remove spacing and rounded borders - > .filename { - margin: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - // Code block with line numbers - unfortunately, these selectors need to - // be overly specific so they don't bleed into code blocks in annotations. - > .highlighttable { - margin: 0; - - // Remove rounded borders on line numbers and titles - > tbody > tr > .filename span.filename, - > tbody > tr > .linenos { - margin: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; - } - - // Remove rounded borders on code blocks - > tbody > tr > .code > div > pre > code { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - } - - // Code block result container - adjust spacing - + .result { - margin-top: px2em(-2px); - } - } - - // Adjust spacing for nested tabbed container - > .tabbed-set { - margin: 0; - } - } - - // Tabbed button - .tabbed-button { - display: block; - align-self: center; - width: px2rem(18px); - height: px2rem(18px); - margin-top: px2rem(2px); - color: var(--md-default-fg-color--light); - pointer-events: initial; - cursor: pointer; - border-radius: 100%; - transition: background-color 250ms; - - // Tabbed button on hover - &:hover { - color: var(--md-accent-fg-color); - background-color: var(--md-accent-fg-color--transparent); - } - - // Tabbed button icon - &::after { - display: block; - width: 100%; - height: 100%; - content: ""; - background-color: currentcolor; - transition: - background-color 250ms, - transform 250ms; - mask-image: var(--md-tabbed-icon--prev); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - } - - // Tabbed control - .tabbed-control { - position: absolute; - display: flex; - justify-content: start; - width: px2rem(24px); - height: px2rem(38px); - pointer-events: none; - background: - linear-gradient( - to right, - var(--md-default-bg-color) 60%, - transparent - ); - transition: opacity 125ms; - - // Adjust for right-to-left languages - [dir="rtl"] & { - transform: rotate(180deg); - } - - // Tabbed control is hidden - &[hidden] { - opacity: 0; - } - - // Tabbed control next - &--next { - right: 0; - justify-content: end; - background: - linear-gradient( - to left, - var(--md-default-bg-color) 60%, - transparent - ); - - // Tabbed button icon content - .tabbed-button::after { - mask-image: var(--md-tabbed-icon--next); - } - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: top-level -// ---------------------------------------------------------------------------- - -// [mobile -]: Align with body copy -@include break-to-device(mobile) { - - // Top-level tabbed labels - .md-content__inner > .tabbed-set .tabbed-labels { - max-width: 100vw; - padding-inline-start: px2rem(16px); - margin: 0 px2rem(-16px); - scroll-padding-inline-start: px2rem(16px); - - // Hack: some browsers ignore the right padding on flex containers, - // see https://bit.ly/3lsPS3S - &::after { - padding-inline-end: px2rem(16px); - content: ""; - } - - // Tabbed control previous - ~ .tabbed-control--prev { - width: px2rem(40px); - padding-inline-start: px2rem(16px); - margin-inline-start: px2rem(-16px); - } - - // Tabbed control next - ~ .tabbed-control--next { - width: px2rem(40px); - padding-inline-end: px2rem(16px); - margin-inline-end: px2rem(-16px); - } - } -} - -// ---------------------------------------------------------------------------- -// Placeholders: improve colocation for better compression -// ---------------------------------------------------------------------------- - -// Tab label placeholder -%tabbed-label { - - // [screen]: Show active state - @media screen { - color: var(--md-default-fg-color); - - // [no-js]: Show border (indicator is animated with JavaScript) - .no-js & { - border-color: var(--md-default-fg-color); - } - } -} - -// Tab label on keyboard focus placeholder -%tabbed-label-focus-visible { - color: var(--md-accent-fg-color); -} - -// Tab content placeholder -%tabbed-content { - display: block; -} diff --git a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss b/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss deleted file mode 100644 index a1d1117c..00000000 --- a/src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss +++ /dev/null @@ -1,78 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Tasklist variables -:root { - --md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg"); - --md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg"); -} - -// ---------------------------------------------------------------------------- - -// Scoped in typesetted content to match specificity of regular content -.md-typeset { - - // Tasklist item - .task-list-item { - position: relative; - list-style-type: none; - - // Make checkbox items align with normal list items, but position - // everything in ems for correct layout at smaller font sizes - [type="checkbox"] { - position: absolute; - top: 0.45em; - inset-inline-start: -2em; - } - } - - // Hide native checkbox, when custom classes are enabled - .task-list-control [type="checkbox"] { - z-index: -1; - opacity: 0; - } - - // Tasklist indicator in unchecked state - .task-list-indicator::before { - position: absolute; - top: 0.15em; - width: px2em(20px); - height: px2em(20px); - content: ""; - background-color: var(--md-default-fg-color--lightest); - inset-inline-start: px2em(-24px); - mask-image: var(--md-tasklist-icon); - mask-position: center; - mask-repeat: no-repeat; - mask-size: contain; - } - - // Tasklist indicator in checked state - [type="checkbox"]:checked + .task-list-indicator::before { - background-color: $clr-green-a400; - mask-image: var(--md-tasklist-icon--checked); - } -} diff --git a/src/templates/assets/stylesheets/main/integrations/_mermaid.scss b/src/templates/assets/stylesheets/main/integrations/_mermaid.scss deleted file mode 100644 index d0325f39..00000000 --- a/src/templates/assets/stylesheets/main/integrations/_mermaid.scss +++ /dev/null @@ -1,67 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Mermaid variables -:root > * { - --md-mermaid-font-family: var(--md-text-font-family), sans-serif; - - // General colors - --md-mermaid-edge-color: var(--md-code-fg-color); - --md-mermaid-node-bg-color: var(--md-accent-fg-color--transparent); - --md-mermaid-node-fg-color: var(--md-accent-fg-color); - --md-mermaid-label-bg-color: var(--md-default-bg-color); - --md-mermaid-label-fg-color: var(--md-code-fg-color); - - // Sequence diagram colors - --md-mermaid-sequence-actor-bg-color: var(--md-mermaid-label-bg-color); - --md-mermaid-sequence-actor-fg-color: var(--md-mermaid-label-fg-color); - --md-mermaid-sequence-actor-border-color: var(--md-mermaid-node-fg-color); - --md-mermaid-sequence-actor-line-color: var(--md-default-fg-color--lighter); - --md-mermaid-sequence-actorman-bg-color: var(--md-mermaid-label-bg-color); - --md-mermaid-sequence-actorman-line-color: var(--md-mermaid-node-fg-color); - --md-mermaid-sequence-box-bg-color: var(--md-mermaid-node-bg-color); - --md-mermaid-sequence-box-fg-color: var(--md-mermaid-edge-color); - --md-mermaid-sequence-label-bg-color: var(--md-mermaid-node-bg-color); - --md-mermaid-sequence-label-fg-color: var(--md-mermaid-node-fg-color); - --md-mermaid-sequence-loop-bg-color: var(--md-mermaid-node-bg-color); - --md-mermaid-sequence-loop-fg-color: var(--md-mermaid-edge-color); - --md-mermaid-sequence-loop-border-color: var(--md-mermaid-node-fg-color); - --md-mermaid-sequence-message-fg-color: var(--md-mermaid-edge-color); - --md-mermaid-sequence-message-line-color: var(--md-mermaid-edge-color); - --md-mermaid-sequence-note-bg-color: var(--md-mermaid-label-bg-color); - --md-mermaid-sequence-note-fg-color: var(--md-mermaid-edge-color); - --md-mermaid-sequence-note-border-color: var(--md-mermaid-label-fg-color); - --md-mermaid-sequence-number-bg-color: var(--md-mermaid-node-fg-color); - --md-mermaid-sequence-number-fg-color: var(--md-accent-bg-color); -} - -// ---------------------------------------------------------------------------- - -// Mermaid container -.mermaid { - margin: 1em 0; - line-height: normal; -} diff --git a/src/templates/assets/stylesheets/palette.scss b/src/templates/assets/stylesheets/palette.scss deleted file mode 100644 index ff73a982..00000000 --- a/src/templates/assets/stylesheets/palette.scss +++ /dev/null @@ -1,40 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Dependencies -// ---------------------------------------------------------------------------- - -@import "material-color"; - -// ---------------------------------------------------------------------------- -// Local imports -// ---------------------------------------------------------------------------- - -@import "utilities/break"; -@import "utilities/convert"; - -@import "config"; - -@import "palette/scheme"; -@import "palette/accent"; -@import "palette/primary"; diff --git a/src/templates/assets/stylesheets/palette/_accent.scss b/src/templates/assets/stylesheets/palette/_accent.scss deleted file mode 100644 index 9f69b596..00000000 --- a/src/templates/assets/stylesheets/palette/_accent.scss +++ /dev/null @@ -1,61 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Define accent colors -@each $name, $color in ( - "red": $clr-red-a400, - "pink": $clr-pink-a400, - "purple": $clr-purple-a200, - "deep-purple": $clr-deep-purple-a200, - "indigo": $clr-indigo-a200, - "blue": $clr-blue-a200, - "light-blue": $clr-light-blue-a700, - "cyan": $clr-cyan-a700, - "teal": $clr-teal-a700, - "green": $clr-green-a700, - "light-green": $clr-light-green-a700, - "lime": $clr-lime-a700, - "yellow": $clr-yellow-a700, - "amber": $clr-amber-a700, - "orange": $clr-orange-a400, - "deep-orange": $clr-deep-orange-a200 -) { - - // Color palette - [data-md-color-accent="#{$name}"] { - --md-accent-fg-color: hsla(#{hex2hsl($color)}, 1); - --md-accent-fg-color--transparent: hsla(#{hex2hsl($color)}, 0.1); - - // Inverted text for lighter shades - @if index("lime" "yellow" "amber" "orange", $name) { - --md-accent-bg-color: hsla(0, 0%, 0%, 0.87); - --md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54); - } @else { - --md-accent-bg-color: hsla(0, 0%, 100%, 1); - --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); - } - } -} diff --git a/src/templates/assets/stylesheets/palette/_primary.scss b/src/templates/assets/stylesheets/palette/_primary.scss deleted file mode 100644 index a8653f0f..00000000 --- a/src/templates/assets/stylesheets/palette/_primary.scss +++ /dev/null @@ -1,203 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -@use "sass:list"; - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Define primary colors -@each $name, $colors in ( - "red": $clr-red-400 $clr-red-300 $clr-red-600, - "pink": $clr-pink-500 $clr-pink-400 $clr-pink-700, - "purple": $clr-purple-400 $clr-purple-300 $clr-purple-600, - "deep-purple": $clr-deep-purple-400 $clr-deep-purple-300 $clr-deep-purple-500, - "indigo": $clr-indigo-500 $clr-indigo-400 $clr-indigo-700, - "blue": $clr-blue-500 $clr-blue-400 $clr-blue-700, - "light-blue": $clr-light-blue-500 $clr-light-blue-400 $clr-light-blue-700, - "cyan": $clr-cyan-500 $clr-cyan-400 $clr-cyan-700, - "teal": $clr-teal-500 $clr-teal-400 $clr-teal-700, - "green": $clr-green-500 $clr-green-400 $clr-green-700, - "light-green": $clr-light-green-500 $clr-light-green-400 $clr-light-green-700, - "lime": $clr-lime-500 $clr-lime-400 $clr-lime-700, - "yellow": $clr-yellow-500 $clr-yellow-400 $clr-yellow-700, - "amber": $clr-amber-500 $clr-amber-400 $clr-amber-700, - "orange": $clr-orange-400 $clr-orange-400 $clr-orange-600, - "deep-orange": $clr-deep-orange-400 $clr-deep-orange-300 $clr-deep-orange-600, - "brown": $clr-brown-500 $clr-brown-400 $clr-brown-700, - "grey": $clr-grey-600 $clr-grey-500 $clr-grey-700, - "blue-grey": $clr-blue-grey-600 $clr-blue-grey-500 $clr-blue-grey-700 -) { - - // Color palette - [data-md-color-primary="#{$name}"] { - --md-primary-fg-color: hsl(#{hex2hsl(list.nth($colors, 1))}); - --md-primary-fg-color--light: hsl(#{hex2hsl(list.nth($colors, 2))}); - --md-primary-fg-color--dark: hsl(#{hex2hsl(list.nth($colors, 3))}); - - // Inverted text for lighter shades - @if index("lime" "yellow" "amber" "orange", $name) { - --md-primary-bg-color: hsla(0, 0%, 0%, 0.87); - --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54); - } @else { - --md-primary-bg-color: hsla(0, 0%, 100%, 1); - --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7); - } - - // Typeset color shades - @if index("grey" "blue-grey", $name) { - --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); - } - } -} - -// ---------------------------------------------------------------------------- - -// Adjust link colors for light primary colors -@each $name, $color in ( - "light-green": hsl(88, 58%, 43%), - "lime": hsl(66, 88%, 32%), - "yellow": hsl(54, 100%, 36%), - "amber": hsl(45, 100%, 41%), - "orange": hsl(36, 100%, 45%) -) { - [data-md-color-primary="#{$name}"]:not([data-md-color-scheme="slate"]) { - --md-typeset-a-color: #{$color}; - } -} - -// ---------------------------------------------------------------------------- -// Rules: white -// ---------------------------------------------------------------------------- - -// Define primary colors for white -[data-md-color-primary="white"] { - --md-primary-fg-color: hsla(var(--md-hue), 0%, 100%, 1); - --md-primary-fg-color--light: hsla(var(--md-hue), 0%, 100%, 0.7); - --md-primary-fg-color--dark: hsla(var(--md-hue), 0%, 0%, 0.07); - --md-primary-bg-color: hsla(var(--md-hue), 0%, 0%, 0.87); - --md-primary-bg-color--light: hsla(var(--md-hue), 0%, 0%, 0.54); - - // Typeset `a` color shades - --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); - - // Form button - .md-button { - color: var(--md-typeset-a-color); - - // Primary button - &--primary { - color: hsla(var(--md-hue), 0%, 100%, 1); - background-color: var(--md-typeset-a-color); - border-color: var(--md-typeset-a-color); - } - } - - // [tablet portrait +]: Header-embedded search - @include break-from-device(tablet landscape) { - - // Search form - .md-search__form { - background-color: hsla(var(--md-hue), 0%, 0%, 0.07); - - // Search form on hover - &:hover { - background-color: hsla(var(--md-hue), 0%, 0%, 0.32); - } - } - - // Search icon - .md-search__input + .md-search__icon { - color: hsla(var(--md-hue), 0%, 0%, 0.87); - } - } - - // [screen +]: Add bottom border for tabs - @include break-from-device(screen) { - - // Navigation tabs - .md-tabs { - border-bottom: px2rem(1px) solid hsla(0, 0%, 0%, 0.07); - } - } -} - -// ---------------------------------------------------------------------------- -// Rules: black -// ---------------------------------------------------------------------------- - -// Define primary colors for black -[data-md-color-primary="black"] { - --md-primary-fg-color: hsla(var(--md-hue), 15%, 9%, 1); - --md-primary-fg-color--light: hsla(var(--md-hue), 15%, 9%, 0.54); - --md-primary-fg-color--dark: hsla(var(--md-hue), 15%, 9%, 1); - --md-primary-bg-color: hsla(var(--md-hue), 15%, 100%, 1); - --md-primary-bg-color--light: hsla(var(--md-hue), 15%, 100%, 0.7); - - // Typeset `a` color shades - --md-typeset-a-color: hsl(#{hex2hsl($clr-indigo-500)}); - - // Form button - .md-button { - color: var(--md-typeset-a-color); - - // Primary button - &--primary { - color: hsla(var(--md-hue), 0%, 100%, 1); - background-color: var(--md-typeset-a-color); - border-color: var(--md-typeset-a-color); - } - } - - // Header - .md-header { - background-color: hsla(var(--md-hue), 15%, 9%, 1); - } - - // [tablet portrait -]: Layered navigation - @include break-to-device(tablet portrait) { - - // Repository information container - .md-nav__source { - background-color: hsla(var(--md-hue), 15%, 11%, 0.87); - } - } - - // [tablet -]: Layered navigation - @include break-to-device(tablet) { - - // Site title in main navigation - html & .md-nav--primary .md-nav__title[for="__drawer"] { - background-color: hsla(var(--md-hue), 15%, 9%, 1); - } - } - - // [screen +]: Set background color for tabs - @include break-from-device(screen) { - - // Navigation tabs - .md-tabs { - background-color: hsla(var(--md-hue), 15%, 9%, 1); - } - } -} diff --git a/src/templates/assets/stylesheets/palette/_scheme.scss b/src/templates/assets/stylesheets/palette/_scheme.scss deleted file mode 100644 index 0a9f9823..00000000 --- a/src/templates/assets/stylesheets/palette/_scheme.scss +++ /dev/null @@ -1,145 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -// ---------------------------------------------------------------------------- -// Rules -// ---------------------------------------------------------------------------- - -// Only use dark mode on screens -@media screen { - - // Slate theme, i.e. dark mode - [data-md-color-scheme="slate"] { - - // Indicate that the site is rendered with a dark color scheme - color-scheme: dark; - - // Default color shades - --md-default-fg-color: hsla(var(--md-hue), 15%, 90%, 0.82); - --md-default-fg-color--light: hsla(var(--md-hue), 15%, 90%, 0.56); - --md-default-fg-color--lighter: hsla(var(--md-hue), 15%, 90%, 0.32); - --md-default-fg-color--lightest: hsla(var(--md-hue), 15%, 90%, 0.12); - --md-default-bg-color: hsla(var(--md-hue), 15%, 14%, 1); - --md-default-bg-color--light: hsla(var(--md-hue), 15%, 14%, 0.54); - --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 14%, 0.26); - --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 14%, 0.07); - - // Code color shades - --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82); - --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1); - - // Code highlighting color shades - --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.15); - --md-code-hl-number-color: hsla(6, 74%, 63%, 1); - --md-code-hl-special-color: hsla(340, 83%, 66%, 1); - --md-code-hl-function-color: hsla(291, 57%, 65%, 1); - --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); - --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); - --md-code-hl-string-color: hsla(150, 58%, 44%, 1); - --md-code-hl-name-color: var(--md-code-fg-color); - --md-code-hl-operator-color: var(--md-default-fg-color--light); - --md-code-hl-punctuation-color: var(--md-default-fg-color--light); - --md-code-hl-comment-color: var(--md-default-fg-color--light); - --md-code-hl-generic-color: var(--md-default-fg-color--light); - --md-code-hl-variable-color: var(--md-default-fg-color--light); - - // Typeset color shades - --md-typeset-color: var(--md-default-fg-color); - - // Typeset `a` color shades - --md-typeset-a-color: var(--md-primary-fg-color); - - // Typeset `kbd` color shades - --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12); - --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 90%, 0.2); - --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1); - - // Typeset `mark` color shades - --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3); - - // Typeset `table` color shades - --md-typeset-table-color: hsla(var(--md-hue), 15%, 95%, 0.12); - --md-typeset-table-color--light: hsla(var(--md-hue), 15%, 95%, 0.035); - - // Admonition color shades - --md-admonition-fg-color: var(--md-default-fg-color); - --md-admonition-bg-color: var(--md-default-bg-color); - - // Footer color shades - --md-footer-bg-color: hsla(var(--md-hue), 15%, 10%, 0.87); - --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 8%, 1); - - // Shadow depth 1 - --md-shadow-z1: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); - - // Shadow depth 2 - --md-shadow-z2: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.25), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); - - // Shadow depth 3 - --md-shadow-z3: - 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.4), - 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); - - // Hide images for light mode - img[src$="#only-light"], - img[src$="#gh-light-mode-only"] { - display: none; - } - } - - // -------------------------------------------------------------------------- - - // Adjust link colors for dark primary colors - @each $name, $color in ( - "pink": hsl(340, 81%, 63%), - "purple": hsl(291, 53%, 63%), - "deep-purple": hsl(262, 73%, 70%), - "indigo": hsl(219, 76%, 62%), - "teal": hsl(174, 100%, 40%), - "green": hsl(122, 39%, 60%), - "deep-orange": hsl(14, 100%, 65%), - "brown": hsl(16, 45%, 56%), - - // Set neutral colors to indigo - "grey": hsl(219, 66%, 62%), - "blue-grey": hsl(219, 66%, 62%), - "white": hsl(219, 66%, 62%), - "black": hsl(219, 66%, 62%) - ) { - [data-md-color-scheme="slate"][data-md-color-primary="#{$name}"] { - --md-typeset-a-color: #{$color}; - } - } - - // -------------------------------------------------------------------------- - - // Switching in progress - disable all transitions temporarily - [data-md-color-switching] *, - [data-md-color-switching] *::before, - [data-md-color-switching] *::after { - transition-duration: 0ms !important; // stylelint-disable-line - } -} diff --git a/src/templates/assets/stylesheets/utilities/_break.scss b/src/templates/assets/stylesheets/utilities/_break.scss deleted file mode 100644 index 7ccd8622..00000000 --- a/src/templates/assets/stylesheets/utilities/_break.scss +++ /dev/null @@ -1,219 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -@use "sass:list"; -@use "sass:map"; -@use "sass:math"; - -// ---------------------------------------------------------------------------- -// Variables -// ---------------------------------------------------------------------------- - -/// -/// Device-specific breakpoints -/// -/// @example -/// $break-devices: ( -/// mobile: ( -/// portrait: 220px 479px, -/// landscape: 480px 719px -/// ), -/// tablet: ( -/// portrait: 720px 959px, -/// landscape: 960px 1219px -/// ), -/// screen: ( -/// small: 1220px 1599px, -/// medium: 1600px 1999px, -/// large: 2000px -/// ) -/// ); -/// -$break-devices: () !default; - -// ---------------------------------------------------------------------------- -// Helpers -// ---------------------------------------------------------------------------- - -/// -/// Choose minimum and maximum device widths -/// -@function break-select-min-max($devices) { - $min: 1000000; - $max: 0; - @each $key, $value in $devices { - @while type-of($value) == map { - $value: break-select-min-max($value); - } - @if type-of($value) == list { - @each $number in $value { - @if type-of($number) == number { - $min: math.min($number, $min); - @if $max { - $max: math.max($number, $max); - } - } @else { - @error "Invalid number: #{$number}"; - } - } - } @else if type-of($value) == number { - $min: math.min($value, $min); - $max: null; - } @else { - @error "Invalid value: #{$value}"; - } - } - @return $min, $max; -} - -/// -/// Select minimum and maximum widths for a device breakpoint -/// -@function break-select-device($device) { - $current: $break-devices; - @for $n from 1 through length($device) { - @if type-of($current) == map { - $current: map.get($current, list.nth($device, $n)); - } @else { - @error "Invalid device map: #{$devices}"; - } - } - @if type-of($current) == list or type-of($current) == number { - $current: (default: $current); - } - @return break-select-min-max($current); -} - -// ---------------------------------------------------------------------------- -// Mixins -// ---------------------------------------------------------------------------- - -/// -/// A minimum-maximum media query breakpoint -/// -@mixin break-at($breakpoint) { - @if type-of($breakpoint) == number { - @media screen and (min-width: $breakpoint) { - @content; - } - } @else if type-of($breakpoint) == list { - $min: list.nth($breakpoint, 1); - $max: list.nth($breakpoint, 2); - @if type-of($min) == number and type-of($max) == number { - @media screen and (min-width: $min) and (max-width: $max) { - @content; - } - } @else { - @error "Invalid breakpoint: #{$breakpoint}"; - } - } @else { - @error "Invalid breakpoint: #{$breakpoint}"; - } -} - -/// -/// An orientation media query breakpoint -/// -@mixin break-at-orientation($breakpoint) { - @if type-of($breakpoint) == string { - @media screen and (orientation: $breakpoint) { - @content; - } - } @else { - @error "Invalid breakpoint: #{$breakpoint}"; - } -} - -/// -/// A maximum-aspect-ratio media query breakpoint -/// -@mixin break-at-ratio($breakpoint) { - @if type-of($breakpoint) == number { - @media screen and (max-aspect-ratio: $breakpoint) { - @content; - } - } @else { - @error "Invalid breakpoint: #{$breakpoint}"; - } -} - -/// -/// A minimum-maximum media query device breakpoint -/// -@mixin break-at-device($device) { - @if type-of($device) == string { - $device: $device,; - } - @if type-of($device) == list { - $breakpoint: break-select-device($device); - @if list.nth($breakpoint, 2) { - $min: list.nth($breakpoint, 1); - $max: list.nth($breakpoint, 2); - - @media screen and (min-width: $min) and (max-width: $max) { - @content; - } - } @else { - @error "Invalid device: #{$device}"; - } - } @else { - @error "Invalid device: #{$device}"; - } -} - -/// -/// A minimum media query device breakpoint -/// -@mixin break-from-device($device) { - @if type-of($device) == string { - $device: $device,; - } - @if type-of($device) == list { - $breakpoint: break-select-device($device); - $min: list.nth($breakpoint, 1); - - @media screen and (min-width: $min) { - @content; - } - } @else { - @error "Invalid device: #{$device}"; - } -} - -/// -/// A maximum media query device breakpoint -/// -@mixin break-to-device($device) { - @if type-of($device) == string { - $device: $device,; - } - @if type-of($device) == list { - $breakpoint: break-select-device($device); - $max: list.nth($breakpoint, 2); - - @media screen and (max-width: $max) { - @content; - } - } @else { - @error "Invalid device: #{$device}"; - } -} diff --git a/src/templates/assets/stylesheets/utilities/_convert.scss b/src/templates/assets/stylesheets/utilities/_convert.scss deleted file mode 100644 index 8199c9c8..00000000 --- a/src/templates/assets/stylesheets/utilities/_convert.scss +++ /dev/null @@ -1,79 +0,0 @@ -//// -/// Copyright (c) 2016-2023 Martin Donath -/// -/// Permission is hereby granted, free of charge, to any person obtaining a -/// copy of this software and associated documentation files (the "Software"), -/// to deal in the Software without restriction, including without limitation -/// the rights to use, copy, modify, merge, publish, distribute, sublicense, -/// and/or sell copies of the Software, and to permit persons to whom the -/// Software is furnished to do so, subject to the following conditions: -/// -/// The above copyright notice and this permission notice shall be included in -/// all copies or substantial portions of the Software. -/// -/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL -/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -/// DEALINGS -//// - -@use "sass:math"; - -// ---------------------------------------------------------------------------- -// Helpers -// ---------------------------------------------------------------------------- - -/// -/// Strip units from a number -/// -@function strip-units($number) { - @return math.div($number, ($number * 0 + 1)); -} - -/// -/// Convert color in HEX to HSL -/// -/// Note, that we need to strip the `deg` units from the `hue` value, as they -/// were added in Color Level 4, which not all browsers support. -/// -@function hex2hsl($color) { - @return - round(strip-units(hue($color))), - round(saturation($color)), - round(lightness($color)); -} - -// ---------------------------------------------------------------------------- - -/// -/// Convert font size in px to em -/// -@function px2em($size, $base: 16px) { - @if unit($size) == px { - @if unit($base) == px { - @return math.div($size, $base) * 1em; - } @else { - @error "Invalid base: #{$base} - unit must be 'px'"; - } - } @else { - @error "Invalid size: #{$size} - unit must be 'px'"; - } -} - -/// -/// Convert font size in px to rem -/// -@function px2rem($size, $base: 20px) { - @if unit($size) == px { - @if unit($base) == px { - @return math.div($size, $base) * 1rem; - } @else { - @error "Invalid base: #{$base} - unit must be 'px'"; - } - } @else { - @error "Invalid size: #{$size} - unit must be 'px'"; - } -} diff --git a/src/templates/base.html b/src/templates/base.html deleted file mode 100644 index 8323e76e..00000000 --- a/src/templates/base.html +++ /dev/null @@ -1,445 +0,0 @@ - - -{% import "partials/language.html" as lang with context %} - - - - - - - {% block site_meta %} - - - - - {% if page.meta and page.meta.description %} - - {% elif config.site_description %} - - {% endif %} - - - {% if page.meta and page.meta.author %} - - {% elif config.site_author %} - - {% endif %} - - - {% if page.canonical_url %} - - {% endif %} - - - {% if page.previous_page %} - - {% endif %} - - - {% if page.next_page %} - - {% endif %} - - - {% if "rss" in config.plugins %} - - - {% endif %} - - - - - - - {% endblock %} - - - {% block htmltitle %} - {% if page.meta and page.meta.title %} - {{ page.meta.title }} - {{ config.site_name }} - {% elif page.title and not page.is_homepage %} - {{ page.title | striptags }} - {{ config.site_name }} - {% else %} - {{ config.site_name }} - {% endif %} - {% endblock %} - - - {% block styles %} - - - - {% if config.theme.palette %} - {% set palette = config.theme.palette %} - - {% endif %} - - - {% include "partials/icons.html" %} - {% endblock %} - - - {% block libs %} - {% for script in config.extra.polyfills %} - {{ script | script_tag }} - {% endfor %} - {% endblock %} - - - {% block fonts %} - - - {% if config.theme.font != false %} - {% set text = config.theme.font.get("text", "Roboto") %} - {% set code = config.theme.font.get("code", "Roboto Mono") %} - - - - {% endif %} - {% endblock %} - - - {% for path in config.extra_css %} - - {% endfor %} - - - {% include "partials/javascripts/base.html" %} - - - {% block analytics %} - {% include "partials/integrations/analytics.html" %} - {% endblock %} - - - {% if page.meta and page.meta.meta %} - {% for tag in page.meta.meta %} - - {% endfor %} - {% endif %} - - - {% block extrahead %}{% endblock %} - - - - {% set direction = config.theme.direction or lang.t("direction") %} - {% if config.theme.palette %} - {% set palette = config.theme.palette %} - {% if not palette is mapping %} - {% set palette = palette | first %} - {% endif %} - {% set scheme = palette.scheme | d("default", true) %} - {% set primary = palette.primary | d("indigo", true) %} - {% set accent = palette.accent | d("indigo", true) %} - - {% else %} - - {% endif %} - {% set features = config.theme.features or [] %} - - - {% if not config.theme.palette is mapping %} - {% include "partials/javascripts/palette.html" %} - {% endif %} - - - - - - - - - -
    - {% if page.toc | first is defined %} - {% set skip = page.toc | first %} - - {{ lang.t("action.skip") }} - - {% endif %} -
    - - -
    - {% if self.announce() %} - - {% endif %} -
    - - - {% if config.extra.version %} - - {% endif %} - - - {% block header %} - {% include "partials/header.html" %} - {% endblock %} - - -
    - - - {% block hero %}{% endblock %} - - - {% block tabs %} - {% if "navigation.tabs.sticky" not in features %} - {% if "navigation.tabs" in features %} - {% include "partials/tabs.html" %} - {% endif %} - {% endif %} - {% endblock %} - - -
    -
    - - - {% block site_nav %} - - - {% if nav %} - {% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "navigation" in page.meta.hide %} - {% endif %} - - {% endif %} - - - {% if "toc.integrate" not in features %} - {% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "toc" in page.meta.hide %} - {% endif %} - - {% endif %} - {% endblock %} - - - {% block container %} -
    -
    - {% block content %} - {% include "partials/content.html" %} - {% endblock %} -
    -
    - {% endblock %} - - - {% include "partials/javascripts/content.html" %} -
    - - - {% if "navigation.top" in features %} - {% include "partials/top.html" %} - {% endif %} -
    - - - {% block footer %} - {% include "partials/footer.html" %} - {% endblock %} -
    - - -
    -
    -
    - - - {% if "navigation.instant.progress" in features %} - {% include "partials/progress.html" %} - {% endif %} - - - {% if config.extra.consent %} - - - - {% include "partials/javascripts/consent.html" %} - {% endif %} - - - {% block config %} - {%- set app = { - "base": base_url, - "features": features, - "translations": {}, - "search": "assets/javascripts/workers/search.js" | url - } -%} - - - {%- if config.extra.version -%} - {%- set _ = app.update({ "version": config.extra.version }) -%} - {%- endif -%} - - - {%- if config.extra.tags -%} - {%- set _ = app.update({ "tags": config.extra.tags }) -%} - {%- endif -%} - - - {%- set translations = app.translations -%} - {%- for key in [ - "clipboard.copy", - "clipboard.copied", - "search.result.placeholder", - "search.result.none", - "search.result.one", - "search.result.other", - "search.result.more.one", - "search.result.more.other", - "search.result.term.missing", - "select.version" - ] -%} - {%- set _ = translations.update({ key: lang.t(key) }) -%} - {%- endfor -%} - - - - {% endblock %} - - - {% block scripts %} - - - - {% for script in config.extra_javascript %} - {{ script | script_tag }} - {% endfor %} - {% endblock %} - - diff --git a/src/templates/blog-post.html b/src/templates/blog-post.html deleted file mode 100644 index 73fb669f..00000000 --- a/src/templates/blog-post.html +++ /dev/null @@ -1,164 +0,0 @@ - - -{% extends "main.html" %} - -{% import "partials/nav-item.html" as item with context %} - - -{% block container %} -
    - - -
    -
    -
    - - - - {% if "toc.integrate" in features %} - {% include "partials/toc.html" %} - {% endif %} -
    -
    -
    - - -
    - {% block content %} - {% include "partials/content.html" %} - {% endblock %} -
    -
    -{% endblock %} diff --git a/src/templates/blog.html b/src/templates/blog.html deleted file mode 100644 index eedc77d9..00000000 --- a/src/templates/blog.html +++ /dev/null @@ -1,48 +0,0 @@ - - -{% extends "main.html" %} - - -{% block container %} -
    -
    - - -
    - {{ page.content }} -
    - - - {% for post in posts %} - {% include "partials/post.html" %} - {% endfor %} - - - {% if pagination %} - {% block pagination %} - {% include "partials/pagination.html" %} - {% endblock %} - {% endif %} -
    -
    -{% endblock %} diff --git a/src/templates/main.html b/src/templates/main.html deleted file mode 100644 index 3b77d200..00000000 --- a/src/templates/main.html +++ /dev/null @@ -1,23 +0,0 @@ - - -{% extends "base.html" %} diff --git a/src/templates/mkdocs_theme.yml b/src/templates/mkdocs_theme.yml deleted file mode 100644 index aaa47f5e..00000000 --- a/src/templates/mkdocs_theme.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2016-2023 Martin Donath - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -# Language for theme localization -language: en - -# Text direction (can be ltr or rtl), default: ltr -direction: - -# Feature flags for functionality that alters behavior significantly, and thus -# may be a matter of taste -features: [] - -# Fonts used by Material, automatically loaded from Google Fonts - see the site -# for a list of available fonts -font: - - # Default font for text - text: Roboto - - # Fixed-width font for code listings - code: Roboto Mono - -# From Material 5.x on, icons are inlined into the HTML and CSS as SVGs. -# Icons that are part of the HTML can be configured and replaced -icon: - -# Favicon to be rendered -favicon: assets/images/favicon.png - -# Static pages to build -static_templates: - - 404.html diff --git a/src/templates/partials/actions.html b/src/templates/partials/actions.html deleted file mode 100644 index 75fcb8eb..00000000 --- a/src/templates/partials/actions.html +++ /dev/null @@ -1,54 +0,0 @@ - - - -{% if page.edit_url %} - - - {% if "content.action.edit" in features %} - - {% set icon = config.theme.icon.edit or "material/file-edit-outline" %} - {% include ".icons/" ~ icon ~ ".svg" %} - - {% endif %} - - - {% if "content.action.view" in features %} - {% if "/blob/" in page.edit_url %} - {% set part = "blob" %} - {% else %} - {% set part = "edit" %} - {% endif %} - - {% set icon = config.theme.icon.view or "material/file-eye-outline" %} - {% include ".icons/" ~ icon ~ ".svg" %} - - {% endif %} -{% endif %} diff --git a/src/templates/partials/alternate.html b/src/templates/partials/alternate.html deleted file mode 100644 index 7d7c925b..00000000 --- a/src/templates/partials/alternate.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -
    -
    - {% set icon = config.theme.icon.alternate or "material/translate" %} - -
    - -
    -
    -
    diff --git a/src/templates/partials/comments.html b/src/templates/partials/comments.html deleted file mode 100644 index 6641d20e..00000000 --- a/src/templates/partials/comments.html +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/src/templates/partials/consent.html b/src/templates/partials/consent.html deleted file mode 100644 index c84622bc..00000000 --- a/src/templates/partials/consent.html +++ /dev/null @@ -1,107 +0,0 @@ - - - -{% set cookies = config.extra.consent.cookies | d({}) %} -{% if config.extra.analytics %} - {% if "analytics" not in cookies %} - {% set _ = cookies.update({ "analytics": "Google Analytics" }) %} - {% endif %} -{% endif %} -{% if config.repo_url and "github.com" in config.repo_url %} - {% if "github" not in cookies %} - {% set _ = cookies.update({ "github": "GitHub" }) %} - {% endif %} -{% endif %} - - -{% set actions = config.extra.consent.actions %} -{% if not actions %} - {% set actions = ["accept", "manage"] %} -{% endif %} - - -{% if "manage" not in actions %} - {% set checked = "checked" %} -{% endif %} - - -

    {{ config.extra.consent.title }}

    -

    {{ config.extra.consent.description }}

    - - - - - - - diff --git a/src/templates/partials/content.html b/src/templates/partials/content.html deleted file mode 100644 index 2b78b09b..00000000 --- a/src/templates/partials/content.html +++ /dev/null @@ -1,54 +0,0 @@ - - - -{% if "material/tags" in config.plugins and tags %} - {% include "partials/tags.html" %} -{% endif %} - - -{% include "partials/actions.html" %} - - -{% if "\x3ch1" not in page.content %} -

    {{ page.title | d(config.site_name, true)}}

    -{% endif %} - - -{{ page.content }} - - -{% if page.meta and ( - page.meta.git_revision_date_localized or - page.meta.revision_date -) %} - {% include "partials/source-file.html" %} -{% endif %} - - -{% include "partials/feedback.html" %} - - -{% include "partials/comments.html" %} diff --git a/src/templates/partials/copyright.html b/src/templates/partials/copyright.html deleted file mode 100644 index 070948d2..00000000 --- a/src/templates/partials/copyright.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - diff --git a/src/templates/partials/feedback.html b/src/templates/partials/feedback.html deleted file mode 100644 index bf27c640..00000000 --- a/src/templates/partials/feedback.html +++ /dev/null @@ -1,79 +0,0 @@ - - - -{% if config.extra.analytics %} - {% set feedback = config.extra.analytics.feedback %} -{% endif %} - - -{% if page.meta and page.meta.hide %} - {% if "feedback" in page.meta.hide %} - {% set feedback = None %} - {% endif %} -{% endif %} - - -{% if feedback %} - -{% endif %} diff --git a/src/templates/partials/footer.html b/src/templates/partials/footer.html deleted file mode 100644 index ebe9278f..00000000 --- a/src/templates/partials/footer.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - diff --git a/src/templates/partials/header.html b/src/templates/partials/header.html deleted file mode 100644 index 9b6d2e2e..00000000 --- a/src/templates/partials/header.html +++ /dev/null @@ -1,112 +0,0 @@ - - - -{% set class = "md-header" %} -{% if "navigation.tabs.sticky" in features %} - {% set class = class ~ " md-header--shadow md-header--lifted" %} -{% elif "navigation.tabs" not in features %} - {% set class = class ~ " md-header--shadow" %} -{% endif %} - - -
    - - - - {% if "navigation.tabs.sticky" in features %} - {% if "navigation.tabs" in features %} - {% include "partials/tabs.html" %} - {% endif %} - {% endif %} -
    diff --git a/src/templates/partials/icons.html b/src/templates/partials/icons.html deleted file mode 100644 index 17dd20d8..00000000 --- a/src/templates/partials/icons.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -{% 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 %} - - -{% if config.theme.icon.annotation %} - {% set style = ["\x3cstyle\x3e:root{"] %} - {% import ".icons/" ~ config.theme.icon.annotation ~ ".svg" as icon %} - {% set _ = style.append( - "--md-annotation-icon:" ~ - "url('data:image/svg+xml;charset=utf-8," ~ - icon | replace("\n", "") ~ - "');" - ) %} - {% set _ = style.append("}\x3c/style\x3e") %} - {{ style | join }} -{% endif %} - - -{% if config.theme.icon.tag %} - {% set style = ["\x3cstyle\x3e"] %} - {% for type, icon in config.theme.icon.tag.items() %} - {% import ".icons/" ~ icon ~ ".svg" as icon %} - {% if type != "default" %} - {% set modifier = "--" ~ type %} - {% endif %} - {% set _ = style.append( - ".md-tag" ~ modifier ~ "{" ~ - "--md-tag-icon:" ~ - "url('data:image/svg+xml;charset=utf-8," ~ - icon | replace("\n", "") ~ - "');" ~ - "}" - ) %} - {% endfor %} - {% set _ = style.append("\x3c/style\x3e") %} - {{ style | join }} -{% endif %} diff --git a/src/templates/partials/integrations/analytics.html b/src/templates/partials/integrations/analytics.html deleted file mode 100644 index 4b483046..00000000 --- a/src/templates/partials/integrations/analytics.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -{% if config.extra.analytics %} - {% set provider = config.extra.analytics.provider %} -{% endif %} - - -{% if provider %} - {% include "partials/integrations/analytics/" ~ provider ~ ".html" %} - - - {% if config.extra.consent %} - - - - {% else %} - - {% endif %} -{% endif %} diff --git a/src/templates/partials/integrations/analytics/google.html b/src/templates/partials/integrations/analytics/google.html deleted file mode 100644 index a9fa37d9..00000000 --- a/src/templates/partials/integrations/analytics/google.html +++ /dev/null @@ -1,97 +0,0 @@ - - - -{% if config.extra.analytics %} - {% set property = config.extra.analytics.property | d("", true) %} -{% endif %} - - - diff --git a/src/templates/partials/javascripts/announce.html b/src/templates/partials/javascripts/announce.html deleted file mode 100644 index f13961b2..00000000 --- a/src/templates/partials/javascripts/announce.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - diff --git a/src/templates/partials/javascripts/base.html b/src/templates/partials/javascripts/base.html deleted file mode 100644 index f0eeeb8a..00000000 --- a/src/templates/partials/javascripts/base.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - diff --git a/src/templates/partials/javascripts/consent.html b/src/templates/partials/javascripts/consent.html deleted file mode 100644 index 13730da7..00000000 --- a/src/templates/partials/javascripts/consent.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - diff --git a/src/templates/partials/javascripts/content.html b/src/templates/partials/javascripts/content.html deleted file mode 100644 index d361f18b..00000000 --- a/src/templates/partials/javascripts/content.html +++ /dev/null @@ -1,39 +0,0 @@ - - - -{% if "content.tabs.link" in features %} - -{% endif %} diff --git a/src/templates/partials/javascripts/outdated.html b/src/templates/partials/javascripts/outdated.html deleted file mode 100644 index 576f3c85..00000000 --- a/src/templates/partials/javascripts/outdated.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - diff --git a/src/templates/partials/javascripts/palette.html b/src/templates/partials/javascripts/palette.html deleted file mode 100644 index a2daef1d..00000000 --- a/src/templates/partials/javascripts/palette.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - diff --git a/src/templates/partials/language.html b/src/templates/partials/language.html deleted file mode 100644 index e37b953b..00000000 --- a/src/templates/partials/language.html +++ /dev/null @@ -1,28 +0,0 @@ - - - -{% import "partials/languages/" ~ config.theme.language ~ ".html" as lang %} -{% import "partials/languages/en.html" as fallback %} - - -{% macro t(key) %}{{ lang.t(key) or fallback.t(key) or key }}{% endmacro %} diff --git a/src/templates/partials/languages/af.html b/src/templates/partials/languages/af.html deleted file mode 100644 index b7f9f8fa..00000000 --- a/src/templates/partials/languages/af.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "af", - "action.edit": "Wysig hierdie bladsy", - "action.skip": "Slaan oor na inhoud", - "action.view": "Bekyk bron van hierdie bladsy", - "announce.dismiss": "Moenie dit weer wys nie", - "blog.archive": "Argief", - "blog.categories": "Kategorieë", - "blog.categories.in": "binne", - "blog.continue": "Lees verder", - "blog.draft": "Konsep", - "blog.index": "Terug na indeks", - "blog.meta": "Metadata", - "blog.references": "Verwante skakels", - "clipboard.copy": "Kopieer na knipbord", - "clipboard.copied": "gekopieer na knipbord", - "consent.accept": "Aanvaar", - "consent.manage": "Bestuur instellings", - "consent.reject": "Verwerp", - "footer": "Voetskrif", - "footer.next": "Volgende", - "footer.previous": "Vorige", - "header": "Kopskrif", - "meta.comments": "Kommentaar", - "meta.source": "Bron", - "nav": "Navigasie", - "readtime.one": "1 minuut se lees", - "readtime.other": "# minuut se lees", - "rss.created": "RSS-voer geskep", - "rss.updated": "RSS-voer van opgedateerde inhoud", - "search": "Soek", - "search.config.lang": "nl", - "search.placeholder": "Soek", - "search.share": "Deel", - "search.reset": "Terugstel", - "search.result.initializer": "Inisialisering van soektog", - "search.result.placeholder": "Tik om te begin soek", - "search.result.none": "Geen ooreenstemmende dokumente", - "search.result.one": "1 ooreenstemmende dokument", - "search.result.other": "# ooreenstemmende dokumente", - "search.result.more.one": "1 meer op hierdie bladsy", - "search.result.more.other": "# meer op hierdie bladsy", - "search.result.term.missing": "Vermis", - "select.language": "Kies taal", - "select.version": "Kies weergawe", - "source": "Slaan oor na inhoud", - "source.file.contributors": "Medewerkers", - "source.file.date.created": "Geskep", - "source.file.date.updated": "Laaste opdatering", - "tabs": "Duimgids", - "toc": "Inhoudsopgawe", - "top": "Terug na bo" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ar.html b/src/templates/partials/languages/ar.html deleted file mode 100644 index 4d5da33a..00000000 --- a/src/templates/partials/languages/ar.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ar", - "direction": "rtl", - "action.edit": "عدل الصفحة", - "action.skip": "انتقل إلى المحتوى", - "action.view": "عرض مصدر هذه الصفحة", - "announce.dismiss": "لا تظهر هذا مرة أخرى", - "blog.archive": "أرشيف", - "blog.categories": "فئات", - "blog.categories.in": "ضمن", - "blog.continue": "أكمل القراءة", - "blog.draft": "مسودة", - "blog.index": "رجوع إلى الفهرس", - "blog.meta": "بيانات وصفية", - "blog.references": "روابط ذات علاقة", - "clipboard.copy": "نسخ إلى الحافظة", - "clipboard.copied": "تم النسخ الى الحافظة", - "consent.accept": "قبول", - "consent.manage": "إدارة الإعدادات", - "consent.reject": "رفض", - "footer": "هامش سفلي", - "footer.next": "التالية", - "footer.previous": "السابقة", - "header": "عنوان العارضة", - "meta.comments": "التعليقات", - "meta.source": "المصدر", - "nav": "تصفح", - "readtime.one": "قراءة لمدة دقيقة", - "readtime.other": "دقائق قراءة #", - "rss.created": "ملقم بالخلاصات", - "rss.updated": "ملقم بالخلاصات المحدثة", - "search": "إبحث", - "search.config.pipeline": " ", - "search.placeholder": "بحث", - "search.share": "شارك", - "search.reset": "مسح كلي", - "search.result.initializer": "بدء البحث", - "search.result.placeholder": "اكتب لبدء البحث", - "search.result.none": "لا توجد نتائج", - "search.result.one": "نتائج البحث مستند واحد", - "search.result.other": "نتائج البحث # مستندات", - "search.result.more.one": "أكثر من 1 في هذه الصفحة", - "search.result.more.other": "أكثر من # في هذه الصفحة", - "search.result.term.missing": "مفقود", - "select.language": "إختر اللغة", - "select.version": "إختر الإصدار", - "source": "اذهب إلى المصدر", - "source.file.contributors": "المساهمون", - "source.file.date.created": "خلقت", - "source.file.date.updated": "اخر تحديث", - "tabs": "نوافذ", - "toc": "جدول المحتويات", - "top": "عد إلى الأعلى" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/be.html b/src/templates/partials/languages/be.html deleted file mode 100644 index c36c8402..00000000 --- a/src/templates/partials/languages/be.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "be", - "direction": "ltr", - "action.edit": "Правіць старонку", - "action.skip": "Перайсці да зместа", - "action.view": "Паглядзець зыходны код старонкі", - "announce.dismiss": "Больш не паказваць", - "blog.archive": "Заархіваваць", - "blog.categories": "Катэгорыі", - "blog.categories.in": "у", - "blog.continue": "Працягнуць чытаць", - "blog.draft": "Чарнавік", - "blog.index": "Вярнуцца на хатнюю", - "blog.meta": "Метаданыя", - "blog.references": "Спасылкі па тэме", - "clipboard.copy": "Скапіраваць у буфер абмена", - "clipboard.copied": "Скапіравана ў буфер абмена", - "consent.accept": "Прыняць", - "consent.manage": "Кіраваць наладамі", - "consent.reject": "Адхіліць", - "footer": "Ніжні калантытул", - "footer.next": "Наступная", - "footer.previous": "Папярэдняя", - "header": "Верхні калантытул", - "meta.comments": "Каментарыі", - "meta.source": "Зыходны код", - "nav": "Навігацыя", - "readtime.one": "Прачытанне зойме 1 хв", - "readtime.other": "Прачытанне зойме # хв", - "rss.created": "RSS стужка", - "rss.updated": "RSS стужка з абноўленым зместам", - "search": "Пошук", - "search.config.lang": "ru", - "search.placeholder": "Пошук", - "search.share": "Падзяліцца", - "search.reset": "Ачысціць", - "search.result.initializer": "Пачынаем пошук", - "search.result.placeholder": "Пачніце друкаваць для пошуку", - "search.result.none": "Нічога ня знойдзена", - "search.result.one": "Адзін адпаведны дакумент", - "search.result.other": "Адпаведных дакументаў: #", - "search.result.more.one": "Яшчэ 1 на гэтай старонцы", - "search.result.more.other": "Яшчэ # на гэтай старонцы", - "search.result.term.missing": "Адсутнічае", - "select.language": "Выберыце мову", - "select.version": "Выберыце версію", - "source": "Перайсці ў рэпазітар", - "source.file.contributors": "Укладальнікі", - "source.file.date.created": "Створана", - "source.file.date.updated": "Апошняе абнаўленне", - "tabs": "Укладкі", - "toc": "Змест", - "top": "Вярнуцца да пачатку" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/bg.html b/src/templates/partials/languages/bg.html deleted file mode 100644 index f36fd437..00000000 --- a/src/templates/partials/languages/bg.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "bg", - "action.edit": "Редактирай тази страница", - "action.skip": "Към съдържанието", - "action.view": "Виж съдържанието на тази страница", - "announce.dismiss": "Не показвай повече", - "blog.archive": "Архив", - "blog.categories": "Категории", - "blog.categories.in": "В", - "blog.continue": "Продължи четенето", - "blog.draft": "Чернова", - "blog.index": "Назад към индекса", - "blog.meta": "Метаданни", - "blog.references": "Свързани линкове", - "clipboard.copy": "Копирай", - "clipboard.copied": "Копирано", - "consent.accept": "Приеми", - "consent.manage": "Управление на настойките", - "consent.reject": "Откажи", - "footer": "Долен колонтитул", - "footer.next": "Следваща", - "footer.previous": "Предишна", - "header": "Горен колонтитул", - "meta.comments": "Коментари", - "meta.source": "Код", - "nav": "Навигация", - "readtime.one": "1 мин четено", - "readtime.other": "# мин четено", - "rss.created": "RSS публикации", - "rss.updated": "RSS публикации с актуализирано съдържание", - "search": "Търси", - "search.config.lang": "ru", - "search.placeholder": "Търси", - "search.share": "Сподели", - "search.reset": "Изчисти", - "search.result.initializer": "Инициализирано търсене", - "search.result.placeholder": "Започнете да пишете, за да търсите", - "search.result.none": "Няма резултати", - "search.result.one": "1 резултат", - "search.result.other": "# резултата", - "search.result.more.one": "още 1 на тази страница", - "search.result.more.other": "още # на тази страница", - "search.result.term.missing": "Липсващо", - "select.language": "Избери език", - "select.version": "Избери версия", - "source": "Към хранилището", - "source.file.contributors": "Участници", - "source.file.date.created": "Създаден", - "source.file.date.updated": "Последна промяна", - "tabs": "Табове", - "toc": "Съдържание", - "top": "Върни се в началото" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/bn.html b/src/templates/partials/languages/bn.html deleted file mode 100644 index 0a3ee6d0..00000000 --- a/src/templates/partials/languages/bn.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "bn", - "action.edit": "এই পেজ এডিট করুন", - "action.skip": "কনটেন্টে যান", - "action.view": "পেজের ভিউ", - "announce.dismiss": "আর কখনো দেখাবে না", - "blog.archive": "সংরক্ষণাগার", - "blog.categories": "বিভাগ", - "blog.categories.in": "বিভাগের মধ্যে", - "blog.continue": "পড়তে থাকুন", - "blog.draft": "খসড়া", - "blog.index": "ইনডেক্সে ফিরে যান", - "blog.meta": "মেটাডেটা", - "blog.references": "সম্পর্কিত লিংক", - "clipboard.copy": "ক্লিপবোর্ডে কপি করুন", - "clipboard.copied": "ক্লিপবোর্ডে কপি হয়েছে", - "consent.accept": "গ্রহণ", - "consent.manage": "সেটিংস ব্যবস্থাপনা", - "consent.reject": "প্রত্যাখ্যান", - "footer": "ফুটার", - "footer.next": "পরে", - "footer.previous": "পূর্ববর্তী", - "header": "হেডার", - "meta.comments": "মন্তব্য", - "meta.source": "উৎস", - "nav": "ন্যাভিগেশন", - "readtime.one": "১ মিনিট পড়া", - "readtime.other": "# মিনিট পড়া", - "rss.created": "আরএসএস ফিড", - "rss.updated": "আপডেট করা বিষয়বস্তুর আরএসএস ফিড", - "search": "অনুসন্ধান করুন", - "search.config.pipeline": " ", - "search.placeholder": "অনুসন্ধান করুন", - "search.share": "শেয়ার", - "search.reset": "রিসেট", - "search.result.initializer": "অনুসন্ধান শুরু করা হচ্ছে", - "search.result.placeholder": "সার্চ টাইপ করুন", - "search.result.none": "কিছু পাওয়া যায়নি", - "search.result.one": "১ টা ডকুমেন্ট", - "search.result.other": "# টা ডকুমেন্ট", - "search.result.more.one": "এই পৃষ্ঠায় আরও ১টি আছে", - "search.result.more.other": "এই পৃষ্ঠায় আরও #টি আছে", - "search.result.term.missing": "অনুপস্থিত", - "select.language": "ভাষা নির্বাচন করুণ", - "select.version": "সংস্করণ নির্বাচন করুণ", - "source": "রিপোজিটরিতে যান", - "source.file.contributors": "অবদানকারী", - "source.file.date.created": "তৈরি হয়েছে", - "source.file.date.updated": "শেষ আপডেট", - "tabs": "ট্যাব", - "toc": "সূচি তালিকা", - "top": "উপরে ফিরে যাও" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ca.html b/src/templates/partials/languages/ca.html deleted file mode 100644 index 8fd2b03a..00000000 --- a/src/templates/partials/languages/ca.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ca", - "action.edit": "Edita aquesta pàgina", - "action.skip": "Salta el contingut", - "action.view": "Visualitza el codi font", - "announce.dismiss": "No ho tornis a mostrar", - "blog.archive": "Arxiva", - "blog.categories": "Categories", - "blog.categories.in": "a", - "blog.continue": "Continua llegint", - "blog.draft": "Esborrany", - "blog.index": "Torna a l'índex", - "blog.meta": "Metadades", - "blog.references": "Enllaços relacionats", - "clipboard.copy": "Còpia al porta-retalls", - "clipboard.copied": "Copiat al porta-retalls", - "consent.accept": "Accepta", - "consent.manage": "Gestiona la configuració", - "consent.reject": "Rebutja", - "footer": "Peu de pàgina", - "footer.next": "Següent", - "footer.previous": "Anterior", - "header": "Capçalera", - "meta.comments": "Comentaris", - "meta.source": "Codi font", - "nav": "Navegació", - "readtime.one": "1 min de lectura", - "readtime.other": "# min de lectura", - "rss.created": "Canal RSS", - "rss.updated": "Canal RSS de contingut actualitzat", - "search": "Cerca", - "search.placeholder": "Cerca", - "search.share": "Comparteix", - "search.reset": "Neteja", - "search.result.initializer": "Inicialitzant cerca", - "search.result.placeholder": "Escriu per a començar a cercar", - "search.result.none": "Cap document coincideix", - "search.result.one": "1 document coincident", - "search.result.other": "# documents coincidents", - "search.result.more.one": "1 més en aquesta pàgina", - "search.result.more.other": "# més en aquesta pàgina", - "search.result.term.missing": "Desaparegut", - "select.language": "Selecciona la llengua", - "select.version": "Selecciona la versió", - "source": "Ves al repositori", - "source.file.contributors": "Col·laboradors", - "source.file.date.created": "Creada", - "source.file.date.updated": "Darrera actualització", - "tabs": "Pestanyes", - "toc": "Taula de continguts", - "top": "Torna a l'inici" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/cs.html b/src/templates/partials/languages/cs.html deleted file mode 100644 index fb955865..00000000 --- a/src/templates/partials/languages/cs.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "cs", - "action.edit": "Upravit tuto stránku", - "action.skip": "Přeskočit obsah", - "action.view": "Zobrazit zdroj této stránky", - "announce.dismiss": "Již nezobrazovat", - "blog.archive": "Archiv", - "blog.categories": "Kategorie", - "blog.categories.in": "v", - "blog.continue": "Pokračovat ve čtení", - "blog.draft": "Návrh", - "blog.index": "Zpět na index", - "blog.meta": "Metadata", - "blog.references": "Související odkazy", - "clipboard.copy": "Kopírovat do schránky", - "clipboard.copied": "Zkopírováno do schránky", - "consent.accept": "Akceptovat", - "consent.manage": "Spravovat nastavení", - "consent.reject": "Odmítnout", - "footer": "Zápatí", - "footer.next": "Další", - "footer.previous": "Předchozí", - "header": "Záhlaví", - "meta.comments": "Komentáře", - "meta.source": "Zdroj", - "nav": "Navigace", - "readtime.one": "1 min čtení", - "readtime.other": "# min čtení", - "rss.created": "RSS kanál", - "rss.updated": "RSS zdroj aktualizovaného obsahu", - "search": "Vyhledávání", - "search.placeholder": "Hledat", - "search.share": "Sdílet", - "search.reset": "Vyčistit", - "search.result.initializer": "Inicializace vyhledávání", - "search.result.placeholder": "Pište co se má vyhledat", - "search.result.none": "Nenalezeny žádné dokumenty", - "search.result.one": "Nalezený dokument: 1", - "search.result.other": "Nalezené dokumenty: #", - "search.result.more.one": "1 další na této stránce", - "search.result.more.other": "# více na této stránce", - "search.result.term.missing": "Chybějící", - "select.language": "Zvolte jazyk", - "select.version": "Vyberte verzi", - "source": "Přejít do repozitáře", - "source.file.contributors": "Přispěvatelé", - "source.file.date.created": "Vytvořeno", - "source.file.date.updated": "Poslední aktualizace", - "tabs": "Karty", - "toc": "Obsah", - "top": "Zpět na začátek" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/da.html b/src/templates/partials/languages/da.html deleted file mode 100644 index 2f9da2db..00000000 --- a/src/templates/partials/languages/da.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "da", - "action.edit": "Redigér denne side", - "action.skip": "Gå til indholdet", - "action.view": "Vis kildetekst på denne side", - "announce.dismiss": "Vis ikke dette igen", - "blog.archive": "Arkiv", - "blog.categories": "Kategorier", - "blog.categories.in": "i", - "blog.continue": "Læs mere", - "blog.draft": "Udkast", - "blog.index": "Gå tilbage", - "blog.meta": "Metadata", - "blog.references": "Relateret indhold", - "clipboard.copy": "Kopiér til udklipsholderen", - "clipboard.copied": "Kopieret til udklipsholderen", - "consent.accept": "Accepter", - "consent.manage": "Administrer indstillinger", - "consent.reject": "Afvis", - "footer": "Sidefod", - "footer.next": "Næste", - "footer.previous": "Forrige", - "header": "Sidehoved", - "meta.comments": "Kommentarer", - "meta.source": "Kilde", - "nav": "Navigation", - "readtime.one": "1 minuts læsetid", - "readtime.other": "# minuts læstid", - "rss.created": "RSS feed", - "rss.updated": "RSS feed af opdateret indhold", - "search": "Søg", - "search.config.lang": "da", - "search.placeholder": "Søg", - "search.share": "Del", - "search.reset": "Nulstil søgning", - "search.result.initializer": "Start søgning", - "search.result.placeholder": "Indtast søgeord", - "search.result.none": "Ingen resultater fundet", - "search.result.one": "1 resultat", - "search.result.other": "# resultater", - "search.result.more.one": "1 resultat mere på denne side", - "search.result.more.other": "# resultater mere på denne side", - "search.result.term.missing": "Mangler", - "select.language": "Vælg sprog", - "select.version": "Vælg version", - "source": "Åbn arkiv", - "source.file.contributors": "Bidragydere", - "source.file.date.created": "Oprettet", - "source.file.date.updated": "Sidste ændring", - "tabs": "Faner", - "toc": "Indholdsfortegnelse", - "top": "Tilbage til start" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/de.html b/src/templates/partials/languages/de.html deleted file mode 100644 index bfd8b909..00000000 --- a/src/templates/partials/languages/de.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "de", - "action.edit": "Seite editieren", - "action.skip": "Zum Inhalt", - "action.view": "Quellcode der Seite anzeigen", - "announce.dismiss": "Nicht mehr anzeigen", - "blog.archive": "Archiv", - "blog.categories": "Kategorien", - "blog.categories.in": "in", - "blog.continue": "Weiterlesen", - "blog.draft": "Entwurf", - "blog.index": "Zur Übersicht", - "blog.meta": "Metadaten", - "blog.references": "Weiterführende Links", - "clipboard.copy": "In Zwischenablage kopieren", - "clipboard.copied": "In Zwischenablage kopiert", - "consent.accept": "Akzeptieren", - "consent.manage": "Einstellungen", - "consent.reject": "Ablehnen", - "footer": "Fußzeile", - "footer.next": "Weiter", - "footer.previous": "Zurück", - "header": "Kopfzeile", - "meta.comments": "Kommentare", - "meta.source": "Quellcode", - "nav": "Navigation", - "readtime.one": "1 Min. Lesezeit", - "readtime.other": "# Min. Lesezeit", - "rss.created": "RSS Feed", - "rss.updated": "RSS Feed der aktualisierten Inhalte", - "search": "Suche", - "search.config.lang": "de", - "search.placeholder": "Suche", - "search.share": "Teilen", - "search.reset": "Zurücksetzen", - "search.result.initializer": "Suche wird initialisiert", - "search.result.placeholder": "Suchbegriff eingeben", - "search.result.none": "Keine Suchergebnisse", - "search.result.one": "1 Suchergebnis", - "search.result.other": "# Suchergebnisse", - "search.result.more.one": "1 weiteres Suchergebnis auf dieser Seite", - "search.result.more.other": "# weitere Suchergebnisse auf dieser Seite", - "search.result.term.missing": "Es fehlt", - "select.language": "Sprache wechseln", - "select.version": "Version auswählen", - "source": "Zum Repository", - "source.file.contributors": "Mitwirkende", - "source.file.date.created": "Erstellt", - "source.file.date.updated": "Letztes Update", - "tabs": "Hauptnavigation", - "toc": "Inhaltsverzeichnis", - "top": "Zurück zum Seitenanfang" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/el.html b/src/templates/partials/languages/el.html deleted file mode 100644 index 8dce1793..00000000 --- a/src/templates/partials/languages/el.html +++ /dev/null @@ -1,74 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "el", - "action.edit": "Επεξεργασία αυτής της σελίδας", - "action.skip": "Μετάβαση στο περιεχόμενο", - "action.view": "Προβολή πηγαίου κώδικα", - "announce.dismiss": "Μην το ξαναδείξετε αυτό", - "blog.archive": "Aρχείο", - "blog.categories": "Κατηγορίες", - "blog.categories.in": "Στο", - "blog.continue": "Περισσότερα", - "blog.draft": "Πρόχειρο", - "blog.index": "Eπιστροφή", - "blog.references": "Σχετικοί σύνδεσμοι", - "clipboard.copy": "Αντιγραφή στο πρόχειρο", - "clipboard.copied": "Αντιγράφηκε στο πρόχειρο", - "consent.accept": "Αποδοχή", - "consent.manage": "Περισσότερες επιλογές", - "consent.reject": "Απόρριψη", - "footer": "Υποσέλιδο", - "footer.next": "Επόμενο", - "footer.previous": "Προηγούμενο", - "header": "Κεφαλίδα", - "meta.comments": "Σχόλια", - "meta.source": "Πηγή", - "nav": "Πλοήγηση", - "readtime.one": "1 λεπτό διάβασμα", - "readtime.other": "# λεπτά διάβασμα", - "rss.created": "Ροές Δεδομένων RSS", - "rss.updated": "Ροές Δεδομένων RSS. Τελευταία νέα", - "search": "Αναζήτηση", - "search.placeholder": "Αναζήτηση", - "search.share": "Διαμοίραση", - "search.reset": "Καθαρισμός", - "search.result.initializer": "Αρχικοποίηση αναζήτησης", - "search.result.placeholder": "Πληκτρολογήστε για να αρχίσει η αναζήτηση", - "search.result.none": "δεν βρήκε κάποιο έγγραφο", - "search.result.one": "1 έγγραφο που ταιριάζει", - "search.result.other": "# έγγραφα που ταιριάζουν", - "search.result.more.one": "1 ακόμα σε αυτήν τη σελίδα", - "search.result.more.other": "# ακόμα σε αυτήν τη σελίδα", - "search.result.term.missing": "Λείπει", - "select.language": "Επιλογή γλώσσας", - "select.version": "Επιλογή έκδοσης", - "source": "Μετάβαση στο αποθετήριο", - "source.file.contributors": "Συνεισφέροντες", - "source.file.date.created": "Δημιουργήθηκε", - "source.file.date.updated": "τελευταία ενημέρωση", - "tabs": "Καρτέλες", - "toc": "Πίνακας περιεχομένων", - "top": "Επιστροφή στην αρχή" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/en.html b/src/templates/partials/languages/en.html deleted file mode 100644 index 0e6a73ac..00000000 --- a/src/templates/partials/languages/en.html +++ /dev/null @@ -1,79 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "en", - "direction": "ltr", - "action.edit": "Edit this page", - "action.skip": "Skip to content", - "action.view": "View source of this page", - "announce.dismiss": "Don't show this again", - "blog.archive": "Archive", - "blog.categories": "Categories", - "blog.categories.in": "in", - "blog.continue": "Continue reading", - "blog.draft": "Draft", - "blog.index": "Back to index", - "blog.meta": "Metadata", - "blog.references": "Related links", - "clipboard.copy": "Copy to clipboard", - "clipboard.copied": "Copied to clipboard", - "consent.accept": "Accept", - "consent.manage": "Manage settings", - "consent.reject": "Reject", - "footer": "Footer", - "footer.next": "Next", - "footer.previous": "Previous", - "header": "Header", - "meta.comments": "Comments", - "meta.source": "Source", - "nav": "Navigation", - "readtime.one": "1 min read", - "readtime.other": "# min read", - "rss.created": "RSS feed", - "rss.updated": "RSS feed of updated content", - "search": "Search", - "search.config.lang": "en", - "search.config.pipeline": "stopWordFilter", - "search.config.separator": "[\\s\\-]+", - "search.placeholder": "Search", - "search.share": "Share", - "search.reset": "Clear", - "search.result.initializer": "Initializing search", - "search.result.placeholder": "Type to start searching", - "search.result.none": "No matching documents", - "search.result.one": "1 matching document", - "search.result.other": "# matching documents", - "search.result.more.one": "1 more on this page", - "search.result.more.other": "# more on this page", - "search.result.term.missing": "Missing", - "select.language": "Select language", - "select.version": "Select version", - "source": "Go to repository", - "source.file.contributors": "Contributors", - "source.file.date.created": "Created", - "source.file.date.updated": "Last update", - "tabs": "Tabs", - "toc": "Table of contents", - "top": "Back to top" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/eo.html b/src/templates/partials/languages/eo.html deleted file mode 100644 index cd3829a8..00000000 --- a/src/templates/partials/languages/eo.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "eo", - "action.edit": "Redakti ĉi tiun paĝon", - "action.skip": "Saltu al enhavo", - "clipboard.copy": "Kopii al tondujo", - "clipboard.copied": "Kopiado al klipo", - "footer": "Piedlinio", - "footer.next": "Sekva", - "footer.previous": "Antaŭa", - "header": "Kaplinio", - "meta.comments": "Komentoj", - "meta.source": "Fontkodo", - "nav": "Navigado", - "search.config.lang": "es", - "search.placeholder": "Serĉo", - "search.reset": "Klara", - "search.result.placeholder": "Tajpu por komenci serĉadon", - "search.result.none": "Neniuj kongruaj dokumentoj", - "search.result.one": "1 kongrua dokumento", - "search.result.other": "# kongruaj dokumentoj", - "source": "Iru al deponejo", - "source.file.date.created": "Kreita", - "source.file.date.updated": "Lasta ĝisdatigo", - "tabs": "Langetoj", - "toc": "Enhavtabelo" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/es.html b/src/templates/partials/languages/es.html deleted file mode 100644 index bbbd9dc1..00000000 --- a/src/templates/partials/languages/es.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "es", - "action.edit": "Editar esta página", - "action.skip": "Saltar a contenido", - "action.view": "Ver código fuente de esta página", - "announce.dismiss": "No mostrar esto de nuevo", - "blog.archive": "Archivo", - "blog.categories": "Categorías", - "blog.categories.in": "en", - "blog.continue": "Seguir leyendo", - "blog.draft": "Borrador", - "blog.index": "Regresar al índice", - "blog.meta": "Metadata", - "blog.references": "Enlaces relacionados", - "clipboard.copy": "Copiar al portapapeles", - "clipboard.copied": "Copiado al portapapeles", - "consent.accept": "Aceptar", - "consent.manage": "Gestionar cookies", - "consent.reject": "Rechazar", - "footer": "Pie", - "footer.next": "Siguiente", - "footer.previous": "Anterior", - "header": "Cabecera", - "meta.comments": "Comentarios", - "meta.source": "Fuente", - "nav": "Navegación", - "readtime.one": "1 minuto de lectura", - "readtime.other": "# minutos de lectura", - "rss.created": "Fuente RSS", - "rss.updated": "Fuente RSS de contenido actualizado", - "search": "Buscar", - "search.config.lang": "es", - "search.placeholder": "Búsqueda", - "search.share": "Compartir", - "search.reset": "Limpiar", - "search.result.initializer": "Inicializando búsqueda", - "search.result.placeholder": "Teclee para comenzar búsqueda", - "search.result.none": "No se encontraron documentos", - "search.result.one": "1 documento encontrado", - "search.result.other": "# documentos encontrados", - "search.result.more.one": "1 más en esta página", - "search.result.more.other": "# más en esta página", - "search.result.term.missing": "Falta", - "select.language": "Seleccionar idioma", - "select.version": "Seleccionar versión", - "source": "Ir al repositorio", - "source.file.contributors": "Contribuidores", - "source.file.date.created": "Creado", - "source.file.date.updated": "Última actualización", - "tabs": "Pestañas", - "toc": "Tabla de contenidos", - "top": "Volver al principio" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/et.html b/src/templates/partials/languages/et.html deleted file mode 100644 index 8add4225..00000000 --- a/src/templates/partials/languages/et.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "et", - "action.edit": "Muuda seda lehte", - "action.skip": "Keri sisuni", - "clipboard.copy": "Kopeeri lõikelauale", - "clipboard.copied": "Kopeeritud", - "footer.next": "Järgmine", - "footer.previous": "Eelmine", - "meta.comments": "Kommentaarid", - "meta.source": "Lähtekood", - "search.placeholder": "Otsi", - "search.result.placeholder": "Otsimiseks kirjuta siia", - "search.result.none": "Otsingule ei leitud ühtegi vastet", - "search.result.one": "Leiti üks tulemus", - "search.result.other": "Leiti # tulemust", - "source": "Ava repositooriumis", - "source.file.date.created": "Loodud", - "source.file.date.updated": "Viimane uuendus", - "toc": "Sisukord" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/eu.html b/src/templates/partials/languages/eu.html deleted file mode 100644 index 0e52f925..00000000 --- a/src/templates/partials/languages/eu.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "eu", - "action.edit": "Editatu orri hau", - "action.skip": "Joan zuzenean edukira", - "action.view": "Ikusi orri honen iturburua", - "announce.dismiss": "Ez erakutsi hau berriro", - "blog.archive": "Artxiboa", - "blog.categories": "Kategoriak", - "blog.categories.in": "kategoria", - "blog.continue": "Jarraitu irakurtzen", - "blog.draft": "Zirriborroa", - "blog.index": "Itzuli aurkibidera", - "blog.meta": "Metadatuak", - "blog.references": "Erlazionatutako estekak", - "clipboard.copy": "Kopiatu arbelean", - "clipboard.copied": "Arbelean kopiatuta", - "consent.accept": "Onartu", - "consent.manage": "Kudeatu ezarpenak", - "consent.reject": "Ukatu", - "footer": "Orri-oina", - "footer.next": "Hurrengoa", - "footer.previous": "Aurrekoa", - "header": "Atalburua", - "meta.comments": "Iruzkinak", - "meta.source": "Iturburua", - "nav": "Nabigazioa", - "readtime.one": "Minutu batean irakurtzeko", - "readtime.other": "# minututan irakurtzeko", - "rss.created": "RSS jarioa", - "rss.updated": "Eduki eguneratuen RSS jarioa", - "search": "Bilatu", - "search.placeholder": "Bilatu", - "search.share": "Partekatu", - "search.reset": "Garbitu", - "search.result.initializer": "Bilaketa hasieratzen", - "search.result.placeholder": "Idatzi bilatzen hasteko", - "search.result.none": "Bat datorren dokumenturik ez", - "search.result.one": "Bat datorren dokumentu bat", - "search.result.other": "Bat datozen # dokumentu", - "search.result.more.one": "Bat gehiago orri honetan", - "search.result.more.other": "# gehiago orri honetan", - "search.result.term.missing": "Falta da", - "select.language": "Hautatu hizkuntza", - "select.version": "Hautatu bertsioa", - "source": "Joan biltegira", - "source.file.contributors": "Kolaboratzaileak", - "source.file.date.created": "Sortze data", - "source.file.date.updated": "Azken eguneratzea", - "tabs": "Fitxak", - "toc": "Edukiak", - "top": "Igo goraino" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fa.html b/src/templates/partials/languages/fa.html deleted file mode 100644 index deaa8bca..00000000 --- a/src/templates/partials/languages/fa.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "fa", - "direction": "rtl", - "action.edit": "این صفحه را ویرایش کنید", - "action.skip": "پرش به محتویات", - "action.view": "محتویات این صفحه را نشان بده", - "announce.dismiss": "این را دیگر نشان نده", - "blog.archive": "بایگانی", - "blog.categories": "دسته‌بندی‌ها", - "blog.categories.in": "در", - "blog.continue": "ادامه به خواندن", - "blog.draft": "پیش‌نویس", - "blog.index": "برگشت به فهرست", - "blog.meta": "فراداده", - "blog.references": "پیوندهای مربوط", - "clipboard.copy": "کپی کردن", - "clipboard.copied": "کپی شد", - "consent.accept": "تایید", - "consent.manage": "مدیریت تنظیمات", - "consent.reject": "رد کردن", - "footer": "پاورقی", - "footer.next": "بعدی", - "footer.previous": "قبلی", - "header": "سرتیتر", - "meta.comments": "نظرات", - "meta.source": "منبع", - "nav": "هدایت", - "readtime.one": "1 دقیقه زمان خواندن", - "readtime.other": "# دقیقه زمان خواندن", - "rss.created": "خوراک آراس‌اس", - "rss.updated": "خوراک آراساس محتویات به‌روز شده", - "search": "جستجو", - "search.config.pipeline": " ", - "search.placeholder": "جستجو", - "search.share": "هم‌رسانی", - "search.reset": "بازنشانی", - "search.result.initializer": "راه‌اندازی جستجو", - "search.result.placeholder": "برای شروع جستجو تایپ کنید", - "search.result.none": "سندی یافت نشد", - "search.result.one": "1 سند یافت شد", - "search.result.other": "# سند یافت شد", - "search.result.more.one": "1 مورد دیگر در این صفحه", - "search.result.more.other": "# مورد دیگر در این صفحه", - "search.result.term.missing": "موجود نیست", - "select.language": "انتخاب زبان", - "select.version": "انتخاب ویرایش", - "source": "رفتن به مخزن", - "source.file.contributors": "مشارکت کنندگان", - "source.file.date.created": "ایجاد شده", - "source.file.date.updated": "اخرین بروزرسانی", - "tabs": "زبانه‌ها", - "toc": "فهرست موضوعات", - "top": "برگشت به بالا" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fi.html b/src/templates/partials/languages/fi.html deleted file mode 100644 index 8ee09122..00000000 --- a/src/templates/partials/languages/fi.html +++ /dev/null @@ -1,44 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "fi", - "action.edit": "Muokkaa tätä sivua", - "action.skip": "Hyppää sisältöön", - "clipboard.copy": "Kopioi leikepöydälle", - "clipboard.copied": "Kopioitu leikepöydälle", - "footer.next": "Seuraava", - "footer.previous": "Edellinen", - "meta.comments": "Kommentit", - "meta.source": "Lähdekodi", - "search.config.lang": "fi", - "search.placeholder": "Hae", - "search.result.placeholder": "Kirjoita aloittaaksesi haun", - "search.result.none": "Ei täsmääviä dokumentteja", - "search.result.one": "1 täsmäävä dokumentti", - "search.result.other": "# täsmäävää dokumenttia", - "source": "Mene repositoryyn", - "source.file.date.created": "Luotu", - "source.file.date.updated": "Viimeisin päivitys", - "toc": "Sisällysluettelo" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/fr.html b/src/templates/partials/languages/fr.html deleted file mode 100644 index 9d49535a..00000000 --- a/src/templates/partials/languages/fr.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "fr", - "action.edit": "Editer cette page", - "action.skip": "Aller au contenu", - "action.view": "Consulter la source de cette page", - "announce.dismiss": "Ne plus montrer cela", - "blog.archive": "Archive", - "blog.categories": "Catégories", - "blog.categories.in": "dans", - "blog.continue": "Continuer à lire", - "blog.draft": "Brouillon", - "blog.index": "Retourner à l'index", - "blog.meta": "Metadonnées", - "blog.references": "Liens connexes", - "clipboard.copy": "Copier dans le presse-papier", - "clipboard.copied": "Copié dans le presse-papier", - "consent.accept": "Accepter", - "consent.manage": "Paramétrer vos choix", - "consent.reject": "Refuser", - "footer": "Pied de page", - "footer.next": "Suivant", - "footer.previous": "Précédent", - "header": "En-tête", - "meta.comments": "Commentaires", - "meta.source": "Source", - "nav": "Navigation", - "readtime.one": "1 min de lecture", - "readtime.other": "# min de lecture", - "rss.created": "Flux RSS", - "rss.updated": "Flux RSS du contenu mis à jour", - "search": "Recherche", - "search.config.lang": "fr", - "search.placeholder": "Rechercher", - "search.share": "Partager", - "search.reset": "Effacer", - "search.result.initializer": "Initialisation de la recherche", - "search.result.placeholder": "Taper pour démarrer la recherche", - "search.result.none": "Aucun document trouvé", - "search.result.one": "1 document trouvé", - "search.result.other": "# documents trouvés", - "search.result.more.one": "1 de plus sur cette page", - "search.result.more.other": "# de plus sur cette page", - "search.result.term.missing": "Non trouvé", - "select.language": "Sélectionner la langue", - "select.version": "Sélectionner la version", - "source": "Aller au dépôt", - "source.file.contributors": "Contributeurs", - "source.file.date.created": "Créé", - "source.file.date.updated": "Dernière mise à jour", - "tabs": "Onglets", - "toc": "Table des matières", - "top": "Retour en haut de la page" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/gl.html b/src/templates/partials/languages/gl.html deleted file mode 100644 index ecb54ffd..00000000 --- a/src/templates/partials/languages/gl.html +++ /dev/null @@ -1,56 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "gl", - "action.edit": "Editar esta páxina", - "action.skip": "Ir ao contido", - "clipboard.copy": "Copiar no cortapapeis", - "clipboard.copied": "Copiado no cortapapeis", - "footer": "Pé", - "footer.next": "Seguinte", - "footer.previous": "Anterior", - "header": "Cabeceira", - "meta.comments": "Comentarios", - "meta.source": "Fonte", - "nav": "Navegación", - "search.config.lang": "es", - "search.placeholder": "Procura", - "search.reset": "Limpar", - "search.result.initializer": "Inicializando procura", - "search.result.placeholder": "Insira un termo", - "search.result.none": "Sen resultados", - "search.result.one": "1 resultado atopado", - "search.result.other": "# resultados atopados", - "search.result.more.one": "1 máis nesta páxina", - "search.result.more.other": "# máis nesta páxina", - "search.result.term.missing": "Falta", - "select.language": "Seleccionar idioma", - "select.version": "Seleccionar version", - "source": "Ir ao repositorio", - "source.file.date.created": "Creada", - "source.file.date.updated": "Última actualización", - "tabs": "Pestanas", - "toc": "Táboa de contidos", - "top": "Volver ao principio" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/he.html b/src/templates/partials/languages/he.html deleted file mode 100644 index 128edab5..00000000 --- a/src/templates/partials/languages/he.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "he", - "direction": "rtl", - "action.edit": "עריכת הדף הזה", - "action.skip": "לדלג לתוכן", - "action.view": "צפה במקור של דף זה", - "announce.dismiss": "לא להציג את זה שוב", - "blog.archive": "ארכיון", - "blog.categories": "קטגוריות", - "blog.categories.in": "בתוך", - "blog.continue": "המשך לקרוא", - "blog.draft": "טיוטה", - "blog.index": "חזרה לאינדקס", - "blog.meta": "מטא-נתונים", - "blog.references": "קישורים קשורים", - "clipboard.copy": "העתקה ללוח", - "clipboard.copied": "הועתק ללוח", - "consent.accept": "לקבל", - "consent.manage": "לנהל הגדרות", - "consent.reject": "לדחות", - "footer": "כותרת תחתונה", - "footer.next": "הבא", - "footer.previous": "הקודם", - "header": "כותרת עליונה", - "meta.comments": "הערות", - "meta.source": "מקור", - "nav": "ניווט", - "readtime.one": "קריאה 1 דקות", - "readtime.other": "# דקות קריאה", - "rss.created": "RSS הזנת", - "rss.updated": "הזנת RSS של תוכן מעודכן", - "search": "חיפוש", - "search.config.pipeline": " ", - "search.placeholder": "חיפוש", - "search.share": "שיתוף", - "search.reset": "ניקוי", - "search.result.initializer": "אתחול חיפוש", - "search.result.placeholder": "יש להקליד כדי להתחיל לחפש", - "search.result.none": "אין מסמכים תואמים", - "search.result.one": "מסמך1 תואם", - "search.result.other": "# מסמך תואם", - "search.result.more.one": "עוד אחד בדף הזה", - "search.result.more.other": "עוד # בדף הזה", - "search.result.term.missing": "חסר", - "select.language": "בחירת שפה", - "select.version": "בחירת גרסה", - "source": "לעבור אל המאגר", - "source.file.contributors": "תורמים", - "source.file.date.created": "נוצר", - "source.file.date.updated": "עדכון אחרון", - "tabs": "לשוניות", - "toc": "תוכן העניינים", - "top": "חזרה למעלה" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hi.html b/src/templates/partials/languages/hi.html deleted file mode 100644 index 8c27c259..00000000 --- a/src/templates/partials/languages/hi.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "hi", - "action.edit": "इस पृष्ठ को संपादित करें", - "action.skip": "विषय पर बढ़ें", - "action.view": "इस पृष्ठ का सूत्र देखें", - "announce.dismiss": "इसे पुनः न दिखायें", - "blog.archive": "पुरालेख", - "blog.categories": "वर्ग", - "blog.categories.in": "में", - "blog.continue": "पढ़ते रहिये", - "blog.draft": "मसौदा", - "blog.index": "सूचि को लौटें", - "blog.meta": "मेटाडेटा", - "blog.references": "सम्बंधित लिंक", - "clipboard.copy": "क्लिपबोर्ड पर कॉपी करें", - "clipboard.copied": "क्लिपबोर्ड पर कॉपी कर दिया गया", - "consent.accept": "स्वीकार करें", - "consent.manage": "सेटिंग्स मैनेज करें", - "consent.reject": "अस्वीकार करें", - "footer": "फुटर", - "footer.next": "आगामी", - "footer.previous": "पिछला", - "header": "शीर्षक", - "meta.comments": "टिप्पणियाँ", - "meta.source": "स्रोत", - "nav": "नैविगेशन", - "readtime.one": "1 मिनट पढ़ने को", - "readtime.other": "# मिनट पढ़ने को", - "rss.created": "RSS फीड", - "rss.updated": "नवीनतम विषयवस्तु का RSS feed", - "search": "खोजें", - "search.config.lang": "hi", - "search.placeholder": "खोज", - "search.share": "शेयर करें", - "search.reset": "हटा दें", - "search.result.initializer": "खोज शुरू करें", - "search.result.placeholder": "खोज शुरू करने के लिए टाइप करें", - "search.result.none": "कोई मिलान डॉक्यूमेंट नहीं", - "search.result.one": "1 मिलान डॉक्यूमेंट", - "search.result.other": "# मिलान डाक्यूमेंट्स", - "search.result.more.one": "1 और इस पृष्ठ पर", - "search.result.more.other": "# और इस पृष्ठ पर", - "search.result.term.missing": "ग़ायब", - "select.language": "भाषा चुनें", - "select.version": "वर्शन चुनें", - "source": "रिपॉजिटरी पर जाएं", - "source.file.contributors": "योगदाता", - "source.file.date.created": "बनाया था", - "source.file.date.updated": "आखिरी अपडेट", - "tabs": "टैब", - "toc": "विषय - सूची", - "top": "शीर्षभाग पर लौटें" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hr.html b/src/templates/partials/languages/hr.html deleted file mode 100644 index 30afe6a9..00000000 --- a/src/templates/partials/languages/hr.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "hr", - "action.edit": "Uredi stranicu", - "action.skip": "Preskoči na sadržaj", - "action.view": "Pregledaj izvorni kod ove stranice", - "announce.dismiss": "Ne prikazuj ovo opet", - "blog.archive": "Arhiva", - "blog.categories": "Kategorije", - "blog.categories.in": "u", - "blog.continue": "Nastavi čitati", - "blog.draft": "Nacrt", - "blog.index": "Natrag na indeks", - "blog.meta": "Metapodaci", - "blog.references": "Srodne poveznice", - "clipboard.copy": "Kopiraj u međuspremnik", - "clipboard.copied": "Kopirano u međuspremnik", - "consent.accept": "Prihvati", - "consent.manage": "Upravljaj postavkama", - "consent.reject": "Odbij", - "footer": "Podnožje", - "footer.next": "Sljedeće", - "footer.previous": "Prethodno", - "header": "Zaglavlje", - "meta.comments": "Komentari", - "meta.source": "Izvor", - "nav": "Navigacija", - "readtime.one": "1 minuta čitanja", - "readtime.other": "# minut(a/e) čitanja", - "rss.created": "RSS izvor", - "rss.updated": "RSS izvor osvježenog sadržaja", - "search": "Pretraživanje", - "search.placeholder": "Pretraži", - "search.share": "Podijeli", - "search.reset": "Očisti", - "search.result.initializer": "Inicijaliziranje pretraživanja", - "search.result.placeholder": "Unesite pojam pretraživanja", - "search.result.none": "Ništa nije pronađeno", - "search.result.one": "1 rezultat pretraživanja", - "search.result.other": "# rezultat(a) pretraživanja", - "search.result.more.one": "1 rezultat na ovoj stranici", - "search.result.more.other": "# rezultat(a) na ovoj stranici", - "search.result.term.missing": "Nedostaje", - "select.language": "Odabir jezika", - "select.version": "Odabir verzije", - "source": "Idi u repozitorij", - "source.file.contributors": "Suradnici", - "source.file.date.created": "Stvaranje", - "source.file.date.updated": "Posljednje ažuriranje", - "tabs": "Kartice", - "toc": "Sadržaj", - "top": "Vrati se na vrh" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hu.html b/src/templates/partials/languages/hu.html deleted file mode 100644 index 44798dd8..00000000 --- a/src/templates/partials/languages/hu.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "hu", - "action.edit": "Oldal szerkesztése", - "action.skip": "Kihagyás", - "action.view": "Oldal forrásának megtekintése", - "announce.dismiss": "Ne mutasd többet", - "blog.archive": "Archívum", - "blog.categories": "Kategóriák", - "blog.categories.in": "Kategória:", - "blog.continue": "Folytatás", - "blog.draft": "Piszkozat", - "blog.index": "Vissza a főoldalra", - "blog.meta": "Metaadat", - "blog.references": "Kapcsolódó linkek", - "clipboard.copy": "Másolás vágólapra", - "clipboard.copied": "Vágólapra másolva", - "consent.accept": "Elfogadás", - "consent.manage": "Beállítások", - "consent.reject": "Visszautasítás", - "footer": "Élőláb", - "footer.next": "Következő", - "footer.previous": "Előző", - "header": "Élőfej", - "meta.comments": "Hozzászólások", - "meta.source": "Forrás", - "nav": "Navigáció", - "readtime.one": "1 percnyi", - "readtime.other": "# percnyi", - "rss.created": "RSS feed", - "rss.updated": "Frissített tartalom RSS feedje", - "search": "Keresés", - "search.config.lang": "hu", - "search.placeholder": "Keresés", - "search.share": "Megosztás", - "search.reset": "Törlés", - "search.result.initializer": "Keresés inicializálása", - "search.result.placeholder": "Kereséshez írj ide valamit", - "search.result.none": "Nincs találat", - "search.result.one": "1 egyező dokumentum", - "search.result.other": "# egyező dokumentum", - "search.result.more.one": "1 további találat az oldalon", - "search.result.more.other": "# további találat az oldalon", - "search.result.term.missing": "Üres", - "select.language": "Nyelvváltás", - "select.version": "Verzióváltás", - "source": "Főoldalra ugrás", - "source.file.contributors": "Szerzők", - "source.file.date.created": "Létrehozva", - "source.file.date.updated": "Utolsó frissítés", - "tabs": "Lapok", - "toc": "Tartalomjegyzék", - "top": "Vissza a tetejére" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/hy.html b/src/templates/partials/languages/hy.html deleted file mode 100644 index e3418341..00000000 --- a/src/templates/partials/languages/hy.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "hy", - "action.edit": "Խմբագրել այս էջը", - "action.skip": "Անցնել պարունակությանը", - "action.view": "Դիտել այս էջի սկզբնաղբյուրը", - "announce.dismiss": "Այլևս չցուցադրել", - "blog.archive": "Արխիվ", - "blog.categories": "Կատեգորիաներ", - "blog.categories.in": "in", - "blog.continue": "Շարունակել կարդալ", - "blog.draft": "Սևագիր", - "blog.index": "Հետ դեպի ինդեքս", - "blog.meta": "Մետատվյալներ", - "blog.references": "Առնչվող հղումներ", - "clipboard.copy": "Պատճենել", - "clipboard.copied": "Պատճենված է", - "consent.accept": "Ընդունել", - "consent.manage": "Կառավարել կարգավորումները", - "consent.reject": "Մերժել", - "footer": "Էջատակ", - "footer.next": "Հաջորդը", - "footer.previous": "Նախորդը", - "header": "Գլխագիր", - "meta.comments": "Մեկնաբանությունները", - "meta.source": "Աղբյուր", - "nav": "Տեղորոշում", - "readtime.one": "Ընթերցում՝ 1 րոպե", - "readtime.other": "Ընթերցում՝ # րոպե", - "rss.created": "RSS հոսք", - "rss.updated": "Արդիացված բովանդակության RSS հոսք", - "search": "Որոնում", - "search.config.pipeline": " ", - "search.placeholder": "Որոնում", - "search.share": "Կիսվել", - "search.reset": "Ջնջել", - "search.result.initializer": "Որոնում", - "search.result.placeholder": "Մուտքագրեք որոնելու համար", - "search.result.none": "Արդյունքներ չկան", - "search.result.one": "1 արդյունք", - "search.result.other": "# արդյունք", - "search.result.more.one": "ևս 1-ը այս էջում", - "search.result.more.other": "ևս #-ը այս էջում", - "search.result.term.missing": "Բացակայում է", - "select.language": "Ընտրել լեզուն", - "select.version": "Ընտրել տարբերակը", - "source": "Դեպի պահոց", - "source.file.contributors": "Հեղինակողներ", - "source.file.date.created": "Ստեղծված է", - "source.file.date.updated": "Վերջին թարմացումը", - "tabs": "Ներդիրներ", - "toc": "Բովանդակություն", - "top": "Վերադառնալ սկիզբ" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/id.html b/src/templates/partials/languages/id.html deleted file mode 100644 index c54229f0..00000000 --- a/src/templates/partials/languages/id.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "id", - "action.edit": "Ubah halaman ini", - "action.skip": "Lewati ke isi", - "action.view": "Lihat sumber halaman ini", - "announce.dismiss": "Jangan lihat ini lagi", - "blog.archive": "Arsip", - "blog.categories": "Kategori", - "blog.categories.in": "dalam", - "blog.continue": "Lanjut membaca", - "blog.draft": "Draf", - "blog.index": "Kembali ke indeks", - "blog.meta": "Metadata", - "blog.references": "Tautan yang berhubungan", - "clipboard.copy": "Salin ke clipboard", - "clipboard.copied": "Tersalin ke clipboard", - "consent.accept": "Terima", - "consent.manage": "Kelola pengaturan", - "consent.reject": "Tolak", - "footer": "Footer", - "footer.next": "Selanjutnya", - "footer.previous": "Sebelumnya", - "header": "Header", - "meta.comments": "Komentar", - "meta.source": "Sumber", - "nav": "Navigasi", - "readtime.one": "1 menit baca", - "readtime.other": "# menit baca", - "rss.created": "Umpan RSS", - "rss.updated": "Umpan RSS dari konten yang diperbarui", - "search": "Cari", - "search.config.pipeline": " ", - "search.placeholder": "Cari", - "search.share": "Bagikan", - "search.reset": "Kosongkan", - "search.result.initializer": "Mempersiapkan pencarian", - "search.result.placeholder": "Ketik untuk mulai pencarian", - "search.result.none": "Tidak ada dokumen yang sesuai", - "search.result.one": "1 dokumen ditemukan", - "search.result.other": "# dokumen ditemukan", - "search.result.more.one": "1 lagi di halaman ini", - "search.result.more.other": "# lagi di halaman ini", - "search.result.term.missing": "Tidak ada", - "select.language": "Pilih bahasa", - "select.version": "Pilih versi", - "source": "Ke repositori", - "source.file.contributors": "Kontributor", - "source.file.date.created": "Dibuat", - "source.file.date.updated": "Pembaruan terakhir", - "tabs": "Tab", - "toc": "Daftar isi", - "top": "Kembali ke atas" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/is.html b/src/templates/partials/languages/is.html deleted file mode 100644 index 5b9a47e8..00000000 --- a/src/templates/partials/languages/is.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "is", - "action.edit": "Breyta þessari síðu", - "action.skip": "Hoppa yfir í efnið", - "action.view": "Skoða frumgögn þessarar síðu", - "announce.dismiss": "Ekki sýna þetta aftur", - "blog.archive": "Safn", - "blog.categories": "Flokkar", - "blog.categories.in": "í", - "blog.continue": "Lesa meira", - "blog.draft": "Uppkast", - "blog.index": "Til baka í yfirlit", - "blog.meta": "Lýsigögn", - "blog.references": "Þessu tengt", - "clipboard.copy": "Afrita á klemmuspjald", - "clipboard.copied": "Afritað á klemmuspjald", - "consent.accept": "Samþykkja", - "consent.manage": "Breyta stillingum", - "consent.reject": "Hafna", - "footer": "Síðufótur", - "footer.next": "Næsta", - "footer.previous": "Fyrri", - "header": "Haus", - "meta.comments": "Umræður", - "meta.source": "Frumgögn", - "nav": "Valmynd", - "readtime.one": "1 mín lestur", - "readtime.other": "# mín lestur", - "rss.created": "RSS veita", - "rss.updated": "RSS veita fyrir uppfært innihald", - "search": "Leita", - "search.placeholder": "Leita", - "search.share": "Deila", - "search.reset": "Hreinsa", - "search.result.initializer": "Ræsi leitarvél", - "search.result.placeholder": "Byrjaðu að skrifa til að hefja leit", - "search.result.none": "Engar síður fundust", - "search.result.one": "1 síða fannst", - "search.result.other": "# síður fundust", - "search.result.more.one": "1 til viðbótar á þessari síðu", - "search.result.more.other": "# til viðbótar á þessari síðu", - "search.result.term.missing": "Vantar", - "select.language": "Veldu tungumál", - "select.version": "Veldu útgáfu", - "source": "Fara í gagnageymslu", - "source.file.contributors": "Meðhöfundar", - "source.file.date.created": "Búið til", - "source.file.date.updated": "Síðast uppfært", - "tabs": "Flipar", - "toc": "Efnisyfirlit", - "top": "Fara aftur efst" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/it.html b/src/templates/partials/languages/it.html deleted file mode 100644 index 77956ee7..00000000 --- a/src/templates/partials/languages/it.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "it", - "action.edit": "Modifica", - "action.skip": "Vai al contenuto", - "action.view": "Vedi il sorgente di questa pagina", - "announce.dismiss": "Non mostrare più", - "blog.archive": "Archivio", - "blog.categories": "Categorie", - "blog.categories.in": "in", - "blog.continue": "Continua a leggere", - "blog.draft": "Bozza", - "blog.index": "Torna all'indice", - "blog.meta": "Metadati", - "blog.references": "Collegamenti", - "clipboard.copy": "Copia", - "clipboard.copied": "Copiato", - "consent.accept": "Accetta", - "consent.manage": "Gestisci le opzioni", - "consent.reject": "Rifiuta", - "footer": "Piede", - "footer.next": "Successivo", - "footer.previous": "Precedente", - "header": "Intestazione", - "meta.comments": "Commenti", - "meta.source": "Sorgente", - "nav": "Navigazione", - "readtime.one": "1 minuto di lettura", - "readtime.other": "# minuti di lettura", - "rss.created": "Feed RSS", - "rss.updated": "Contenuto aggiornato del feed RSS", - "search": "Cerca", - "search.config.lang": "it", - "search.placeholder": "Cerca", - "search.share": "Condividi", - "search.reset": "Cancella", - "search.result.initializer": "Inizializza la ricerca", - "search.result.placeholder": "Scrivi per iniziare a cercare", - "search.result.none": "Nessun documento trovato", - "search.result.one": "1 documento trovato", - "search.result.other": "# documenti trovati", - "search.result.more.one": "1 altro in questa pagina", - "search.result.more.other": "# altri in questa pagina", - "search.result.term.missing": "Non presente", - "select.language": "Seleziona la lingua", - "select.version": "Seleziona la versione", - "source": "Apri repository", - "source.file.contributors": "Contributori", - "source.file.date.created": "Creata", - "source.file.date.updated": "Ultimo aggiornamento", - "tabs": "Tabs", - "toc": "Indice", - "top": "Torna su" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ja.html b/src/templates/partials/languages/ja.html deleted file mode 100644 index b4b4279d..00000000 --- a/src/templates/partials/languages/ja.html +++ /dev/null @@ -1,78 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ja", - "action.edit": "編集", - "action.skip": "コンテンツにスキップ", - "action.view": "このページの原文を表示", - "announce.dismiss": "非表示にします", - "blog.archive": "過去の投稿", - "blog.categories": "カテゴリー", - "blog.categories.in": "", - "blog.continue": "続きを読む", - "blog.draft": "下書き", - "blog.index": "ブログトップへ戻る", - "blog.meta": "メタデータ", - "blog.references": "関連リンク", - "clipboard.copy": "クリップボードへコピー", - "clipboard.copied": "コピーしました", - "consent.accept": "同意", - "consent.manage": "サイトの設定", - "consent.reject": "拒否", - "footer": "フッター", - "footer.next": "次", - "footer.previous": "前", - "header": "ヘッダー", - "meta.comments": "コメント", - "meta.source": "ソース", - "nav": "ナビゲーション", - "readtime.one": "このページは約1分で読めます", - "readtime.other": "このページは約#分で読めます", - "rss.created": "新しいページのRSSフィード", - "rss.updated": "更新されたページのRSSフィード", - "search": "検索", - "search.config.lang": "ja", - "search.config.pipeline": "stemmer", - "search.config.separator": "[\\s\\- 、。,.]+", - "search.placeholder": "検索", - "search.share": "共有", - "search.reset": "クリア", - "search.result.initializer": "検索を初期化", - "search.result.placeholder": "検索キーワードを入力してください", - "search.result.none": "何も見つかりませんでした", - "search.result.one": "1件見つかりました", - "search.result.other": "#件見つかりました", - "search.result.more.one": "このページ内にもう1件見つかりました", - "search.result.more.other": "このページ内にあと#件見つかりました", - "search.result.term.missing": "検索に含まれない", - "select.language": "言語切り替え", - "select.version": "バージョン切り替え", - "source": "リポジトリへ", - "source.file.contributors": "投稿者", - "source.file.date.created": "作成日", - "source.file.date.updated": "最終更新日", - "tabs": "タブ", - "toc": "目次", - "top": "ページトップへ戻る" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ka.html b/src/templates/partials/languages/ka.html deleted file mode 100644 index edfd2e02..00000000 --- a/src/templates/partials/languages/ka.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ka", - "action.edit": "გვერდის რედარქირება", - "action.skip": "კონტენტზე გადასვლა", - "clipboard.copy": "კოპირება", - "clipboard.copied": "კოპირებულია", - "footer.next": "შემდეგი", - "footer.previous": "წინა", - "meta.comments": "კომენტარები", - "meta.source": "წყარო", - "nav": "ნავიგაცია", - "search.config.pipeline": " ", - "search.placeholder": "ძებნა", - "search.reset": "გასუფთავება", - "search.result.placeholder": "ჩაწერე ძებნის დასაწყებად", - "search.result.none": "დოკუმენტი ვერ მოიძებნა", - "search.result.one": "მოიძებნა 1 დოკუმენტი", - "search.result.other": "მოიძებნა # დოკუმენტი", - "search.result.more.one": "კიდევ 1 ამ გვერდზე", - "search.result.more.other": "კიდევ # ამ გვერდზე", - "source": "საცავში გადასვლა", - "source.file.date.created": "შეიქმნა", - "source.file.date.updated": "ბოლო განახლება", - "tabs": "ტაბები", - "toc": "სარჩევი" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/kn.html b/src/templates/partials/languages/kn.html deleted file mode 100644 index bd0ff722..00000000 --- a/src/templates/partials/languages/kn.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "kn", - "action.edit": "ಈ ಪುಟವನ್ನು ತಿದ್ದುಪಡಿ ಮಾಡಿ", - "action.skip": "ವಿಷಯಕ್ಕೆ ತೆರಳಿ", - "action.view": "ಈ ಪುಟದ ಮೂಲವನ್ನು ವೀಕ್ಷಿಸಿ", - "announce.dismiss": "ಇದನ್ನು ಮತ್ತೊಮ್ಮೆ ತೋರಿಸಬೇಡಿ", - "blog.archive": "ಹಳೆಯ ಲೇಖನ", - "blog.categories": "ವರ್ಗಗಳು", - "blog.categories.in": "ರಲ್ಲಿ", - "blog.continue": "ಓದು ಮುಂದುವರೆಸಿ", - "blog.draft": "ಆರಂಭಿಕ ಬರವಣಿಗೆ", - "blog.index": "ಸೂಚ್ಯಂಕಕ್ಕೆ ಹಿಂತಿರುಗಿ", - "blog.meta": "ಮಾಹಿತಿಯ ಬಗ್ಗೆ ಮಾಹಿತಿ", - "blog.references": "ಸಂಬಂಧಿತ ಉಲ್ಲೇಖಗಳು", - "clipboard.copy": "ಇದನ್ನು ನಕಲಿಸಿ", - "clipboard.copied": "ಇದನ್ನು ನಕಲು ಮಾಡಿದೆ", - "consent.accept": "ನಾನು ಇದನ್ನು ಒಪ್ಪಿಕೊಳ್ಳುತ್ತೇನೆ", - "consent.manage": "ಸಂರಚನೆಯನ್ನು ನಿರ್ವಹಿಸಿ", - "consent.reject": "ನಾನು ಇದನ್ನು ತಿರಸ್ಕರಿಸುತ್ತೇನೆ", - "footer": "ಅಡಿಟಿಪ್ಪಣಿ", - "footer.next": "ಮುಂದಿನ ಸಂಚಿಕೆ", - "footer.previous": "ಹಿಂದಿನ ಸಂಚಿಕೆ", - "header": "ಮೇಲ್ಟಿಪ್ಪಣಿ", - "meta.comments": "ಪ್ರತಿಕ್ರಿಯೆಗಳು", - "meta.source": "ಮೂಲ", - "nav": "ಸಂಚರಣೆ", - "readtime.one": "ಓದಲು ೧ ನಿಮಿಷ ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", - "readtime.other": "ಓದಲು # ನಿಮಿಷಗಳನ್ನು ತೆಗೆದುಕೊಳ್ಳುತ್ತದೆ", - "rss.created": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆ", - "rss.updated": "ಆರ್ಎಸ್ಎಸ್ ಸೇವೆಯಿಂದ ಇತ್ತೀಚಿನ ನವೀಕರಣ", - "search": "ಹುಡುಕಿ", - "search.placeholder": "ಹುಡುಕಿ", - "search.share": "ಹಂಚಿಕೊಳ್ಳಿ", - "search.reset": "ಅಳಿಸು", - "search.result.initializer": "ಹುಡುಕಾಟವನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗುತ್ತಿದೆ", - "search.result.placeholder": "ಬರೆಯುವ ಮೂಲಕ ಹುಡುಕಲು ಪ್ರಾರಂಭಿಸಿ", - "search.result.none": "ಹೊಂದಾಣಿಕೆಯಾಗುವ ದಾಖಲೆಗಳಿಲ್ಲ", - "search.result.one": "೧ ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಯಿದೆ", - "search.result.other": "# ಹೊಂದಾಣಿಕೆಯ ದಾಖಲೆಗಳಿವೆ", - "search.result.more.one": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ ಒಂದು ಕಂಡುಬಂದಿದೆ", - "search.result.more.other": "ಈ ಪುಟದಲ್ಲಿ ಇನ್ನೂ # ಇವೆ", - "search.result.term.missing": "ಕಾಣೆಯಾಗಿದೆ", - "select.language": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", - "select.version": "ಆವೃತ್ತಿಯನ್ನು ಆಯ್ಕೆಮಾಡಿ", - "source": "ಭಂಡಾರಕ್ಕೆ ಹೋಗಿ", - "source.file.contributors": "ಕೊಡುಗೆದಾರರು", - "source.file.date.created": "ರಚಿಸಿದ ದಿನಾಂಕ", - "source.file.date.updated": "ಕೊನೆಯ ನವೀಕರಣ ದಿನಾಂಕ", - "tabs": "ವಿವಿಧ ಕಿಟಕಿಗಳು", - "toc": "ವಿಷಯಗಳ ಪಟ್ಟಿ", - "top": "ಮೇಲಕ್ಕೆ ಹಿಂತಿರುಗಿ" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ko.html b/src/templates/partials/languages/ko.html deleted file mode 100644 index adadccb7..00000000 --- a/src/templates/partials/languages/ko.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ko", - "action.edit": "이 페이지를 편집", - "action.skip": "콘텐츠로 이동", - "action.view": "페이지소스 보기", - "announce.dismiss": "다시 안보기", - "blog.archive": "아카이브", - "blog.categories": "카테고리", - "blog.categories.in": "카테고리", - "blog.continue": "계속 읽기", - "blog.draft": "임시 저장", - "blog.index": "Index로 돌아가기", - "blog.meta": "메타데이터", - "blog.references": "관련 링크", - "clipboard.copy": "클립보드로 복사", - "clipboard.copied": "클립보드에 복사됨", - "consent.accept": "동의 허락", - "consent.manage": "동의 허락 관리", - "consent.reject": "동의 거부", - "footer": "하단/푸터", - "footer.next": "다음", - "footer.previous": "이전", - "header": "상단/헤더", - "meta.comments": "댓글", - "meta.source": "출처", - "nav": "네비게이션", - "readtime.one": "읽는시간 1분", - "readtime.other": "읽는시간 #분", - "rss.created": "RSS 피드 생성완료", - "rss.updated": "RSS 피드 업데이트완료", - "search": "검색", - "search.config.pipeline": " ", - "search.placeholder": "검색", - "search.share": "공유", - "search.reset": "지우기", - "search.result.initializer": "검색 초기화", - "search.result.placeholder": "검색어를 입력하세요", - "search.result.none": "검색어와 일치하는 문서가 없습니다", - "search.result.one": "1개의 일치하는 문서", - "search.result.other": "#개의 일치하는 문서", - "search.result.more.one": "이 문서에서 1개의 검색 결과 더 보기", - "search.result.more.other": "이 문서에서 #개의 검색 결과 더 보기", - "search.result.term.missing": "포함되지 않은 검색어", - "select.language": "언어설정", - "select.version": "버전 선택", - "source": "저장소로 이동", - "source.file.contributors": "참여자들", - "source.file.date.created": "작성일", - "source.file.date.updated": "마지막 업데이트", - "tabs": "탭", - "toc": "목차", - "top": "맨위로" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ku-IQ.html b/src/templates/partials/languages/ku-IQ.html deleted file mode 100644 index fe9dd1e7..00000000 --- a/src/templates/partials/languages/ku-IQ.html +++ /dev/null @@ -1,64 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ku", - "direction": "rtl", - "action.edit": "دەستکاری ئەم پەڕە بکە", - "action.skip": "ئەم ناوەڕۆکە بپەڕێنە", - "action.view": "سەرچاوەی ئەم لاپەڕەیە نیشان بدە", - "announce.dismiss": "دووبارە ئەمە پیشان مەدە", - "clipboard.copy": "لەبەرگتنەوە بۆ کلیپبۆرد", - "clipboard.copied": "لەبەرگیرایەوە بۆ کلیپ بۆرد", - "consent.accept": "ڕازیبوون", - "consent.manage": "بەڕیوەبردنی ڕیکخستنەکان", - "consent.reject": "ڕەتکردنەوە", - "footer": "ژێرپەڕە", - "footer.next": "دواتر", - "footer.previous": "پێشتر", - "header": "ناونیشانی بەڕه", - "meta.comments": "لێدوانەکان", - "meta.source": "سەرجاوە", - "nav": "ڕێنیشاندەر", - "search": "گەڕان", - "search.config.pipeline": " ", - "search.placeholder": "گەڕان", - "search.share": "گەڕان", - "search.reset": "سڕینەوە", - "search.result.initializer": "ئامادەکردنی گەڕان", - "search.result.placeholder": "بنووسە بۆ دەستپێکردن بە گەڕان", - "search.result.none": "هیچ بەڵگەنامەیەکی هاوتا نیە", - "search.result.one": "١ بەڵگەنامەی هاوتا", - "search.result.other": "بەڵگەنامەی هاوتا #", - "search.result.more.one": "١ دانەی تر لەسەر ئەم پەڕەیە", - "search.result.more.other": "دانەی تر لەسەر ئەم پەڕەیە #", - "search.result.term.missing": "ونبوو", - "select.language": "زمان دیاریبکە", - "select.version": "وەشان دیاریبکە", - "source": "بڕۆ بۆ کۆگا", - "source.file.date.created": "دروسکت کرا", - "source.file.date.updated": "دوایین نوێکردنەوە", - "tabs": "تابەکان", - "toc": "خشتەی ناوەڕۆکەکان", - "top": "گەڕانەوە بۆ سەرەوە" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lb.html b/src/templates/partials/languages/lb.html deleted file mode 100644 index f38c6568..00000000 --- a/src/templates/partials/languages/lb.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "lb", - "direction": "ltr", - "action.edit": "D'Säit beaarbechten", - "action.skip": "Zum Inhalt iwwersprangen", - "action.view": "Quellcode uweisen", - "announce.dismiss": "Net erëm uweisen", - "blog.archive": "Archiv", - "blog.categories": "Kategorien", - "blog.categories.in": "an", - "blog.continue": "Weider liesen", - "blog.draft": "Skizz", - "blog.index": "Zeréck zum Index", - "blog.meta": "Metadaten", - "blog.references": "Änlech Links", - "clipboard.copy": "Kopéieren", - "clipboard.copied": "Kopéiert", - "consent.accept": "Accept", - "consent.manage": "Astellungen beaarbechten", - "consent.reject": "Ofleenen", - "footer": "Footer", - "footer.next": "Weider", - "footer.previous": "Zeréck", - "header": "Header", - "meta.comments": "Kommentaren", - "meta.source": "Quell", - "nav": "Navigatioun", - "readtime.one": "1 min Liesedauer", - "readtime.other": "# min Liesedauer", - "rss.created": "RSS feed", - "rss.updated": "RSS feed vun aktualiséiertem Inhalt", - "search": "Sichen", - "search.placeholder": "Sichen", - "search.share": "Deelen", - "search.reset": "Läschen", - "search.result.initializer": "D'Sich gëtt initialiséiert", - "search.result.placeholder": "Schreif fir eppes ze sichen", - "search.result.none": "Keng zoutreffend Dokumenter", - "search.result.one": "1 zoutreffend Dokument", - "search.result.other": "# zoutreffend Dokumenter", - "search.result.more.one": "1 méi op dëser Säit", - "search.result.more.other": "# méi op dëser Säit", - "search.result.term.missing": "Feelend", - "select.language": "Sprooch auswielen", - "select.version": "Versioun auswielen", - "source": "Op den Repository goen", - "source.file.contributors": "Matwirkender", - "source.file.date.created": "Erstallt", - "source.file.date.updated": "Läscht update", - "tabs": "Tabs", - "toc": "Inhaltsverzeichnis", - "top": "Zeréck zum Ufank" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lt.html b/src/templates/partials/languages/lt.html deleted file mode 100644 index 129505f5..00000000 --- a/src/templates/partials/languages/lt.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "lt", - "action.edit": "Redaguoti šį puslapį", - "action.skip": "Pereiti prie turinio", - "action.view": "Žiūrėti puslapio šaltinius", - "announce.dismiss": "Daugiau neberodyti", - "blog.archive": "Archyvas", - "blog.categories": "Kategorijos", - "blog.categories.in": "į", - "blog.continue": "Skaityti toliau", - "blog.draft": "Ruošinys", - "blog.index": "Grįžti į indeksą", - "blog.meta": "Meta duomenys", - "blog.references": "Susieja saitai", - "clipboard.copy": "Kopijuoti į iškarpinę", - "clipboard.copied": "Nukopijuota į iškarpinę", - "consent.accept": "Sutikti", - "consent.manage": "Redaguoti nustatymus", - "consent.reject": "Atmesti", - "footer": "Poraštė", - "footer.next": "Sekantis", - "footer.previous": "Ankstesnis", - "header": "Antraštė", - "meta.comments": "Komentarai", - "meta.source": "Išeitinis kodas", - "nav": "Navigacija", - "readtime.one": "1 min skaitymo", - "readtime.other": "# min skaitymo", - "rss.created": "RSS šaltinis", - "rss.updated": "RSS šaltinis atnaujinimams", - "search": "Paieška", - "search.config.pipeline": " ", - "search.placeholder": "Paieška", - "search.share": "Dalintis", - "search.reset": "Išvalyti", - "search.result.initializer": "Paieškos inicijavimas", - "search.result.placeholder": "Įveskite norėdami pradėti paiešką", - "search.result.none": "Atitinkančių dokumentų nerasta", - "search.result.one": "1 atitinkantis dokumentas", - "search.result.other": "# atitinkantys dokumentai", - "search.result.more.one": "Dar 1 šiame puslapyje", - "search.result.more.other": "Dar # šiame puslapyje", - "search.result.term.missing": "Nerasta", - "select.language": "Pasirinkti kalbą", - "select.version": "Pasrinkti versiją", - "source": "Eiti į saugyklą", - "source.file.contributors": "Dalininkai", - "source.file.date.created": "Sukurta", - "source.file.date.updated": "Paskutinis atnaujinimas", - "tabs": "Skirtukai", - "toc": "Turinys", - "top": "Grįžti į viršų" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/lv.html b/src/templates/partials/languages/lv.html deleted file mode 100644 index 7bcd9ace..00000000 --- a/src/templates/partials/languages/lv.html +++ /dev/null @@ -1,55 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "lv", - "action.edit": "Rediģēt šo lapu", - "action.skip": "Pāriet uz saturu", - "clipboard.copy": "Kopēt starpliktuvē", - "clipboard.copied": "Kopēts starpliktuvē", - "footer": "Kājene", - "footer.next": "Nākamais", - "footer.previous": "Iepriekšējais", - "header": "Galvene", - "meta.comments": "Komentārs", - "meta.source": "Avots", - "nav": "Navigācija", - "search.placeholder": "Meklēt", - "search.reset": "Notīrīt", - "search.result.initializer": "Notiek meklēšanas inicializācija", - "search.result.placeholder": "Ierakstiet, lai sāktu meklēšanu", - "search.result.none": "Nav atbilstošu dokumentu", - "search.result.one": "1 atbilstošs dokuments", - "search.result.other": "# atbilstoši dokumenti ", - "search.result.more.one": "1 šajā lapā", - "search.result.more.other": "# un vairāk šajā lapā", - "search.result.term.missing": "Trūkstošs", - "select.language": "Izvēlies valodu", - "select.version": "Izvēlies versiju", - "source": "Doties uz repozitoriju", - "source.file.date.created": "Izveidots", - "source.file.date.updated": "Pēdējoreiz atjaunots", - "tabs": "Cilnes", - "toc": "Satura rādītājs", - "top": "Atpakaļ uz augšu" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/mk.html b/src/templates/partials/languages/mk.html deleted file mode 100644 index e3dc114c..00000000 --- a/src/templates/partials/languages/mk.html +++ /dev/null @@ -1,56 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "mk", - "action.edit": "Уредете ја оваа страница", - "action.skip": "Прескокнете до содржината", - "clipboard.copy": "Копирај во таблата", - "clipboard.copied": "Копирано", - "footer": "Подножје", - "footer.next": "Следно", - "footer.previous": "Претходно", - "header": "Заглавје", - "meta.comments": "Коментари", - "meta.source": "Извор", - "nav": "Наслов за навигација", - "search.config.lang": "ru", - "search.placeholder": "Пребарување", - "search.reset": "Чисти", - "search.result.initializer": "Иницијализирање на пребарувањето", - "search.result.placeholder": "Напишете за да започнете со пребарување", - "search.result.none": "Нема соодветни документи", - "search.result.one": "1 документ што се совпаѓа", - "search.result.other": "# соодветни документи", - "search.result.more.one": "Уште 1 на оваа страница", - "search.result.more.other": "Уште # на оваа страница", - "search.result.term.missing": "Недостасува", - "select.language": "Изберете јазик", - "select.version": "Изберете верзија", - "source": "Одете до складиштето", - "source.file.date.created": "Создаден", - "source.file.date.updated": "Последно ажурирање", - "tabs": "Јазичиња", - "toc": "Содржина", - "top": "Вратете се на почетокот" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/mn.html b/src/templates/partials/languages/mn.html deleted file mode 100644 index de9002ab..00000000 --- a/src/templates/partials/languages/mn.html +++ /dev/null @@ -1,51 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "mn", - "action.edit": "Хуудас засварлах", - "action.skip": "Агуулгыг алгасах", - "clipboard.copy": "Хуулах", - "clipboard.copied": "Санах ойд хуулах", - "footer": "Хөл", - "footer.next": "Дараах", - "footer.previous": "Өмнөх", - "header": "Толгой", - "meta.comments": "Сэтгэгдэл", - "meta.source": "Эх үүсвэр", - "nav": "Чиглүүлэгч", - "search.config.lang": "ru", - "search.placeholder": "Хайлт", - "search.reset": "Цэвэрлэх", - "search.result.placeholder": "Хайлтын үгээ бичнэ үү", - "search.result.none": "Таарц илэрсэнгүй", - "search.result.one": "1 таарц илэрлээ", - "search.result.other": "# Тохирох баримт бичиг", - "search.result.more.one": "1 илүү хуудас байна", - "search.result.more.other": "# илүү хуудас байна", - "source": "Хадгалах сан руу очих", - "source.file.date.created": "Үүсгэсэн", - "source.file.date.updated": "Сүүлийн шинэчлэлт", - "tabs": "Табууд", - "toc": "Агуулга" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ms.html b/src/templates/partials/languages/ms.html deleted file mode 100644 index 57b70fc7..00000000 --- a/src/templates/partials/languages/ms.html +++ /dev/null @@ -1,55 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ms", - "action.edit": "Edit halaman ini", - "action.skip": "Langkau tajuk talian", - "clipboard.copy": "Salin ke papan keratan", - "clipboard.copied": "Disalin ke papan keratan", - "footer": "Pengaki", - "footer.next" : "Seterusnya", - "footer.previous": "Sebelumnya", - "header": "Pengepala", - "meta.comments": "Komen", - "meta.source": "Sumber", - "nav": "Navigasi", - "search.placeholder": "Cari", - "search.reset": "Padam", - "search.result.initializer": "Siap carian", - "search.result.placeholder": "Taip untuk mula mencari", - "search.result.none": "Tiada dokumen yang sepadan", - "search.result.one": "1 dokumen yang sepadan", - "search.result.other": "# dokumen yang sepadan", - "search.result.more.one": "1 lagi di halaman ini", - "search.result.more.other": "# lagi di halaman ini", - "search.result.term.missing": "Hilang", - "select.language": "Pilih bahasa", - "select.version": "Pilih versi", - "source": "tajuk talian asal", - "source.file.date.created": "tarikh fil asal dicipta", - "source.file.date.updated": "Tarikh fil dikemas kini", - "tabs": "Tab", - "toc": "Jadual kandungan", - "top": "Kembali ke atas" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/my.html b/src/templates/partials/languages/my.html deleted file mode 100644 index 27ca3ad9..00000000 --- a/src/templates/partials/languages/my.html +++ /dev/null @@ -1,49 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "my", - "action.edit": "ဤ စာမျက်နှာကို ပြင်ရန်", - "action.skip": "မာတိကာ သို့ သွားရန်", - "clipboard.copy": "ကလစ်ဘုတ် သို့ ကူးယူရန်", - "clipboard.copied": "ကလစ်ဘုတ် သို့ ကူယူပြီး", - "footer": "အောက်ခြေ", - "footer.next": "ရှေ့သို့", - "footer.previous": "နောက်သို့", - "header": "ခေါင်းပိုင်း", - "meta.comments": "မှတ်ချက်များ", - "meta.source": "ရင်းမြစ်", - "nav": "လမ်းညွှန်", - "search.config.pipeline": " ", - "search.placeholder": "ရှာရန်", - "search.reset": "ရှင်းလင်း", - "search.result.placeholder": "ရှာဖွေခြင်းစရန် စာရိုက်ပါ", - "search.result.none": "တူညီသော စာရွက်စာတမ်းများ မရှိပါ", - "search.result.one": "စာရွက်စာတမ်း ၁ ခု တူညီသည်", - "search.result.other": "စာရွက်စာတမ်း # ခု တူညီသည်", - "source": "repository သို့ သွားရန်", - "source.file.date.created": "နေပြည်တော်", - "source.file.date.updated": "နောက်ဆုံး ထုတ်ပြန်ချက်", - "tabs": "တက်များ", - "toc": "ပါဝင်အကြောင်းအရာများ" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nb.html b/src/templates/partials/languages/nb.html deleted file mode 100644 index 6be63531..00000000 --- a/src/templates/partials/languages/nb.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "nb", - "action.edit": "Rediger denne siden", - "action.skip": "Gå til innhold", - "action.view": "Vis kildekoden til denne siden", - "announce.dismiss": "Ikke vis dette igjen", - "blog.archive": "Arkiv", - "blog.categories": "Kategorier", - "blog.categories.in": "i", - "blog.continue": "Fortsett å lese", - "blog.draft": "Kladd", - "blog.index": "Tilbake til oversikt", - "blog.meta": "Metadata", - "blog.references": "Relaterte lenker", - "clipboard.copy": "Kopier til utklippstavlen", - "clipboard.copied": "Kopiert til utklippstavlen", - "consent.accept": "Akseptert", - "consent.manage": "Innstillinger", - "consent.reject": "Reject", - "footer": "Footer", - "footer.next": "Neste", - "footer.previous": "Forrige", - "header": "Header", - "meta.comments": "Kommentarer", - "meta.source": "Kilde", - "nav": "Navigasjon", - "readtime.one": "lesteid: 1 min", - "readtime.other": "lesetid: # min", - "rss.created": "RSS feed", - "rss.updated": "Oppdatert RSS feed", - "search": "Søk", - "search.config.lang": "no", - "search.placeholder": "Søk", - "search.share": "Del", - "search.reset": "Nullstill", - "search.result.initializer": "Starter søk", - "search.result.placeholder": "Skriv søkeord", - "search.result.none": "Ingen treff", - "search.result.one": "1 treff", - "search.result.other": "# treff", - "search.result.more.one": "1 til på denne siden", - "search.result.more.other": "# flere på denne siden", - "search.result.term.missing": "Mangler", - "select.language": "Velg språk", - "select.version": "Velg versjon", - "source": "Gå til kilde", - "source.file.contributors": "Bidragsytere", - "source.file.date.created": "Opprettet", - "source.file.date.updated": "Sist oppdatert", - "tabs": "Faner", - "toc": "Innholdsliste", - "top": "Tilbake til toppen" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nl.html b/src/templates/partials/languages/nl.html deleted file mode 100644 index 0000fe60..00000000 --- a/src/templates/partials/languages/nl.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "nl", - "action.edit": "Wijzig deze pagina", - "action.skip": "Ga naar inhoud", - "action.view": "Bron van deze pagina bekijken", - "announce.dismiss": "Niet meer laten zien", - "blog.archive": "Archief", - "blog.categories": "Categorieën", - "blog.categories.in": "in", - "blog.continue": "Doorgaan met lezen", - "blog.draft": "Concept", - "blog.index": "Terug naar de inhoudsopgave", - "blog.meta": "Metadata", - "blog.references": "Gerelateerde links", - "clipboard.copy": "Kopiëren naar klembord", - "clipboard.copied": "Gekopieerd naar klembord", - "consent.accept": "Accepteren", - "consent.manage": "Instellingen", - "consent.reject": "Afwijzen", - "footer": "Footer", - "footer.next": "Volgende", - "footer.previous": "Vorige", - "header": "Header", - "meta.comments": "Reacties", - "meta.source": "Bron", - "nav": "Navigatie", - "readtime.one": "1 min leestijd", - "readtime.other": "# min leestijd", - "rss.created": "RSS feed", - "rss.updated": "RSS feed met geüpdatet inhoud", - "search": "Zoeken", - "search.config.lang": "nl", - "search.placeholder": "Zoeken", - "search.share": "Delen", - "search.reset": "Leegmaken", - "search.result.initializer": "Zoeken initialiseren", - "search.result.placeholder": "Typ om te beginnen met zoeken", - "search.result.none": "Geen overeenkomende documenten", - "search.result.one": "1 overeenkomende document", - "search.result.other": "# overeenkomende documenten", - "search.result.more.one": "1 extra overeenkomst op deze pagina", - "search.result.more.other": "# extra overeenkomsten op deze pagina", - "search.result.term.missing": "Ontbreekt", - "select.language": "Selecteer taal", - "select.version": "Selecteer versie", - "source": "Ga naar repository", - "source.file.contributors": "Bijdragers", - "source.file.date.created": "Gecreëerd", - "source.file.date.updated": "Laatst geüpdatet", - "tabs": "Tabs", - "toc": "Inhoudsopgave", - "top": "Terug naar boven" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/nn.html b/src/templates/partials/languages/nn.html deleted file mode 100644 index 9478bdbe..00000000 --- a/src/templates/partials/languages/nn.html +++ /dev/null @@ -1,62 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "nn", - "action.edit": "Rediger denne sida", - "action.skip": "Gå til innhald", - "announce.dismiss": "Ikkje vis dette att", - "clipboard.copy": "Kopier til utklippstavla", - "clipboard.copied": "Kopiert til utklippstavla", - "consent.accept": "Akseptert", - "consent.manage": "Innstillinger", - "consent.reject": "Reject", - "footer": "Footer", - "footer.next": "Neste", - "footer.previous": "Førre", - "header": "Header", - "meta.comments": "Kommentarar", - "meta.source": "Kjelde", - "nav": "Navigasjon", - "search": "Søk", - "search.config.lang": "no", - "search.placeholder": "Søk", - "search.share": "Del", - "search.reset": "Nullstill", - "search.result.initializer": "Startar søk", - "search.result.placeholder": "Skriv søkeord", - "search.result.none": "Ingen treff", - "search.result.one": "1 treff", - "search.result.other": "# treff", - "search.result.more.one": "1 til på denne sida", - "search.result.more.other": "# fleire på denne sida", - "search.result.term.missing": "Manglar", - "select.language": "Vel språk", - "select.version": "Vel versjon", - "source": "Gå til kjelde", - "source.file.date.created": "Oppretta", - "source.file.date.updated": "Sist oppdatert", - "tabs": "Faner", - "toc": "Innhaldsliste", - "top": "Tilbake til toppen" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pl.html b/src/templates/partials/languages/pl.html deleted file mode 100644 index 7817633a..00000000 --- a/src/templates/partials/languages/pl.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "pl", - "action.edit": "Edytuj tę stronę", - "action.skip": "Przejdź do treści", - "action.view": "Zobacz kod źródłowy tej strony", - "announce.dismiss": "Nie pokazuj tego ponownie", - "blog.archive": "Archiwum", - "blog.categories": "Kategorie", - "blog.categories.in": "", - "blog.continue": "Czytaj dalej", - "blog.draft": "Wersja robocza", - "blog.index": "Powrót do indeksu", - "blog.meta": "Metadane", - "blog.references": "Powiązane łącza", - "clipboard.copy": "Kopiuj do schowka", - "clipboard.copied": "Skopiowano do schowka", - "consent.accept": "Akceptuj", - "consent.manage": "Zarządzaj ustawieniami", - "consent.reject": "Odrzuć", - "footer": "Stopka", - "footer.next": "Następna strona", - "footer.previous": "Poprzednia strona", - "header": "Nagłówek", - "meta.comments": "Komentarze", - "meta.source": "Kod źródłowy", - "nav": "Nawigacja", - "readtime.one": "Czas czytania: 1 min", - "readtime.other": "Czas czytania: # min", - "rss.created": "Kanał RSS", - "rss.updated": "Kanał RSS zaktualizowanych treści", - "search": "Szukaj", - "search.config.pipeline": " ", - "search.placeholder": "Szukaj", - "search.share": "Udostępnij", - "search.reset": "Wyczyść", - "search.result.initializer": "Inicjowanie wyszukiwania", - "search.result.placeholder": "Zacznij pisać, aby szukać", - "search.result.none": "Brak wyników wyszukiwania", - "search.result.one": "Wyniki wyszukiwania: 1", - "search.result.other": "Wyniki wyszukiwania: #", - "search.result.more.one": "1 więcej na tej stronie", - "search.result.more.other": "# więcej na tej stronie", - "search.result.term.missing": "Brak", - "select.language": "Wybierz język", - "select.version": "Wybierz wersję", - "source": "Przejdź do repozytorium", - "source.file.contributors": "Kontrybutorzy", - "source.file.date.created": "Utworzony", - "source.file.date.updated": "Ostatnia aktualizacja", - "tabs": "Zakładki", - "toc": "Spis treści", - "top": "Powrót do góry" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pt-BR.html b/src/templates/partials/languages/pt-BR.html deleted file mode 100644 index d934a9ac..00000000 --- a/src/templates/partials/languages/pt-BR.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "pt", - "action.edit": "Editar esta página", - "action.skip": "Pular para conteúdo", - "action.view": "Exibir fonte desta página", - "announce.dismiss": "Não mostrar isso novamente", - "blog.archive": "Arquivo", - "blog.categories": "Categorias", - "blog.categories.in": "em", - "blog.continue": "Continuar leitura", - "blog.draft": "Rascunho", - "blog.index": "Voltar ao índice", - "blog.meta": "Metadados", - "blog.references": "Links relacionados", - "clipboard.copy": "Copiar para área de transferência", - "clipboard.copied": "Copiado para área de transferência", - "consent.accept": "Aceitar", - "consent.manage": "Gerenciar configurações", - "consent.reject": "Rejeitar", - "footer": "Rodapé", - "footer.next": "Próximo", - "footer.previous": "Anterior", - "header": "Cabeçalho", - "meta.comments": "Comentários", - "meta.source": "Origem", - "nav": "Navegação", - "readtime.one": "1 min de leitura", - "readtime.other": "# min de leitura", - "rss.created": "RSS feed", - "rss.updated": "RSS feed de conteúdo atualizado", - "search": "Pesquisar", - "search.config.lang": "pt", - "search.placeholder": "Buscar", - "search.share": "Compartilhar", - "search.reset": "Limpar", - "search.result.initializer": "Inicializando busca", - "search.result.placeholder": "Digite para iniciar a busca", - "search.result.none": "Nenhum documento encontrado", - "search.result.one": "1 documento encontrado", - "search.result.other": "# documentos encontrados", - "search.result.more.one": "mais 1 nesta página", - "search.result.more.other": "# mais nesta página", - "search.result.term.missing": "Ausente", - "select.language": "Selecione o idioma", - "select.version": "Selecione a versão", - "source": "Ir para repositório", - "source.file.contributors": "Contribuidores", - "source.file.date.created": "Criado em", - "source.file.date.updated": "Última atualização", - "tabs": "Abas", - "toc": "Índice", - "top": "Voltar para o topo" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/pt.html b/src/templates/partials/languages/pt.html deleted file mode 100644 index e5dee1cb..00000000 --- a/src/templates/partials/languages/pt.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "pt", - "action.edit": "Editar esta página", - "action.skip": "Ir para o conteúdo", - "action.view": "Ver fonte desta página", - "announce.dismiss": "Não mostrar novamente", - "blog.archive": "Arquivo", - "blog.categories": "Categorias", - "blog.categories.in": "em", - "blog.continue": "Continuar leitura", - "blog.draft": "Rascunho", - "blog.index": "Voltar ao índice", - "blog.meta": "Metadados", - "blog.references": "Ligações relacionadas", - "clipboard.copy": "Copiar para área de transferência", - "clipboard.copied": "Copiado para área de transferência", - "consent.accept": "Aceitar", - "consent.manage": "Gerir configurações", - "consent.reject": "Rejeitar", - "footer": "Rodapé", - "footer.next": "Próximo", - "footer.previous": "Anterior", - "header": "Cabeçalho", - "meta.comments": "Comentários", - "meta.source": "Fonte", - "nav": "Navegação", - "readtime.one": "1 min de leitura", - "readtime.other": "# min de leitura", - "rss.created": "canal RSS", - "rss.updated": "canal RSS com conteúdo atualizado", - "search": "Pesquisar", - "search.config.lang": "pt", - "search.placeholder": "Buscar", - "search.share": "Compartilhar", - "search.reset": "Limpar", - "search.result.initializer": "Inicializando a pesquisa", - "search.result.placeholder": "Digite para iniciar a busca", - "search.result.none": "Nenhum resultado encontrado", - "search.result.one": "1 resultado encontrado", - "search.result.other": "# resultados encontrados", - "search.result.more.one": "Mais 1 nesta página", - "search.result.more.other": "Mais # nesta página", - "search.result.term.missing": "Ausente", - "select.language": "Selecione o idioma", - "select.version": "Selecione a versão", - "source": "Ir ao repositório", - "source.file.contributors": "Colaboradores", - "source.file.date.created": "Criada", - "source.file.date.updated": "Última atualização", - "tabs": "Abas", - "toc": "Índice", - "top": "Voltar ao topo" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ro.html b/src/templates/partials/languages/ro.html deleted file mode 100644 index 7bea9afb..00000000 --- a/src/templates/partials/languages/ro.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ro", - "action.edit": "Editeaza această pagină", - "action.skip": "Sari la conținut", - "action.view": "Vezi sursa acestei pagini", - "announce.dismiss": "Nu mai arăta asta", - "blog.archive": "Arhivează", - "blog.categories": "Categorii", - "blog.categories.in": "în", - "blog.continue": "Continuă să citești", - "blog.draft": "Ciornă", - "blog.index": "Înapoi la index", - "blog.meta": "Metadata", - "blog.references": "Link-uri relevante", - "clipboard.copy": "Copiază în clipboard", - "clipboard.copied": "Copiat în clipboard", - "consent.accept": "Accept", - "consent.manage": "Gestionați setările", - "consent.reject": "Refuz", - "footer": "Subsol", - "footer.next": "Următor", - "footer.previous": "Anterior", - "header": "Antet", - "meta.comments": "Comentarii", - "meta.source": "Sursă", - "nav": "Navigație", - "readtime.one": "1 minut de citit", - "readtime.other": "# minut de citit", - "rss.created": "Flux RSS", - "rss.updated": "Flux RSS cu conținut actualizat", - "search": "Caută", - "search.config.lang": "ro", - "search.placeholder": "Căutare", - "search.share": "Distribuie", - "search.reset": "Resetează", - "search.result.initializer": "Inițializare căutare", - "search.result.placeholder": "Tastează pentru a începe căutarea", - "search.result.none": "Nu a fost găsit niciun document", - "search.result.one": "1 document găsit", - "search.result.other": "# documente găsite", - "search.result.more.one": "Încă 1 pe această pagină", - "search.result.more.other": "Încă # pe această pagină", - "search.result.term.missing": "Lipsă", - "select.language": "Selectează limba", - "select.version": "Selectează versuine", - "source": "Accesează repository-ul", - "source.file.contributors": "Contribuitori", - "source.file.date.created": "Creată", - "source.file.date.updated": "Ultima actualizare", - "tabs": "File", - "toc": "Cuprins", - "top": "Înapoi sus" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ru.html b/src/templates/partials/languages/ru.html deleted file mode 100644 index ddbd7b95..00000000 --- a/src/templates/partials/languages/ru.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ru", - "action.edit": "Редактировать страницу", - "action.skip": "Перейти к содержанию", - "action.view": "Посмотреть исходный код страницы", - "announce.dismiss": "Больше не показывать", - "blog.archive": "Архив", - "blog.categories": "Категории", - "blog.categories.in": "В", - "blog.continue": "Читать", - "blog.draft": "Черновик", - "blog.index": "На главную", - "blog.meta": "Метаданные", - "blog.references": "Ссылки", - "clipboard.copy": "Копировать в буфер", - "clipboard.copied": "Скопировано в буфер", - "consent.accept": "Принять", - "consent.manage": "Управлять настройками", - "consent.reject": "Отклонить", - "footer": "Нижний колонтитул", - "footer.next": "Вперед", - "footer.previous": "Назад", - "header": "Верхний колонтитул", - "meta.comments": "Комментарии", - "meta.source": "Исходный код", - "nav": "Навигация", - "readtime.one": "Читать 1 минуту", - "readtime.other": "Читать # минут", - "rss.created": "RSS канал", - "rss.updated": "RSS канал с новым контентом", - "search": "Поиск", - "search.config.lang": "ru", - "search.placeholder": "Поиск", - "search.share": "Поделиться", - "search.reset": "Очистить", - "search.result.initializer": "Инициализация поиска", - "search.result.placeholder": "Начните печатать для поиска", - "search.result.none": "Совпадений не найдено", - "search.result.one": "Найдено 1 совпадение", - "search.result.other": "Найдено совпадений: #", - "search.result.more.one": "Ещё 1 на этой странице", - "search.result.more.other": "Ещё # на этой странице", - "search.result.term.missing": "Отсутствует", - "select.language": "Выберите язык", - "select.version": "Выберите версию", - "source": "Перейти к репозиторию", - "source.file.contributors": "Участники", - "source.file.date.created": "Дата создания", - "source.file.date.updated": "Последнее обновление", - "tabs": "Вкладки", - "toc": "Содержание", - "top": "К началу" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sa.html b/src/templates/partials/languages/sa.html deleted file mode 100644 index 338e2b61..00000000 --- a/src/templates/partials/languages/sa.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sa", - "action.edit": "एतत् पृष्ठं सम्पादयतु", - "action.skip": "सामग्रीं त्यजन्तु", - "action.view": "अस्य पृष्ठस्य स्रोतः पश्यन्तु", - "announce.dismiss": "एतत् पुनः न दर्शयतु", - "blog.archive": "लेखागार", - "blog.categories": "श्रेणियाँ", - "blog.categories.in": "इत्यस्मिन्‌", - "blog.continue": "पठनं निरन्तरं कुर्वन्तु", - "blog.draft": "प्रारूप", - "blog.index": "अनुक्रमणिकां प्रति पुनः आगच्छन्तु", - "blog.meta": "परिदत्तांश", - "blog.references": "सन्दर्भाः", - "clipboard.copy": "एतत् प्रतिलिख्यताम्", - "clipboard.copied": "प्रतिलिपितः भवति", - "consent.accept": "अहं तत् स्वीकुर्वन् अस्मि", - "consent.manage": "वविन्यासं प्रबन्धयन्तु", - "consent.reject": "अहं तत् निराकरोमि", - "footer": "पादलेखः", - "footer.next": "अग्रिमः", - "footer.previous": "पूर्वकृत", - "header": "शीर्षकम्", - "meta.comments": "विचाराः", - "meta.source": "स्रोतः", - "nav": "मार्गदर्शनम्", - "readtime.one": "१ निमेषं पठितुं", - "readtime.other": "# निमेषं पठितुं", - "rss.created": "आरएसएस सेवा", - "rss.updated": "आरएसएस सेवातः नवीनतमं अद्यतनम्", - "search": "अन्वेषण", - "search.placeholder": "अन्वेषण", - "search.share": "भजतु", - "search.reset": "तत् स्वच्छं कुर्वन्तु", - "search.result.initializer": "अन्वेषणस्य आरम्भः", - "search.result.placeholder": "अन्वेषणं आरभ्य लिखन्तु", - "search.result.none": "अभिलेखाः नास्ति", - "search.result.one": "१ अभिलेखः अस्ति", - "search.result.other": "# अभिलेखाः सन्ति", - "search.result.more.one": "अस्मिन् पृष्ठे १ अन्यः अस्ति", - "search.result.more.other": "अस्मिन् पृष्ठे # अन्ये सन्ति", - "search.result.term.missing": "शून्य", - "select.language": "भाषां चिनोतु", - "select.version": "संस्करणं चिनोतु", - "source": "भण्डारं गच्छन्तु", - "source.file.contributors": "अंशदाता", - "source.file.date.created": "ननिर्माणस्य तिथिः", - "source.file.date.updated": "परिवर्तनस्य तिथिः", - "tabs": "पट्टाः", - "toc": "सामग्रीसारणी", - "top": "पुनः उपरिभागं प्रति गच्छन्तु" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sh.html b/src/templates/partials/languages/sh.html deleted file mode 100644 index 42a0d902..00000000 --- a/src/templates/partials/languages/sh.html +++ /dev/null @@ -1,70 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sh", - "action.edit": "Ažuriraj stranicu", - "action.skip": "Idi na tekst", - "action.view": "Pogledaj izvorni kod ove stranice", - "announce.dismiss": "Nemoj mi ponovo pokazati ovo", - "blog.archive": "Arhiva", - "blog.categories": "Kategorije", - "blog.categories.in": "u", - "blog.continue": "Nastavi sa čitanjem", - "blog.meta": "Metapodaci", - "blog.references": "Povezani linkovi", - "clipboard.copy": "Kopiraj u klipbord", - "clipboard.copied": "Iskopirano u klipbord", - "consent.accept": "Prihvati", - "consent.manage": "Promeni podešavanja", - "consent.reject": "Odbij", - "footer": "Podnožje", - "footer.next": "Sledeće", - "footer.previous": "Prethodno", - "header": "Zaglavlje", - "meta.comments": "Komentari", - "meta.source": "Izvor", - "nav": "Navigacija", - "readtime.one": "1 minut čitanja", - "readtime.other": "# minuta čitanja", - "search": "Pretraga", - "search.placeholder": "Pretraga", - "search.share": "Deljenje", - "search.reset": "Očisti", - "search.result.initializer": "Inicijalizujem pretragu", - "search.result.placeholder": "Unesite pojam pretrage", - "search.result.none": "Ništa nije pronađeno", - "search.result.one": "1 rezultat pretrage", - "search.result.other": "# rezultata pretrage", - "search.result.more.one": "još 1 na ovoj strani", - "search.result.more.other": "još # na ovoj strani", - "search.result.term.missing": "Nedostaje", - "select.language": "Izaberi jezik", - "select.version": "Izaberi verziju", - "source": "Idi u repozitorijum", - "source.file.date.created": "Kreiran", - "source.file.date.updated": "Ažuriran", - "tabs": "Tabovi", - "toc": "Sadržaj", - "top": "Nazad na vrh" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/si.html b/src/templates/partials/languages/si.html deleted file mode 100644 index eb41309e..00000000 --- a/src/templates/partials/languages/si.html +++ /dev/null @@ -1,51 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "si", - "action.edit": "පිටුව සංස්කරණය", - "action.skip": "අන්තර්ගතය වෙත යන්න", - "clipboard.copy": "කොපි කරන්න", - "clipboard.copied": "කොපි කළා", - "footer": "පාදම", - "footer.next": "මීළඟ", - "footer.previous": "පසුගිය", - "header": "ශීර්ෂය", - "meta.comments": "ප්‍රතිචාර", - "meta.source": "මූලාශ්‍රය", - "nav": "යාත්‍රණය", - "search.config.pipeline": " ", - "search.placeholder": "සොයන්න", - "search.reset": "මකන්න", - "search.result.placeholder": "සෙවීමට ටයිප් කරන්න", - "search.result.none": "කිසිවක් හමු නොවුණි", - "search.result.one": "1 ගැලපෙන ගොනුවක්", - "search.result.other": "ගැලපෙන ගොනු # ක්", - "search.result.more.one": "තව 1 ප්‍රතිඵලයක්", - "search.result.more.other": "තව ප්‍රතිඵල # ක්", - "source": "රිපොසිටරියට යන්න", - "source.file.date.created": "ٺاھيو ويو", - "source.file.date.updated": "අවසන් යාවත්කාලීන වීම", - "tabs": "ටැබ්ස්", - "toc": "පටුන" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sk.html b/src/templates/partials/languages/sk.html deleted file mode 100644 index 701a5a53..00000000 --- a/src/templates/partials/languages/sk.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sk", - "action.edit": "Upraviť túto stránku", - "action.skip": "Preskočiť na obsah", - "clipboard.copy": "Kopírovať do schránky", - "clipboard.copied": "Skopírované do schránky", - "footer.next": "Ďalej", - "footer.previous": "Späť", - "meta.comments": "Komentáre", - "meta.source": "Zdroj", - "search.placeholder": "Hľadať", - "search.result.placeholder": "Pre vyhľadávanie začni písať", - "search.result.none": "Žiadne vyhovujúce dokumenty", - "search.result.one": "Vyhovujúci dokument: 1", - "search.result.other": "Vyhovujúce dokumenty: #", - "source": "Zobraziť repozitár", - "source.file.date.created": "Vytvorené", - "source.file.date.updated": "Posledná aktualizácia", - "toc": "Obsah" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sl.html b/src/templates/partials/languages/sl.html deleted file mode 100644 index a01f31d9..00000000 --- a/src/templates/partials/languages/sl.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sl", - "action.edit": "Uredi stran", - "action.skip": "Skoči na vsebino", - "action.view": "Prikaži izvorno stran", - "announce.dismiss": "Ne prikaži več", - "blog.archive": "Arhiv", - "blog.categories": "Kategorije", - "blog.categories.in": "v", - "blog.continue": "Nadaljuj z branjem", - "blog.draft": "Osnutek", - "blog.index": "Nazaj na kazalo", - "blog.meta": "Metapodatki", - "blog.references": "Sorodne povezave", - "clipboard.copy": "Kopiraj v odložišče", - "clipboard.copied": "Kopirano v odložišče", - "consent.accept": "Sprejmi", - "consent.manage": "Uredi nastavitve", - "consent.reject": "Zavrni", - "footer": "Glava", - "footer.next": "Naslednja stran", - "footer.previous": "Prejšnja stran", - "header": "Noga", - "meta.comments": "Komentarji", - "meta.source": "Izvorna koda", - "nav": "Navigacija", - "readtime.one": "Čas branja: 1 min", - "readtime.other": "Čas branja: # min", - "rss.created": "RSS vir", - "rss.updated": "RSS vir posodobljene vsebine", - "search": "Iskanje", - "search.config.lang": "sl", - "search.placeholder": "Išči", - "search.share": "Deli", - "search.reset": "Počisti", - "search.result.initializer": "Inicializacija iskanja", - "search.result.placeholder": "Vpiši iskalni niz", - "search.result.none": "Ni zadetkov", - "search.result.one": "1 zadetek", - "search.result.other": "# zadetkov", - "search.result.more.one": "Še 1 na tej strani", - "search.result.more.other": "Še # na tej strani", - "search.result.term.missing": "Manjka", - "select.language": "Izberi jezik", - "select.version": "Izberi različico", - "source": "Pojdi na repozitorij", - "source.file.contributors": "Soavtorji", - "source.file.date.created": "Ustvarjeno", - "source.file.date.updated": "Zadnja posodobitev", - "tabs": "Zavihki", - "toc": "Kazalo", - "top": "Nazaj na vrh" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sr.html b/src/templates/partials/languages/sr.html deleted file mode 100644 index 275ea126..00000000 --- a/src/templates/partials/languages/sr.html +++ /dev/null @@ -1,57 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sr", - "action.edit": "Ажурирај страницу", - "action.skip": "Иди на текст", - "clipboard.copy": "Копирај у клипборд", - "clipboard.copied": "Ископирано у клипборд", - "footer": "Подножје", - "footer.next": "Следеће", - "footer.previous": "Претходно", - "header": "Заглавље", - "meta.comments": "Коментари", - "meta.source": "Извор", - "nav": "Навигација", - "search": "Претрага", - "search.placeholder": "Претрага", - "search.share": "Дељење", - "search.reset": "Очисти", - "search.result.initializer": "Иницијализујем претрагу", - "search.result.placeholder": "Унесите појам претраге", - "search.result.none": "Ништа није пронађено", - "search.result.one": "1 резултат претраге", - "search.result.other": "# резултата претраге", - "search.result.more.one": "још 1 на овој страни", - "search.result.more.other": "још # на овој страни", - "search.result.term.missing": "Недостаје", - "select.language": "Изабери језик", - "select.version": "Изабери верзију", - "source": "Иди у репозиторијум", - "source.file.date.created": "Креиран", - "source.file.date.updated": "Ажуриран", - "tabs": "Табови", - "toc": "Садржај", - "top": "Назад на врх" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/sv.html b/src/templates/partials/languages/sv.html deleted file mode 100644 index 52f151d2..00000000 --- a/src/templates/partials/languages/sv.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "sv", - "action.edit": "Redigera sidan", - "action.skip": "Gå till innehållet", - "action.view": "Visa källkoden för denna sida", - "announce.dismiss": "Visa inte igen", - "blog.archive": "Arkivera", - "blog.categories": "Kategorier", - "blog.categories.in": "i", - "blog.continue": "Fortsätt läsa", - "blog.draft": "Utkast", - "blog.index": "Tillbaka till index", - "blog.meta": "Metadata", - "blog.references": "Relaterade länkar", - "clipboard.copy": "Kopiera till urklipp", - "clipboard.copied": "Kopierat till urklipp", - "consent.accept": "Acceptera", - "consent.manage": "Hantera inställningar", - "consent.reject": "Acceptera inte", - "footer": "Sidfot", - "footer.next": "Nästa", - "footer.previous": "Föregående", - "header": "Sidhuvud", - "meta.comments": "Kommentarer", - "meta.source": "Källa", - "nav": "Navigation", - "readtime.one": "1 min lästid", - "readtime.other": "# min lästid", - "rss.created": "RSS-flöde", - "rss.updated": "RSS-flöde av uppdaterat innehåll", - "search": "Sök", - "search.config.lang": "sv", - "search.placeholder": "Sök", - "search.share": "Dela", - "search.reset": "Rensa", - "search.result.initializer": "Initialiserar sök", - "search.result.placeholder": "Skriv sökord", - "search.result.none": "Inga sökresultat", - "search.result.one": "1 sökresultat", - "search.result.other": "# sökresultat", - "search.result.more.one": "1 till på denna sida", - "search.result.more.other": "# till på denna sida", - "search.result.term.missing": "Saknas", - "select.language": "Välj språk", - "select.version": "Välj version", - "source": "Gå till datakatalog", - "source.file.contributors": "Författare", - "source.file.date.created": "Skapad", - "source.file.date.updated": "Senast uppdaterad", - "tabs": "Flikar", - "toc": "Innehållsförteckning", - "top": "Tillbaka till toppen" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/te.html b/src/templates/partials/languages/te.html deleted file mode 100644 index 7529a47c..00000000 --- a/src/templates/partials/languages/te.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "te", - "action.edit": "ఈ పేజీలో దిద్దుబాట్లు చేయండి", - "action.skip": "సమాచారానికి వెళ్లండి", - "action.view": "నేను ఈ పేజీ యొక్క మూలాన్ని చూడాలనుకుంటున్నాను", - "announce.dismiss": "దీన్ని మళ్లీ చూపవద్దు", - "blog.archive": "పాత వ్యాసం", - "blog.categories": "వర్గాలు", - "blog.categories.in": "లో", - "blog.continue": "చదవడం కొనసాగించండి", - "blog.draft": "ప్రారంభ రచన", - "blog.index": "సూచికకు తిరిగి వెళ్ళు", - "blog.meta": "సమాచారం గురించి సమాచారం", - "blog.references": "సంబంధిత సూచనలు", - "clipboard.copy": "దీనిని అనుకరించు", - "clipboard.copied": "దీనిని అతికించు", - "consent.accept": "నేను దీనిని అంగీకరిస్తున్నాను", - "consent.manage": "ఆకృతీకరణను నిర్వహించండి", - "consent.reject": "నేను దీనిని తిరస్కరిస్తున్నాను", - "footer": "అడిటిప్పణి", - "footer.next": "తదుపరి భాగం", - "footer.previous": "మునుపటి భాగం", - "header": "శీర్షిక విభాగం", - "meta.comments": "అభిప్రాయాలు", - "meta.source": "మూలం", - "nav": "మార్గదర్శక పట్టీ", - "readtime.one": "చదవడానికి ఒక నిమిషం పడుతుంది", - "readtime.other": "చదవడానికి # నిమిషాలు పడుతుంది", - "rss.created": "ఆర్ఎస్ఎస్ సేవ", - "rss.updated": "ఆర్ఎస్ఎస్ సేవ నుండి తాజా నవీకరణ", - "search": "వెతకండి", - "search.placeholder": "వెతకండి", - "search.share": "పంచుకోండి", - "search.reset": "తుడిచివేయు", - "search.result.initializer": "శోధనను ప్రారంభిస్తోంది", - "search.result.placeholder": "రాయడం ద్వారా వెతకడం ప్రారంభించండి", - "search.result.none": "సరిపోలే పత్రాలు లేవు", - "search.result.one": "ఒక సరిపోలే పత్రం", - "search.result.other": "# సరిపోలే పత్రాలు", - "search.result.more.one": "ఈ పేజీలో మరొకటి", - "search.result.more.other": "ఈ పేజీలో ఇంకా # ఉన్నాయి", - "search.result.term.missing": "తప్పిపోయింది", - "select.language": "భాషను ఎంచుకోండి", - "select.version": "సంస్కరణను ఎంచుకోండి", - "source": "భండారానికి వెళ్ళండి", - "source.file.contributors": "సహకారులు", - "source.file.date.created": "సృష్టించబడింది", - "source.file.date.updated": "చివరి నవీకరణ", - "tabs": "వివిధ కిటికీలు", - "toc": "విషయ సూచిక", - "top": "పైకి తిరిగి వెళ్ళు" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/th.html b/src/templates/partials/languages/th.html deleted file mode 100644 index c8104fc1..00000000 --- a/src/templates/partials/languages/th.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "th", - "action.edit": "แก้ไขหน้านี้", - "action.skip": "ข้ามไปที่เนื้อหา", - "action.view": "ดูแหล่งที่มาของหน้านี้", - "announce.dismiss": "อย่าแสดงสิ่งนี้อีก", - "blog.archive": "คลังเก็บเอกสาร", - "blog.categories": "หมวดหมู่", - "blog.categories.in": "ใย", - "blog.continue": "อ่านต่อไป", - "blog.draft": "ฉบับร่าง", - "blog.index": "กลับไปยังหน้าแรก", - "blog.meta": "คำอธิบายข้อมูล", - "blog.references": "ลิงก์ที่เกี่ยวข้อง", - "clipboard.copy": "คัดลอก", - "clipboard.copied": "คัดลอกแล้ว", - "consent.accept": "ยอมรับ", - "consent.manage": "จัดการการตั้งค่า", - "consent.reject": "ปฏิเสธ", - "footer": "ส่วนท้าย", - "footer.next": "ต่อไป", - "footer.previous": "ก่อนหน้า", - "header": "หัวข้อ", - "meta.comments": "ความคิดเห็น", - "meta.source": "แหล่งที่มา", - "nav": "ตัวนำทาง", - "readtime.one": "อ่าน 1 นาที", - "readtime.other": "อ่าน # นาที", - "rss.created": "ฟีด RSS", - "rss.updated": "ฟีด RSS ของเนื้อหาที่อัปเดต", - "search": "ค้นหา", - "search.config.lang": "th", - "search.placeholder": "ค้นหา", - "search.share": "แบ่งปัน", - "search.reset": "ล้าง", - "search.result.initializer": "กำลังเริ่มต้นการค้นหา", - "search.result.placeholder": "พิมพ์เพื่อเริ่มค้นหา", - "search.result.none": "ไม่พบเอกสารที่ตรงกัน", - "search.result.one": "พบเอกสารที่ตรงกัน", - "search.result.other": "พบ # เอกสารที่ตรงกัน", - "search.result.more.one": "อีกหนึ่งในหน้านี้", - "search.result.more.other": "# เพิ่มเติมในหน้านี้", - "search.result.term.missing": "ไม่พบ", - "select.language": "เลือกภาษา", - "select.version": "เลือกเวอร์ชัน", - "source": "ไปที่พื้นที่เก็บข้อมูล", - "source.file.contributors": "ผู้มีส่วนร่วม", - "source.file.date.created": "สร้าง", - "source.file.date.updated": "สร้าง", - "tabs": "แท็บ", - "toc": "สารบัญ", - "top": "กลับไปด้านบนสุด" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/tl.html b/src/templates/partials/languages/tl.html deleted file mode 100644 index 00c22c99..00000000 --- a/src/templates/partials/languages/tl.html +++ /dev/null @@ -1,57 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "tl", - "action.edit": "I-edit ang pahinang ito", - "action.skip": "I-skip tungo sa nilalaman", - "clipboard.copy": "Kopyahin sa clipboard", - "clipboard.copied": "Nakopya mula sa clipboard", - "footer": "Lagdang Pangwakas", - "footer.next": "Susunod", - "footer.previous": "Nakaraan", - "header": "Pamuhatan", - "meta.comments": "Mga Komento", - "meta.source": "Pinagmulan", - "nav": "Nabigasyon", - "search": "Hanapin", - "search.placeholder": "Hanapin", - "search.share": "Ibahagi", - "search.reset": "Tanggalin", - "search.result.initializer": "Sinisimulan ang paghahanap", - "search.result.placeholder": "Mag-type upang simulan ang paghahanap", - "search.result.none": "Walang nahanap na dokumento", - "search.result.one": "1 magkatugmang dokumento", - "search.result.other": "# magkatugmang mga dokumento", - "search.result.more.one": "1 meron sa pahina na ito", - "search.result.more.other": "# meron sa pahina na ito", - "search.result.term.missing": "Nawawala", - "select.language": "Pumili ng lenguwahe", - "select.version": "Pumili ng bersyon", - "source": "Pumunta sa repository", - "source.file.date.created": "Nagawa", - "source.file.date.updated": "Huling update", - "tabs": "Mga tala", - "toc": "Talaan ng nilalaman", - "top": "Bumalik sa taas" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/tr.html b/src/templates/partials/languages/tr.html deleted file mode 100644 index 860f8ed7..00000000 --- a/src/templates/partials/languages/tr.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "tr", - "action.edit": "Düzenle", - "action.skip": "Ana içeriğe geç", - "action.view": "Sayfanın kaynağını görüntüle", - "announce.dismiss": "Bir daha gösterme", - "blog.archive": "Arşiv", - "blog.categories": "Kategoriler", - "blog.categories.in": "in", - "blog.continue": "Okumaya devam et", - "blog.draft": "Taslak", - "blog.index": "Dizine geri dön", - "blog.meta": "Metadata", - "blog.references": "İlgili bağlantılar", - "clipboard.copy": "Kopyala", - "clipboard.copied": "Kopyalandı", - "consent.accept": "Kabul et", - "consent.manage": "Ayarları yönet", - "consent.reject": "Reddet", - "footer": "Altbilgi", - "footer.next": "Sonraki", - "footer.previous": "Önceki", - "header": "Başlık", - "meta.comments": "Yorumlar", - "meta.source": "Kaynak", - "nav": "Navigasyon", - "readtime.one": "1 dakika okuma", - "readtime.other": "# dakika okuma", - "rss.created": "RSS beslemesi", - "rss.updated": "Güncellenmiş içeriğin RSS beslemesi", - "search": "Ara", - "search.config.lang": "tr", - "search.placeholder": "Ara", - "search.share": "Paylaş", - "search.reset": "Temizle", - "search.result.initializer": "Arama başlatılıyor", - "search.result.placeholder": "Aramaya başlamak için yazın", - "search.result.none": "Eşleşen doküman bulunamadı", - "search.result.one": "1 doküman bulundu", - "search.result.other": "# doküman bulundu", - "search.result.more.one": "Bu sayfada 1 tane daha", - "search.result.more.other": "Bu sayfada # tane daha", - "search.result.term.missing": "Eksik", - "select.language": "Dil seç", - "select.version": "Versiyon seç", - "source": "Depoya git", - "source.file.contributors": "Katkıda bulunanlar", - "source.file.date.created": "Oluşturuldu", - "source.file.date.updated": "Son Güncelleme", - "tabs": "Sekmeler", - "toc": "İçindekiler", - "top": "Başa dön" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/uk.html b/src/templates/partials/languages/uk.html deleted file mode 100644 index ca5c709c..00000000 --- a/src/templates/partials/languages/uk.html +++ /dev/null @@ -1,75 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "uk", - "action.edit": "Редагувати сторінку", - "action.skip": "Перейти до змісту", - "action.view": "Переглянути вихідний код сторінки", - "announce.dismiss": "Більше не показувати", - "blog.archive": "Архівувати", - "blog.categories": "Категорії", - "blog.categories.in": "в", - "blog.continue": "Читати далі", - "blog.draft": "Чернетка", - "blog.index": "Повернутись на головну", - "blog.meta": "Метадані", - "blog.references": "Пов'язані посилання", - "clipboard.copy": "Скопіювати в буфер", - "clipboard.copied": "Скопійовано в буфер", - "consent.accept": "Прийняти", - "consent.manage": "Керувати налаштуваннями", - "consent.reject": "Відхилити", - "footer": "Футер", - "footer.next": "Вперед", - "footer.previous": "Назад", - "header": "Хедер", - "meta.comments": "Коментарі", - "meta.source": "Вихідний код", - "nav": "Навігація", - "readtime.one": "Час на прочитання: 1 хвилина", - "readtime.other": "Час на прочитання: # хвилин", - "rss.created": "RSS стрічка", - "rss.updated": "RSS стрічка оновленого контенту", - "search": "Шукати", - "search.placeholder": "Пошук", - "search.share": "Поділитись", - "search.reset": "Очистити", - "search.result.initializer": "Пошук розпочато", - "search.result.placeholder": "Розпочніть писати для пошуку", - "search.result.none": "Збігів не знайдено", - "search.result.one": "Знайдено 1 збіг", - "search.result.other": "Знайдено # збігів", - "search.result.more.one": "Ще 1 збіг на цій сторінці", - "search.result.more.other": "Ще # збігів на цій сторінці", - "search.result.term.missing": "Не знайдено запиту", - "select.language": "Обрати мову", - "select.version": "Обрати версію", - "source": "Перейти до вихідного коду", - "source.file.contributors": "Контриб'ютори", - "source.file.date.created": "Створено", - "source.file.date.updated": "Востаннє оновлено", - "tabs": "Вкладки", - "toc": "Зміст", - "top": "Повернутись нагору" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/ur.html b/src/templates/partials/languages/ur.html deleted file mode 100644 index 14a50588..00000000 --- a/src/templates/partials/languages/ur.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "ur", - "direction": "rtl", - "action.edit": "اس صفحے میں ترمیم کریں", - "action.skip": "براہِ راست مواد پر جائیں", - "action.view": "اس صفحہ کا ماخذ دیکھیں", - "announce.dismiss": "اسے دوبارہ مت دکھائیں", - "blog.archive": "محفوظ شدہ", - "blog.categories": "اقسام", - "blog.categories.in": "میں", - "blog.continue": "پڑھنا جاری رکھیے", - "blog.draft": "ڈرافٹ", - "blog.index": "واپس انڈیکس پر جائیں", - "blog.meta": "میٹا ڈیٹا", - "blog.references": "متعلقہ لنکس", - "clipboard.copy": "کلِپ بورڈ میں نقل کریں", - "clipboard.copied": "کلِپ بورڈ میں نقل کر دیا گیا", - "consent.accept": "قبول کریں", - "consent.manage": "سیٹینگ بدلیں", - "consent.reject": "رد کرنا", - "footer": "ذیلی تحریر", - "footer.next": "اگلا", - "footer.previous": "پچھلا", - "header": "سر تحریر", - "meta.comments": "تبصرے", - "meta.source": "ذریعہ", - "nav": "رہنمائی", - "readtime.one": "1 منٹ لگے گا", - "readtime.other": "# منٹ لگیں گے", - "rss.created": "RSS فیڈ", - "rss.updated": "تازہ ترین مواد کی RSS فیڈ", - "search": "تلاش", - "search.config.pipeline": " ", - "search.placeholder": "تلاش کریں", - "search.share": "اشتراک کریں", - "search.reset": "صاف کریں", - "search.result.initializer": "تلاش کا آغاز ہو رہا ہے", - "search.result.placeholder": "تلاش شروع کرنے کے لئے ٹائپ کریں", - "search.result.none": "کوئی ملتی جلتی دستاویزات نہیں", - "search.result.one": "۱ ملتی جلتی دستاویز", - "search.result.other": "# ملتی جلتی دستاویزات", - "search.result.more.one": "اِس صفحے پر مزید ۱", - "search.result.more.other": "اِس صفحے پر مزید #", - "search.result.term.missing": "گمشدہ", - "select.language": "زبان کا انتخاب کریں", - "select.version": "ورژن کا انتخاب کریں", - "source": "ریپازٹری پر جائیں", - "source.file.contributors": "تعاون کار", - "source.file.date.created": "تخلیق", - "source.file.date.updated": "آخری بار تجدید", - "tabs": "ٹیبز", - "toc": "فہرست", - "top": "واپس اوپر جائیں" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/uz.html b/src/templates/partials/languages/uz.html deleted file mode 100644 index d86f4db2..00000000 --- a/src/templates/partials/languages/uz.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "uz", - "action.edit": "Ushbu sahifani tahrirlash", - "action.skip": "Tarkibga o'tish", - "action.view": "Ushbu sahifaning manbasini ko'rish", - "announce.dismiss": "Buni boshqa ko'rsatma", - "blog.archive": "Arxiv", - "blog.categories": "Kategoriyalar", - "blog.categories.in": "ichida", - "blog.continue": "O'qishni davom ettiring", - "blog.draft": "Qoralama", - "blog.index": "Indeks sahifasiga qaytish", - "blog.meta": "Metama'lumot", - "blog.references": "Bog'liq havolalar", - "clipboard.copy": "Buferga nusxalash", - "clipboard.copied": "Buferga nusxalandi", - "consent.accept": "Qabul qilish", - "consent.manage": "Sozlamalarni boshqarish", - "consent.reject": "Rad etish", - "footer": "Pastgi qism", - "footer.next": "Keyingi sahifa", - "footer.previous": "Oldingi sahifa", - "header": "Sarlavha", - "meta.comments": "Izohlar", - "meta.source": "Manba", - "nav": "Navigatsiya", - "readtime.one": "1 daqiqa o'qish", - "readtime.other": "# daqiqa o'qish", - "rss.created": "RSS tasmasi", - "rss.updated": "Yangilangan kontentning RSS tasmasi", - "search": "Qidirish", - "search.config.lang": "tr", - "search.placeholder": "Qidirish", - "search.share": "Ulashish", - "search.reset": "Tozalash", - "search.result.initializer": "Qidiruv ishga tushirilmoqda", - "search.result.placeholder": "Qidiruvni boshlash uchun kiriting", - "search.result.none": "Mos natijalar yo'q", - "search.result.one": "1 ta mos natija", - "search.result.other": "# ta mos keladigan natijalar", - "search.result.more.one": "Ushbu sahifada yana 1 ta natija", - "search.result.more.other": "Bu sahifada yana # ta natija", - "search.result.term.missing": "To'ldirilmagan", - "select.language": "Tilni tanlang", - "select.version": "Versiyani tanlang", - "source": "Repozitoriyga o'tish", - "source.file.contributors": "Hissa qo'shuvchilar", - "source.file.date.created": "Yaratildi", - "source.file.date.updated": "Oxirgi yangilanish", - "tabs": "Yorliqlar", - "toc": "Mundarija", - "top": "Yuqoriga qaytish" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/vi.html b/src/templates/partials/languages/vi.html deleted file mode 100644 index b63a8d82..00000000 --- a/src/templates/partials/languages/vi.html +++ /dev/null @@ -1,76 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "vi", - "action.edit": "Chỉnh sửa", - "action.skip": "Bỏ qua", - "action.view": "Xem mã nguồn của trang", - "announce.dismiss": "Không hiển thị lại", - "blog.archive": "Lưu trữ", - "blog.categories": "Mục", - "blog.categories.in": "Trong", - "blog.continue": "Tiếp tục đọc", - "blog.draft": "Bản nháp", - "blog.index": "Quay lại", - "blog.meta": "Metadata", - "blog.references": "Các liên kết liên quan", - "clipboard.copy": "Sao chép vào bộ nhớ tạm", - "clipboard.copied": "Đã sao chép", - "consent.accept": "Đồng ý", - "consent.manage": "Cài đặt", - "consent.reject": "Từ chối", - "footer": "Chân trang", - "footer.next": "Sau", - "footer.previous": "Trước", - "header": "Đầu trang", - "meta.comments": "Bình luận", - "meta.source": "Mã nguồn", - "nav": "Thanh điều hướng", - "readtime.one": "1 phút đọc", - "readtime.other": "# phút đọc", - "rss.created": "RSS feed", - "rss.updated": "RSS feed of updated content", - "search": "Tìm kiếm", - "search.config.lang": "vi", - "search.placeholder": "Tìm kiếm", - "search.share": "Chia sẻ", - "search.reset": "Xoá", - "search.result.initializer": "Initializing search", - "search.result.placeholder": "Nhập để bắt đầu tìm kiếm", - "search.result.none": "Không tìm thấy tài liệu liên quan", - "search.result.one": "1 tài liệu liên quan", - "search.result.other": "# tài liệu liên quan", - "search.result.more.one": "1 từ khác trong trang", - "search.result.more.other": "# từ khác trong trang", - "search.result.term.missing": "Không", - "select.language": "Chọn ngôn ngữ", - "select.version": "Chọn phiên bản", - "source": "Xem mã nguồn", - "source.file.contributors": "Contributors", - "source.file.date.created": "Tạo", - "source.file.date.updated": "Cập nhật lần cuối", - "tabs": "Tabs", - "toc": "Mục lục", - "top": "Trở lại mục lục" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh-Hant.html b/src/templates/partials/languages/zh-Hant.html deleted file mode 100644 index 578fc82a..00000000 --- a/src/templates/partials/languages/zh-Hant.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "zh-Hant", - "action.edit": "編輯此頁", - "action.skip": "跳轉至", - "action.view": "查看源代碼", - "announce.dismiss": "不再顯示此訊息", - "blog.archive": "存檔", - "blog.categories": "分類", - "blog.categories.in": "分類在", - "blog.continue": "繼續閲讀", - "blog.draft": "草稿", - "blog.index": "回到首頁", - "blog.meta": "元數據", - "blog.references": "相關鏈接", - "clipboard.copy": "拷貝", - "clipboard.copied": "已拷貝", - "consent.accept": "接受", - "consent.manage": "管理設置", - "consent.reject": "拒絕", - "footer": "頁脚", - "footer.next": "下一頁", - "footer.previous": "上一頁", - "header": "頁首", - "meta.comments": "評論", - "meta.source": "來源", - "search.config.pipeline": "stemmer", - "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", - "nav": "導航", - "readtime.one": "需要 1 分鐘閲讀", - "readtime.other": "需要 # 分鐘閲讀", - "rss.created": "簡易資訊聚合", - "rss.updated": "更新之部分的簡易資訊聚合", - "search": "搜尋", - "search.placeholder": "搜尋", - "search.share": "分享", - "search.reset": "清空", - "search.result.initializer": "正在初始化搜尋引擎", - "search.result.placeholder": "鍵入以開始檢索", - "search.result.none": "沒有找到符合條件的結果", - "search.result.one": "找到 1 个符合條件的結果", - "search.result.other": "找到 # 個符合條件的結果", - "search.result.more.one": "此頁尚有 1 個符合的項目", - "search.result.more.other": "此頁尚有 # 個符合的項目", - "search.result.term.missing": "缺失", - "select.language": "選擇語言", - "select.version": "選擇版本", - "source": "前往倉庫", - "source.file.contributors": "貢獻者", - "source.file.date.created": "建立日期", - "source.file.date.updated": "最後更新", - "tabs": "標籤頁", - "toc": "目錄", - "top": "回到頂部" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh-TW.html b/src/templates/partials/languages/zh-TW.html deleted file mode 100644 index 405538f8..00000000 --- a/src/templates/partials/languages/zh-TW.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "zh-TW", - "action.edit": "編輯此頁", - "action.skip": "跳轉到", - "action.view": "查看此頁原始碼", - "announce.dismiss": "不再顯示此訊息", - "blog.archive": "封存", - "blog.categories": "分類", - "blog.categories.in": "於", - "blog.continue": "繼續閱讀", - "blog.draft": "草稿", - "blog.index": "回到主頁", - "blog.meta": "元數據", - "blog.references": "相關連結", - "clipboard.copy": "複製", - "clipboard.copied": "已複製", - "consent.accept": "同意", - "consent.manage": "管理設定", - "consent.reject": "拒絕", - "footer": "頁腳", - "footer.next": "下一頁", - "footer.previous": "上一頁", - "header": "頁首", - "meta.comments": "留言", - "meta.source": "來源", - "nav": "導覽列", - "readtime.one": "需要 1 分鐘閱讀時間", - "readtime.other": "需要 # 分鐘閱讀時間", - "rss.created": "RSS 訂閱", - "rss.updated": "RSS 訂閱內容已更新", - "search": "搜尋", - "search.config.pipeline": "stemmer", - "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", - "search.placeholder": "搜尋", - "search.share": "分享", - "search.reset": "清除", - "search.result.initializer": "正在初始化搜尋引擎", - "search.result.placeholder": "打字進行搜尋", - "search.result.none": "沒有符合的項目", - "search.result.one": "找到 1 個符合的項目", - "search.result.other": "找到 # 個符合的項目", - "search.result.more.one": "此頁尚有 1 個符合的項目", - "search.result.more.other": "此頁尚有 # 個符合的項目", - "search.result.term.missing": "缺少字詞", - "select.language": "選擇語言", - "select.version": "選擇版本", - "source": "前往倉庫", - "source.file.contributors": "貢獻者", - "source.file.date.created": "建立日期", - "source.file.date.updated": "最後更新", - "tabs": "標籤", - "toc": "目錄", - "top": "回到頂端" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/languages/zh.html b/src/templates/partials/languages/zh.html deleted file mode 100644 index 49f233a4..00000000 --- a/src/templates/partials/languages/zh.html +++ /dev/null @@ -1,77 +0,0 @@ - - - -{% macro t(key) %}{{ { - "language": "zh", - "action.edit": "编辑此页", - "action.skip": "跳转至", - "action.view": "查看本页的源代码", - "announce.dismiss": "不再显示此消息", - "blog.archive": "归档", - "blog.categories": "分类", - "blog.categories.in": "分类于", - "blog.continue": "继续阅读", - "blog.draft": "草稿", - "blog.index": "回到主页", - "blog.meta": "元数据", - "blog.references": "相关链接", - "clipboard.copy": "复制", - "clipboard.copied": "已复制", - "consent.accept": "同意", - "consent.manage": "管理设定", - "consent.reject": "拒绝", - "footer": "页脚", - "footer.next": "下一页", - "footer.previous": "上一页", - "header": "页眉", - "meta.comments": "评论", - "meta.source": "来源", - "nav": "导航栏", - "readtime.one": "需要 1 分钟阅读时间", - "readtime.other": "需要 # 分钟阅读时间", - "rss.created": "RSS 订阅", - "rss.updated": "已更新内容的 RSS 订阅", - "search": "查找", - "search.config.pipeline": "stemmer", - "search.config.separator": "[\\s\\u200b\\u3000\\-、。,.?!;]+", - "search.placeholder": "搜索", - "search.share": "分享", - "search.reset": "清空当前内容", - "search.result.initializer": "正在初始化搜索引擎", - "search.result.placeholder": "键入以开始搜索", - "search.result.none": "没有找到符合条件的结果", - "search.result.one": "找到 1 个符合条件的结果", - "search.result.other": "# 个符合条件的结果", - "search.result.more.one": "在该页上还有 1 个符合条件的结果", - "search.result.more.other": "在该页上还有 # 个符合条件的结果", - "search.result.term.missing": "缺少", - "select.language": "选择当前语言", - "select.version": "选择当前版本", - "source": "前往仓库", - "source.file.contributors": "贡献者", - "source.file.date.created": "创建日期", - "source.file.date.updated": "最后更新", - "tabs": "标签", - "toc": "目录", - "top": "回到页面顶部" -}[key] }}{% endmacro %} diff --git a/src/templates/partials/logo.html b/src/templates/partials/logo.html deleted file mode 100644 index 05832c71..00000000 --- a/src/templates/partials/logo.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -{% if config.theme.logo %} - logo -{% else %} - {% set icon = config.theme.icon.logo or "material/library" %} - {% include ".icons/" ~ icon ~ ".svg" %} -{% endif %} diff --git a/src/templates/partials/nav-item.html b/src/templates/partials/nav-item.html deleted file mode 100644 index 24d74a1a..00000000 --- a/src/templates/partials/nav-item.html +++ /dev/null @@ -1,249 +0,0 @@ - - - -{% macro render_status(nav_item, type) %} - {% set class = "md-status md-status--" ~ type %} - - - {% if config.extra.status and config.extra.status[type] %} - - - - - {% else %} - - {% endif %} -{% endmacro %} - - -{% macro render_content(nav_item, ref = nav_item) %} - - - {% if nav_item.is_page and nav_item.meta.icon %} - {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} - {% endif %} - - - - {{ ref.title }} - - - - {% if nav_item.is_page and nav_item.meta.status %} - {{ render_status(nav_item, nav_item.meta.status) }} - {% endif %} -{% endmacro %} - - -{% macro render_pruned(nav_item, ref = nav_item) %} - {% set first = nav_item.children | first %} - - - {% if first and first.children %} - {{ render_pruned(first, ref) }} - - - {% else %} - - {{ render_content(ref) }} - - - {% if nav_item.children | length > 0 %} - - {% endif %} - - {% endif %} -{% endmacro %} - - -{% macro render(nav_item, path, level) %} - - - {% set class = "md-nav__item" %} - {% if nav_item.active %} - {% set class = class ~ " md-nav__item--active" %} - {% endif %} - - - {% if nav_item.children %} - - - {% 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 %} - - - {% set tabs = "navigation.tabs" in features %} - {% set sections = "navigation.sections" in features %} - {% if tabs and level == 1 or sections and tabs >= level - 1 %} - {% set class = class ~ " md-nav__item--section" %} - {% set is_section = true %} - - - {% elif not nav_item.active and "navigation.prune" in features %} - {% set class = class ~ " md-nav__item--pruned" %} - {% set is_pruned = true %} - {% endif %} - - -
  • - {% if not is_pruned %} - {% set checked = "checked" if nav_item.active %} - - - {% set is_expanded = "navigation.expand" in features %} - {% if is_expanded and not checked %} - {% set indeterminate = "md-toggle--indeterminate" %} - {% endif %} - - - - - - {% if not indexes %} - {% set tabindex = "0" if not is_section %} - - - - {% else %} - {% set index = indexes | first %} - {% set class = "md-nav__link--active" if index == page %} - - {% endif %} - - - - - - {% else %} - {{ render_pruned(nav_item) }} - {% endif %} -
  • - - - {% elif nav_item == page %} -
  • - {% set toc = page.toc %} - - - - - - {% set first = toc | first %} - {% if first and first.level == 1 %} - {% set toc = first.children %} - {% endif %} - - - {% if toc %} - - {% endif %} - - {{ render_content(nav_item) }} - - - - {% if toc %} - {% include "partials/toc.html" %} - {% endif %} -
  • - - - {% else %} -
  • - - {{ render_content(nav_item) }} - -
  • - {% endif %} -{% endmacro %} diff --git a/src/templates/partials/nav.html b/src/templates/partials/nav.html deleted file mode 100644 index c41fe694..00000000 --- a/src/templates/partials/nav.html +++ /dev/null @@ -1,69 +0,0 @@ - - -{% import "partials/nav-item.html" as item with context %} - - -{% 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 %} - - - diff --git a/src/templates/partials/pagination.html b/src/templates/partials/pagination.html deleted file mode 100644 index 046ecbe9..00000000 --- a/src/templates/partials/pagination.html +++ /dev/null @@ -1,42 +0,0 @@ - - - -{% import ".icons/material/chevron-double-left.svg" as icon_first %} -{% import ".icons/material/chevron-left.svg" as icon_previous %} -{% import ".icons/material/chevron-right.svg" as icon_next %} -{% import ".icons/material/chevron-double-right.svg" as icon_last %} - - - diff --git a/src/templates/partials/palette.html b/src/templates/partials/palette.html deleted file mode 100644 index ccb8db0a..00000000 --- a/src/templates/partials/palette.html +++ /dev/null @@ -1,55 +0,0 @@ - - - -
    - {% for option in config.theme.palette %} - {% set scheme = option.scheme | d("default", true) %} - {% set primary = option.primary | d("indigo", true) %} - {% set accent = option.accent | d("indigo", true) %} - - {% if option.toggle %} - - {% endif %} - {% endfor %} -
    diff --git a/src/templates/partials/post.html b/src/templates/partials/post.html deleted file mode 100644 index c7233051..00000000 --- a/src/templates/partials/post.html +++ /dev/null @@ -1,99 +0,0 @@ - - - -
    -
    - - - {% if post.authors %} - - {% endif %} - - - -
    - - -
    - {{ post.content }} - - - -
    -
    diff --git a/src/templates/partials/progress.html b/src/templates/partials/progress.html deleted file mode 100644 index f5d13d10..00000000 --- a/src/templates/partials/progress.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -
    diff --git a/src/templates/partials/search.html b/src/templates/partials/search.html deleted file mode 100644 index 1854a7d3..00000000 --- a/src/templates/partials/search.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - diff --git a/src/templates/partials/social.html b/src/templates/partials/social.html deleted file mode 100644 index 5d2c4017..00000000 --- a/src/templates/partials/social.html +++ /dev/null @@ -1,48 +0,0 @@ - - - -
    - {% for social in config.extra.social %} - - - {% set rel = "noopener" %} - {% if "mastodon" in social.icon %} - {% set rel = rel ~ " me" %} - {% endif %} - - - {% set title = social.name %} - {% if not title and "//" in social.link %} - {% set _, url = social.link.split("//") %} - {% set title = url.split("/")[0] %} - {% endif %} - - {% include ".icons/" ~ social.icon ~ ".svg" %} - - {% endfor %} -
    diff --git a/src/templates/partials/source-file.html b/src/templates/partials/source-file.html deleted file mode 100644 index 928e35de..00000000 --- a/src/templates/partials/source-file.html +++ /dev/null @@ -1,44 +0,0 @@ - - - -
    -
    - - - - {% 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 %} -
    - {{ lang.t("source.file.date.created") }}: - {{ page.meta.git_creation_date_localized }} - {% endif %} - - - {% elif page.meta.revision_date %} - {{ lang.t("source.file.date.updated") }}: - {{ page.meta.revision_date }} - {% endif %} -
    -
    diff --git a/src/templates/partials/source.html b/src/templates/partials/source.html deleted file mode 100644 index f4aac3e6..00000000 --- a/src/templates/partials/source.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - -
    - {% set icon = config.theme.icon.repo or "fontawesome/brands/git-alt" %} - {% include ".icons/" ~ icon ~ ".svg" %} -
    -
    - {{ config.repo_name }} -
    -
    diff --git a/src/templates/partials/tabs-item.html b/src/templates/partials/tabs-item.html deleted file mode 100644 index 7a12a742..00000000 --- a/src/templates/partials/tabs-item.html +++ /dev/null @@ -1,71 +0,0 @@ - - - -{% macro render_content(nav_item, ref = nav_item) %} - - - {% if nav_item == ref or "navigation.indexes" in features %} - {% if nav_item.is_index and nav_item.meta.icon %} - {% include ".icons/" ~ nav_item.meta.icon ~ ".svg" %} - {% endif %} - {% endif %} - - - {{ ref.title }} -{% endmacro %} - - -{% macro render(nav_item, ref = nav_item) %} - - - {% set class = "md-tabs__item" %} - {% if ref.active %} - {% set class = class ~ " md-tabs__item--active" %} - {% endif %} - - - {% if nav_item.children %} - {% set first = nav_item.children | first %} - - - {% if first.children %} - {{ render(first, ref) }} - - - {% else %} -
  • - - {{ render_content(first, ref) }} - -
  • - {% endif %} - - - {% else %} -
  • - - {{ render_content(nav_item) }} - -
  • - {% endif %} -{% endmacro %} diff --git a/src/templates/partials/tabs.html b/src/templates/partials/tabs.html deleted file mode 100644 index 0ea590cf..00000000 --- a/src/templates/partials/tabs.html +++ /dev/null @@ -1,38 +0,0 @@ - - -{% import "partials/tabs-item.html" as item with context %} - - - diff --git a/src/templates/partials/tags.html b/src/templates/partials/tags.html deleted file mode 100644 index b3dea295..00000000 --- a/src/templates/partials/tags.html +++ /dev/null @@ -1,52 +0,0 @@ - - - -{% if page.meta and page.meta.hide %} - {% set hidden = "hidden" if "tags" in page.meta.hide %} -{% endif %} - - - diff --git a/src/templates/partials/toc-item.html b/src/templates/partials/toc-item.html deleted file mode 100644 index 1af82c56..00000000 --- a/src/templates/partials/toc-item.html +++ /dev/null @@ -1,39 +0,0 @@ - - - -
  • - - {{ toc_item.title }} - - - - {% if toc_item.children %} - - {% endif %} -
  • diff --git a/src/templates/partials/toc.html b/src/templates/partials/toc.html deleted file mode 100644 index cb50b257..00000000 --- a/src/templates/partials/toc.html +++ /dev/null @@ -1,56 +0,0 @@ - - - -{% set title = lang.t("toc") %} -{% if config.mdx_configs.toc and config.mdx_configs.toc.title %} - {% set title = config.mdx_configs.toc.title %} -{% endif %} - - - diff --git a/src/templates/partials/top.html b/src/templates/partials/top.html deleted file mode 100644 index 737e6248..00000000 --- a/src/templates/partials/top.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - diff --git a/src/templates/redirect.html b/src/templates/redirect.html deleted file mode 100644 index 80869c4f..00000000 --- a/src/templates/redirect.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - {{ config.site_name }} - - - - - diff --git a/src/test.py b/src/test.py deleted file mode 100644 index 349b1bfd..00000000 --- a/src/test.py +++ /dev/null @@ -1,4 +0,0 @@ -import infini - -client = infini.Cli() -client.parse_args() -- cgit v1.2.3-70-g09d2