diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..b601136 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,31 @@ +# Update the NODE_VERSION arg in docker-compose.yml to pick a Node version: 10, 12, 14 +ARG NODE_VERSION=14 +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${NODE_VERSION} + +# VARIANT can be either 'hugo' for the standard version or 'hugo_extended' for the extended version. +ARG VARIANT=hugo +# VERSION can be either 'latest' or a specific version number +ARG VERSION=latest + +# Download Hugo +RUN apt-get update && apt-get install -y ca-certificates openssl git curl && \ + rm -rf /var/lib/apt/lists/* && \ + case ${VERSION} in \ + latest) \ + export VERSION=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}') ;;\ + esac && \ + echo ${VERSION} && \ + wget -O ${VERSION}.tar.gz https://github.com/gohugoio/hugo/releases/download/v${VERSION}/${VARIANT}_${VERSION}_Linux-64bit.tar.gz && \ + tar xf ${VERSION}.tar.gz && \ + mv hugo /usr/bin/hugo + +# Hugo dev server port +EXPOSE 1313 + +# [Optional] Uncomment this section to install additional OS packages you may want. +# +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install more global node packages +# RUN sudo -u node npm install -g diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4bf8a19 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,45 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.202.3/containers/hugo +{ + "name": "Hugo (Community)", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update VARIANT to pick hugo variant. + // Example variants: hugo, hugo_extended + // Rebuild the container if it already exists to update. + "VARIANT": "hugo_extended", + // Update VERSION to pick a specific hugo version. + // Example versions: latest, 0.73.0, 0,71.1 + // Rebuild the container if it already exists to update. + "VERSION": "latest", + // Update NODE_VERSION to pick the Node.js version: 12, 14 + "NODE_VERSION": "14", + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "html.format.templating": true, + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "bungcip.better-toml", + "davidanson.vscode-markdownlint" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 1313 + ], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "node", + "features": { + "golang": "latest" + } +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a222d4e..95f0c18 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ -ko_fi: jimmycai \ No newline at end of file +ko_fi: jimmycai +github: CaiJimmy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ff142d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +public +resources +assets/jsconfig.json +.hugo_build.lock \ No newline at end of file diff --git a/README.md b/README.md index ddacf82..1c59619 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,13 @@ > Card-style Hugo theme designed for bloggers. +## Quickstart + +Use this template: [CaiJimmy/hugo-theme-stack-starter](https://github.com/CaiJimmy/hugo-theme-stack-starter) + ## Demo -[Example Site](https://theme-stack.jimmycai.com/) +[Example Site](https://demo.stack.jimmycai.com/) [![Netlify Status](https://api.netlify.com/api/v1/badges/a2d2807a-a905-4bcb-97da-8da8d847da3d/deploy-status)](https://app.netlify.com/sites/hugo-theme-stack/deploys) @@ -28,6 +32,7 @@ Stack is a simple card-style Hugo theme designed for bloggers, some of its featu - Properly cropped thumbnails - Subsection support - Table of contents +- Multilingual mode and RTL support ## Requirements @@ -35,7 +40,10 @@ It's necessary to use **Hugo Extended ≥ 0.87.0**. ## Installation -Clone / Download this repository to `theme` folder, and edit your site config following `exampleSite/config.yaml`. +* Route 1: Clone / Download this repository to `theme` folder +* Route 2: Turn your site into a hugo module and add this theme as a module dependency + + Edit your site config following `exampleSite/config.yaml`. *Note: Remove `config.toml` if there is one in the site folder.* @@ -77,4 +85,4 @@ Some references that I took while building this theme: | Project | Licence| | ------- | ------| | [artchen/hexo-theme-element](https://github.com/artchen/hexo-theme-element) | [MIT](https://github.com/artchen/hexo-theme-element/blob/master/LICENSE) | -| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) | \ No newline at end of file +| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) | diff --git a/assets/icons/brand-github.svg b/assets/icons/brand-github.svg new file mode 100644 index 0000000..1fe7e0b --- /dev/null +++ b/assets/icons/brand-github.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/brand-twitter.svg b/assets/icons/brand-twitter.svg new file mode 100644 index 0000000..17ab1b1 --- /dev/null +++ b/assets/icons/brand-twitter.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/categories.svg b/assets/icons/categories.svg new file mode 100644 index 0000000..e00ab1d --- /dev/null +++ b/assets/icons/categories.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/language.svg b/assets/icons/language.svg new file mode 100644 index 0000000..66ede1c --- /dev/null +++ b/assets/icons/language.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/jsconfig.json b/assets/jsconfig.json index 9321136..040177a 100644 --- a/assets/jsconfig.json +++ b/assets/jsconfig.json @@ -6,5 +6,7 @@ "*" ] }, + "lib": ["es2020", "dom"], + "jsx": "preserve" } } \ No newline at end of file diff --git a/assets/scss/grid.scss b/assets/scss/grid.scss index a0eddca..84967ef 100644 --- a/assets/scss/grid.scss +++ b/assets/scss/grid.scss @@ -11,7 +11,7 @@ /// Display right sidebar when min-width: lg @include respond(lg) { - display: block; + display: flex; } } @@ -67,24 +67,6 @@ } } - &.align-items--flex-start { - align-items: flex-start; - } - - .grow { - flex-grow: 1; - } - - .do-not-shrink { - flex-shrink: 0; - } - - .do-not-overflow { - min-width: 0; - flex-shrink: 1; - max-width: 100%; - } - .full-width { width: 100%; } @@ -94,15 +76,22 @@ main.main { min-width: 0; max-width: 100%; flex-grow: 1; - padding-top: var(--main-top-padding); + display: flex; + flex-direction: column; + gap: var(--section-separation); + + @include respond(md) { + padding-top: var(--main-top-padding); + } } .main-container { min-height: 100vh; align-items: flex-start; padding: 0 15px; - column-gap: var(--section-separation); - + gap: var(--section-separation); + padding-top: var(--main-top-padding); + @include respond(md) { padding: 0 20px; } diff --git a/assets/scss/partials/article.scss b/assets/scss/partials/article.scss index 459c22f..f085ff0 100644 --- a/assets/scss/partials/article.scss +++ b/assets/scss/partials/article.scss @@ -2,6 +2,7 @@ .article-list { display: flex; flex-direction: column; + gap: var(--section-separation); article { display: flex; @@ -17,10 +18,6 @@ box-shadow: var(--shadow-l2); } - &:not(:last-of-type) { - margin-bottom: var(--section-separation); - } - .article-image { img { width: 100%; @@ -52,13 +49,13 @@ display: flex; flex-direction: column; justify-content: center; - padding: var(--card-padding); + gap: 15px; } .article-title { font-weight: 600; - margin: 10px 0; + margin: 0; color: var(--card-text-color-main); font-size: 2.2rem; @@ -73,52 +70,58 @@ color: var(--card-text-color-main); } } - - & + .article-subtitle { - margin-top: 0; - } } .article-subtitle { font-weight: normal; color: var(--card-text-color-secondary); - margin: 5px 0; line-height: 1.5; - + margin: 0; font-size: 1.75rem; @include respond(xl) { font-size: 2rem; } } -.article-time { +.article-title-wrapper { + display: flex; + flex-direction: column; + gap: 8px; +} + +.article-time, +.article-translations { display: flex; align-items: center; color: var(--card-text-color-tertiary); gap: 15px; - margin-top: 10px; flex-wrap: wrap; svg { vertical-align: middle; - margin-right: 15px; width: 20px; height: 20px; stroke-width: 1.33; } - time { + time, + a { font-size: 1.4rem; + color: var(--card-text-color-tertiary); } & > div { display: inline-flex; align-items: center; + gap: 15px; } } .article-category, .article-tags { + display: flex; + gap: 10px; + a { color: var(--accent-color-text); background-color: var(--accent-color); @@ -126,8 +129,6 @@ border-radius: var(--tag-border-radius); display: inline-block; font-size: 1.4rem; - margin-right: 10px; - margin-bottom: 10px; transition: background-color 0.5s ease; &:hover { @@ -148,15 +149,12 @@ --image-size: 60px; } - & + .pagination { - margin-top: var(--section-separation); - } - article { & > a { display: flex; align-items: center; padding: var(--small-card-padding); + gap: 15px; } &:not(:last-of-type) { @@ -166,8 +164,8 @@ .article-details { flex-grow: 1; padding: 0; - padding-right: 15px; min-height: var(--image-size); + gap: 10px; } .article-title { diff --git a/assets/scss/partials/base.scss b/assets/scss/partials/base.scss index ab3bf42..efb4b8f 100644 --- a/assets/scss/partials/base.scss +++ b/assets/scss/partials/base.scss @@ -1,7 +1,6 @@ html { font-size: 62.5%; overflow-y: scroll; - scroll-behavior: smooth; } * { @@ -36,4 +35,4 @@ body { ::-webkit-scrollbar-track { background-color: transparent; } -/**/ +/**/ \ No newline at end of file diff --git a/assets/scss/partials/footer.scss b/assets/scss/partials/footer.scss index 1c60dc7..ccb7394 100644 --- a/assets/scss/partials/footer.scss +++ b/assets/scss/partials/footer.scss @@ -2,7 +2,6 @@ footer.site-footer { padding: 20px 0 var(--section-separation) 0; font-size: 1.4rem; line-height: 1.75; - margin-top: var(--section-separation); &:before { content: ""; diff --git a/assets/scss/partials/highlight/light.scss b/assets/scss/partials/highlight/light.scss index 9ddcff9..1798218 100644 --- a/assets/scss/partials/highlight/light.scss +++ b/assets/scss/partials/highlight/light.scss @@ -10,7 +10,8 @@ } /* Other */ -.chroma .x {} +.chroma .x { +} /* Error */ .chroma .err { @@ -40,367 +41,369 @@ .chroma .hl { display: block; width: 100%; - background-color: #ffffcc + background-color: #ffffcc; } /* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; - color: #7f7f7f + color: #7f7f7f; } /* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; - color: #7f7f7f + color: #7f7f7f; } /* Keyword */ .chroma .k { - color: #00a8c8 + color: #00a8c8; } /* KeywordConstant */ .chroma .kc { - color: #00a8c8 + color: #00a8c8; } /* KeywordDeclaration */ .chroma .kd { - color: #00a8c8 + color: #00a8c8; } /* KeywordNamespace */ .chroma .kn { - color: #f92672 + color: #f92672; } /* KeywordPseudo */ .chroma .kp { - color: #00a8c8 + color: #00a8c8; } /* KeywordReserved */ .chroma .kr { - color: #00a8c8 + color: #00a8c8; } /* KeywordType */ .chroma .kt { - color: #00a8c8 + color: #00a8c8; } /* Name */ .chroma .n { - color: #111111 + color: #111111; } /* NameAttribute */ .chroma .na { - color: #75af00 + color: #75af00; } /* NameBuiltin */ .chroma .nb { - color: #111111 + color: #111111; } /* NameBuiltinPseudo */ .chroma .bp { - color: #111111 + color: #111111; } /* NameClass */ .chroma .nc { - color: #75af00 + color: #75af00; } /* NameConstant */ .chroma .no { - color: #00a8c8 + color: #00a8c8; } /* NameDecorator */ .chroma .nd { - color: #75af00 + color: #75af00; } /* NameEntity */ .chroma .ni { - color: #111111 + color: #111111; } /* NameException */ .chroma .ne { - color: #75af00 + color: #75af00; } /* NameFunction */ .chroma .nf { - color: #75af00 + color: #75af00; } /* NameFunctionMagic */ .chroma .fm { - color: #111111 + color: #111111; } /* NameLabel */ .chroma .nl { - color: #111111 + color: #111111; } /* NameNamespace */ .chroma .nn { - color: #111111 + color: #111111; } /* NameOther */ .chroma .nx { - color: #75af00 + color: #75af00; } /* NameProperty */ .chroma .py { - color: #111111 + color: #111111; } /* NameTag */ .chroma .nt { - color: #f92672 + color: #f92672; } /* NameVariable */ .chroma .nv { - color: #111111 + color: #111111; } /* NameVariableClass */ .chroma .vc { - color: #111111 + color: #111111; } /* NameVariableGlobal */ .chroma .vg { - color: #111111 + color: #111111; } /* NameVariableInstance */ .chroma .vi { - color: #111111 + color: #111111; } /* NameVariableMagic */ .chroma .vm { - color: #111111 + color: #111111; } /* Literal */ .chroma .l { - color: #ae81ff + color: #ae81ff; } /* LiteralDate */ .chroma .ld { - color: #d88200 + color: #d88200; } /* LiteralString */ .chroma .s { - color: #d88200 + color: #d88200; } /* LiteralStringAffix */ .chroma .sa { - color: #d88200 + color: #d88200; } /* LiteralStringBacktick */ .chroma .sb { - color: #d88200 + color: #d88200; } /* LiteralStringChar */ .chroma .sc { - color: #d88200 + color: #d88200; } /* LiteralStringDelimiter */ .chroma .dl { - color: #d88200 + color: #d88200; } /* LiteralStringDoc */ .chroma .sd { - color: #d88200 + color: #d88200; } /* LiteralStringDouble */ .chroma .s2 { - color: #d88200 + color: #d88200; } /* LiteralStringEscape */ .chroma .se { - color: #8045ff + color: #8045ff; } /* LiteralStringHeredoc */ .chroma .sh { - color: #d88200 + color: #d88200; } /* LiteralStringInterpol */ .chroma .si { - color: #d88200 + color: #d88200; } /* LiteralStringOther */ .chroma .sx { - color: #d88200 + color: #d88200; } /* LiteralStringRegex */ .chroma .sr { - color: #d88200 + color: #d88200; } /* LiteralStringSingle */ .chroma .s1 { - color: #d88200 + color: #d88200; } /* LiteralStringSymbol */ .chroma .ss { - color: #d88200 + color: #d88200; } /* LiteralNumber */ .chroma .m { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberBin */ .chroma .mb { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberFloat */ .chroma .mf { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberHex */ .chroma .mh { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberInteger */ .chroma .mi { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberIntegerLong */ .chroma .il { - color: #ae81ff + color: #ae81ff; } /* LiteralNumberOct */ .chroma .mo { - color: #ae81ff + color: #ae81ff; } /* Operator */ .chroma .o { - color: #f92672 + color: #f92672; } /* OperatorWord */ .chroma .ow { - color: #f92672 + color: #f92672; } /* Punctuation */ .chroma .p { - color: #111111 + color: #111111; } /* Comment */ .chroma .c { - color: #75715e + color: #75715e; } /* CommentHashbang */ .chroma .ch { - color: #75715e + color: #75715e; } /* CommentMultiline */ .chroma .cm { - color: #75715e + color: #75715e; } /* CommentSingle */ .chroma .c1 { - color: #75715e + color: #75715e; } /* CommentSpecial */ .chroma .cs { - color: #75715e + color: #75715e; } /* CommentPreproc */ .chroma .cp { - color: #75715e + color: #75715e; } /* CommentPreprocFile */ .chroma .cpf { - color: #75715e + color: #75715e; } /* Generic */ -.chroma .g {} - +.chroma .g { +} /* GenericDeleted */ -.chroma .gd {} - +.chroma .gd { + color: #f92672; +} /* GenericEmph */ .chroma .ge { - font-style: italic + font-style: italic; } - /* GenericError */ -.chroma .gr {} - +.chroma .gr { +} /* GenericHeading */ -.chroma .gh {} - +.chroma .gh { +} /* GenericInserted */ -.chroma .gi {} - +.chroma .gi { + color: #7ca727; +} /* GenericOutput */ -.chroma .go {} - +.chroma .go { +} /* GenericPrompt */ -.chroma .gp {} - +.chroma .gp { +} /* GenericStrong */ .chroma .gs { - font-weight: bold + font-weight: bold; } - /* GenericSubheading */ -.chroma .gu {} - +.chroma .gu { + color: #75715e; +} /* GenericTraceback */ -.chroma .gt {} - +.chroma .gt { +} /* GenericUnderline */ -.chroma .gl {} - +.chroma .gl { +} /* TextWhitespace */ -.chroma .w {} \ No newline at end of file +.chroma .w { +} diff --git a/assets/scss/partials/layout/archives.scss b/assets/scss/partials/layout/archives.scss deleted file mode 100644 index 34e5f62..0000000 --- a/assets/scss/partials/layout/archives.scss +++ /dev/null @@ -1,3 +0,0 @@ -.archives-group { - margin-bottom: var(--section-separation); -} \ No newline at end of file diff --git a/assets/scss/partials/layout/article.scss b/assets/scss/partials/layout/article.scss index 561361d..36f326a 100644 --- a/assets/scss/partials/layout/article.scss +++ b/assets/scss/partials/layout/article.scss @@ -13,10 +13,6 @@ box-shadow: var(--shadow-l1); overflow: hidden; - &.main-article { - margin-bottom: var(--section-separation); - } - .article-header { .article-image { img { @@ -57,11 +53,11 @@ display: flex; align-items: center; font-size: 1.4rem; + gap: 15px; svg { width: 20px; height: 20px; - margin-right: 15px; stroke-width: 1.33; } } @@ -71,7 +67,8 @@ text-transform: unset; } - .article-copyright, .article-lastmod { + .article-copyright, + .article-lastmod { a { color: var(--body-text-color); } @@ -121,80 +118,6 @@ } } -.article-page.has-toc { - scroll-behavior: smooth; - - .left-sidebar { - display: none; - } - - .right-sidebar { - width: 100%; - padding: 0; - display: none; - - @include respond(xl) { - display: block; - top: var(--main-top-padding); - } - } - - #article-toolbar { - display: block; - - @include respond(md) { - padding: 0; - } - - @include respond(xl) { - margin-top: 0; - position: sticky; - top: var(--main-top-padding); - flex-shrink: 1; - - a { - background: transparent; - box-shadow: none; - border: 1px solid var(--body-text-color); - width: 100%; - margin-right: 0; - - svg { - flex-shrink: 0; - } - } - } - } - - .main-container { - align-items: start; - flex-direction: column; - - @include respond(xl) { - flex-direction: row; - } - } - - .main { - padding-top: 0; - - @include respond(xl) { - padding-top: var(--main-top-padding); - } - } -} - -.widget-head { - display: flex; - align-items: center; - - .widget-title { - font-size: 1.6rem; - font-weight: bold; - color: var(--body-text-color); - } -} - .widget--toc { background-color: var(--card-background); border-radius: var(--card-border-radius); @@ -204,6 +127,10 @@ color: var(--card-text-color-main); overflow: hidden; + ::-webkit-scrollbar-thumb { + background-color: var(--card-separator-color); + } + #TableOfContents { overflow-x: auto; max-height: 75vh; @@ -218,7 +145,7 @@ list-style-type: none; counter-reset: item; - li:before { + li a::before { counter-increment: item; content: counters(item, ".") ". "; font-weight: bold; @@ -231,8 +158,7 @@ } li { - list-style-type: none; - margin: 15px 20px; + margin: 15px 0 15px 20px; padding: 5px; & > ol, @@ -246,11 +172,39 @@ } } } - } -} + li.active-class > a { + border-left: var(--heading-border-size) solid var(--accent-color); + font-weight: bold; + } -.related-contents--wrapper { - margin-bottom: var(--section-separation); + ul li.active-class > a { + display: block; + } + + @function repeat($str, $n) { + $result: ""; + @for $_ from 0 to $n { + $result: $result + $str; + } + @return $result; + } + + // Support up to 6 levels of indentation for lists in ToCs + @for $i from 0 to 5 { + & > ul #{repeat("> li > ul", $i)} > li.active-class > a { + $n: 25 + $i * 35; + margin-left: calc(-#{$n}px - 1em); + padding-left: calc(#{$n}px + 1em - var(--heading-border-size)); + } + + & > ol #{repeat("> li > ol", $i)} > li.active-class > a { + $n: 9 + $i * 35; + margin-left: calc(-#{$n}px - 1em); + padding-left: calc(#{$n}px + 1em - var(--heading-border-size)); + display: block; + } + } + } } .related-contents { @@ -298,9 +252,9 @@ h4, h5, h6 { - margin-left: calc((var(--card-padding)) * -1); - padding-left: calc(var(--card-padding) - var(--heading-border-size)); - border-left: var(--heading-border-size) solid var(--accent-color); + margin-inline-start: calc((var(--card-padding)) * -1); + padding-inline-start: calc(var(--card-padding) - var(--heading-border-size)); + border-inline-start: var(--heading-border-size) solid var(--accent-color); } figure { @@ -315,7 +269,7 @@ blockquote { position: relative; margin: 1.5em 0; - border-left: var(--blockquote-border-size) solid var(--card-separator-color); + border-inline-start: var(--blockquote-border-size) solid var(--card-separator-color); padding: 15px calc(var(--card-padding) - var(--blockquote-border-size)); background-color: var(--blockquote-background-color); } @@ -343,13 +297,10 @@ flex-direction: row; justify-content: center; margin: 1.5em 0; + gap: 10px; figure { margin: 0; - - & + figure { - margin-left: 10px; - } } } @@ -362,7 +313,10 @@ line-height: 1.428571429; word-break: break-all; padding: var(--card-padding); - + // keep Codeblocks LTR + [dir="rtl"] & { + direction: ltr; + } code { color: unset; border: none; @@ -371,6 +325,50 @@ } } + .highlight { + background-color: var(--pre-background-color); + padding: var(--card-padding); + position: relative; + + &:hover { + .copyCodeButton { + opacity: 1; + } + } + // keep Codeblocks LTR + [dir="rtl"] & { + direction: ltr; + } + pre { + margin: initial; + padding: 0; + margin: 0; + width: auto; + } + } + + .copyCodeButton { + position: absolute; + top: calc(var(--card-padding)); + right: calc(var(--card-padding)); + background: var(--card-background); + border: none; + box-shadow: var(--shadow-l2); + border-radius: var(--tag-border-radius); + padding: 8px 16px; + color: var(--card-text-color-main); + cursor: pointer; + font-size: 14px; + opacity: 0; + transition: opacity 0.3s ease; + } + + .table-wrapper { + padding: 0 var(--card-padding); + overflow-x: auto; + display: block; + } + table { width: 100%; border-collapse: collapse; @@ -419,38 +417,14 @@ /// Negative margins blockquote, figure, - .gallery, + .highlight, pre, + .gallery, .video-wrapper, + .table-wrapper, .s_video_simple { margin-left: calc((var(--card-padding)) * -1); margin-right: calc((var(--card-padding)) * -1); width: calc(100% + var(--card-padding) * 2); } - - .highlight { - position: relative; - - &:hover { - .copyCodeButton { - opacity: 1; - } - } - } - - .copyCodeButton { - position: absolute; - top: calc(var(--card-padding)); - right: 0; - background: var(--card-background); - border: none; - box-shadow: var(--shadow-l2); - border-radius: var(--tag-border-radius); - padding: 8px 16px; - color: var(--card-text-color-main); - cursor: pointer; - font-size: 14px; - opacity: 0; - transition: opacity 0.3s ease; - } } diff --git a/assets/scss/partials/layout/list.scss b/assets/scss/partials/layout/list.scss index 1a0e346..d7815ca 100644 --- a/assets/scss/partials/layout/list.scss +++ b/assets/scss/partials/layout/list.scss @@ -3,21 +3,16 @@ background-color: var(--card-background); padding: var(--small-card-padding); box-shadow: var(--shadow-l1); - margin-bottom: var(--section-separation); display: flex; align-items: center; + gap: 20px; --separation: 15px; .section-term { font-size: 2.2rem; margin: 0; - margin-top: calc(var(--separation) / 2); color: var(--card-text-color-main); - - & + .section-description { - margin-top: var(--separation); - } } .section-description { @@ -29,7 +24,9 @@ .section-details { flex-grow: 1; - margin-right: 20px; + display: flex; + flex-direction: column; + gap: 8px; } .section-image { @@ -49,7 +46,6 @@ } .subsection-list { - margin-bottom: var(--section-separation); overflow-x: auto; .article-list--tile { diff --git a/assets/scss/partials/layout/search.scss b/assets/scss/partials/layout/search.scss index b390a7b..89cdcef 100644 --- a/assets/scss/partials/layout/search.scss +++ b/assets/scss/partials/layout/search.scss @@ -1,5 +1,4 @@ .search-form { - margin-bottom: var(--section-separation); position: relative; --button-size: 80px; @@ -25,7 +24,7 @@ label { position: absolute; top: 15px; - left: 20px; + inset-inline-start: 20px; font-size: 1.4rem; color: var(--card-text-color-tertiary); } @@ -52,7 +51,7 @@ button { position: absolute; - right: 0; + inset-inline-end: 0; top: 0; height: 100%; width: var(--button-size); @@ -79,4 +78,5 @@ height: 20px; } } + } \ No newline at end of file diff --git a/assets/scss/partials/menu.scss b/assets/scss/partials/menu.scss index 2c7ad74..4f29c9f 100644 --- a/assets/scss/partials/menu.scss +++ b/assets/scss/partials/menu.scss @@ -101,11 +101,16 @@ background: none; border: none; position: absolute; - right: 30px; - top: 30px; + right: 0; + top: 0; z-index: 2; cursor: pointer; + [dir="rtl"] & { + left: 0; + right: auto; + } + @include respond(md) { display: none; } @@ -125,21 +130,31 @@ .menu { padding-left: 0; list-style: none; - display: flex; flex-direction: column; overflow-y: auto; flex-grow: 1; font-size: 1.4rem; - background-color: var(--card-background); - padding: 15px 0; + box-shadow: var(--shadow-l1); display: none; + margin: 0 calc(var(--container-padding) * -1); - margin: 0 -15px; + padding: 30px 30px; + @include respond(xl) { + padding: 15px 0; + } + + &, + .menu-bottom-section { + gap: 30px; + @include respond(xl) { + gap: 25px; + } + } &.show { - display: block; + display: flex; } @include respond(md) { @@ -149,34 +164,19 @@ padding: 0; box-shadow: none; margin: 0; - margin-top: var(--sidebar-element-separation); - } - - @include respond(xl) { - margin-top: 30px; } li { position: relative; vertical-align: middle; - padding: 10px 30px; - - &:not(:last-of-type) { - margin-bottom: 15px; - - @include respond(xl) { - margin-bottom: 20px; - } - } + padding: 0; @include respond(md) { width: 100%; - padding: 10px 0; } svg { stroke-width: 1.33; - margin-right: 40px; width: 20px; height: 20px; @@ -187,6 +187,7 @@ display: inline-flex; align-items: center; color: var(--body-text-color); + gap: var(--menu-icon-separation); } span { @@ -200,4 +201,27 @@ } } } + + .menu-bottom-section { + margin-top: auto; + display: flex; + flex-direction: column; + width: 100%; + } +} + +.social-menu { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + gap: 10px; + + svg { + width: 24px; + height: 24px; + stroke: var(--body-text-color); + stroke-width: 1.33; + } } diff --git a/assets/scss/partials/pagination.scss b/assets/scss/partials/pagination.scss index a6c6882..ca46780 100644 --- a/assets/scss/partials/pagination.scss +++ b/assets/scss/partials/pagination.scss @@ -5,7 +5,6 @@ border-radius: var(--card-border-radius); overflow: hidden; flex-wrap: wrap; - margin: var(--section-separation) 0; .page-link { padding: 16px 32px; diff --git a/assets/scss/partials/sidebar.scss b/assets/scss/partials/sidebar.scss index 462729d..95310ca 100644 --- a/assets/scss/partials/sidebar.scss +++ b/assets/scss/partials/sidebar.scss @@ -11,13 +11,15 @@ flex-direction: column; flex-shrink: 0; align-self: stretch; - - width: 100%; - padding: 30px 0 15px 0; + gap: var(--sidebar-element-separation); max-width: none; + width: 100%; + position: relative; - --sidebar-avatar-size: 120px; + --sidebar-avatar-size: 100px; --sidebar-element-separation: 20px; + --emoji-size: 40px; + --emoji-font-size: 20px; @include respond(md) { width: auto; @@ -27,18 +29,49 @@ } @include respond(2xl) { - --sidebar-avatar-size: 140px; + --sidebar-avatar-size: 120px; --sidebar-element-separation: 25px; + --emoji-size: 40px; } &.sticky { top: 0; } + + &.compact { + --sidebar-avatar-size: 80px; + --emoji-size: 30px; + --emoji-font-size: 15px; + + header { + @include respond(lg) { + flex-direction: row; + } + + .site-meta { + gap: 5px; + } + + .site-name { + font-size: 1.4rem; + + @include respond(2xl) { + font-size: 1.75rem; + } + } + + .site-description { + font-size: 1.4rem; + } + } + } } .right-sidebar { - flex-shrink: 0; + width: 100%; display: none; + flex-direction: column; + gap: var(--widget-separation); &.sticky { top: 0; @@ -49,11 +82,12 @@ } } -.site-info { +.sidebar header { z-index: 1; transition: box-shadow 0.5s ease; - - padding: 15px; + display: flex; + flex-direction: column; + gap: var(--sidebar-element-separation); @include respond(md) { padding: 0; @@ -64,8 +98,7 @@ margin: 0; width: var(--sidebar-avatar-size); height: var(--sidebar-avatar-size); - - margin-bottom: var(--sidebar-element-separation); + flex-shrink: 0; .site-logo { width: 100%; @@ -76,58 +109,44 @@ .emoji { position: absolute; - width: 40px; - height: 40px; - line-height: 40px; + width: var(--emoji-size); + height: var(--emoji-size); + line-height: var(--emoji-size); border-radius: 100%; bottom: 0; right: 0; text-align: center; - font-size: 20px; + font-size: var(--emoji-font-size); background-color: var(--card-background); box-shadow: var(--shadow-l2); - - @include respond(2xl) { - width: 50px; - height: 50px; - line-height: 50px; - } } } + .site-meta { + display: flex; + flex-direction: column; + gap: 10px; + justify-content: center; + } + .site-name { color: var(--accent-color); margin: 0; - font-size: 1.8rem; - - @include respond(2xl) { - font-size: 2rem; - } - } - - .site-description { - color: var(--body-text-color); - font-weight: normal; - margin: 10px 0; font-size: 1.6rem; @include respond(2xl) { font-size: 1.8rem; } } -} -.sidebar { - .widget { - margin-bottom: var(--section-separation); + .site-description { + color: var(--body-text-color); + font-weight: normal; + margin: 0; + font-size: 1.4rem; - &:not(:last-of-type):after { - content: ""; - width: 100px; - height: 2px; - background-color: var(--body-text-color); - display: block; - margin-top: var(--section-separation); + @include respond(2xl) { + font-size: 1.6rem; } } } @@ -153,8 +172,27 @@ display: flex; align-items: center; cursor: pointer; + gap: var(--menu-icon-separation); .icon-tabler-toggle-right { display: none; } } + +#i18n-switch { + color: var(--body-text-color); + display: inline-flex; + align-content: center; + gap: var(--menu-icon-separation); + + select { + border: 0; + background-color: transparent; + color: var(--body-text-color); + + option { + color: var(--card-text-color-main); + background-color: var(--card-background); + } + } +} diff --git a/assets/scss/partials/widgets.scss b/assets/scss/partials/widgets.scss index 33a02dd..42cfcc2 100644 --- a/assets/scss/partials/widgets.scss +++ b/assets/scss/partials/widgets.scss @@ -1,4 +1,7 @@ .widget { + display: flex; + flex-direction: column; + .widget-icon { svg { width: 32px; @@ -14,16 +17,14 @@ .tagCloud-tags { display: flex; flex-wrap: wrap; + gap: 10px; a { background: var(--card-background); box-shadow: var(--shadow-l1); border-radius: var(--tag-border-radius); padding: 8px 20px; - color: var(--card-text-color-main); - margin-bottom: 10px; - margin-right: 5px; font-size: 1.4rem; transition: box-shadow 0.3s ease; diff --git a/assets/scss/style.scss b/assets/scss/style.scss index 2f5aac9..43e95a5 100644 --- a/assets/scss/style.scss +++ b/assets/scss/style.scss @@ -19,7 +19,6 @@ @import "partials/pagination.scss"; @import "partials/sidebar.scss"; @import "partials/base.scss"; -@import "partials/layout/archives.scss"; @import "partials/layout/article.scss"; @import "partials/layout/list.scss"; @import "partials/layout/404.scss"; diff --git a/assets/scss/variables.scss b/assets/scss/variables.scss index c75eb5c..afddb80 100644 --- a/assets/scss/variables.scss +++ b/assets/scss/variables.scss @@ -1,25 +1,11 @@ $defaultTagBackgrounds: #8ea885, #df7988, #0177b8, #ffb900, #6b69d6; $defaultTagColors: #fff, #fff, #fff, #fff, #fff; -[data-scheme="light"] { - --pre-text-color: #272822; - --pre-background-color: #fafafa; - @import "partials/highlight/light.scss"; -} - -[data-scheme="dark"] { - --pre-text-color: #f8f8f2; - --pre-background-color: #272822; - @import "partials/highlight/dark.scss"; -} - /* * Global style */ :root { - @include respond(md) { - --main-top-padding: 35px; - } + --main-top-padding: 35px; @include respond(xl) { --main-top-padding: 50px; @@ -45,7 +31,7 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff; --accent-color-darker: #bdc3c7; --accent-color-text: #000; --body-text-color: rgba(255, 255, 255, 0.7); - --scrollbar-thumb: #424242; + --scrollbar-thumb: hsl(0, 0%, 40%); --scrollbar-track: var(--body-background); } } @@ -159,3 +145,21 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff; --shadow-l4: 0px 24px 32px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04); } + +[data-scheme="light"] { + --pre-text-color: #272822; + --pre-background-color: #fafafa; + @import "partials/highlight/light.scss"; +} + +[data-scheme="dark"] { + --pre-text-color: #f8f8f2; + --pre-background-color: #272822; + @import "partials/highlight/dark.scss"; +} + +:root { + --menu-icon-separation: 40px; + --container-padding: 15px; + --widget-separation: var(--section-separation); +} diff --git a/assets/ts/gallery.ts b/assets/ts/gallery.ts index 5de13a2..9840f1e 100644 --- a/assets/ts/gallery.ts +++ b/assets/ts/gallery.ts @@ -57,6 +57,60 @@ class StackGallery { } public static createGallery(container: HTMLElement) { + /// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook + /// because it can not detect whether image is being wrapped by a link or not + /// and it lead to a invalid HTML construction (
) + + const images = container.querySelectorAll('img.gallery-image'); + for (const img of Array.from(images)) { + /// Images are wrapped with figure tag if the paragraph has only images without texts + /// This is done to allow inline images within paragraphs + const paragraph = img.closest('p'); + + if (!paragraph || !container.contains(paragraph)) continue; + + if (paragraph.textContent.trim() == '') { + /// Once we insert figcaption, this check no longer works + /// So we add a class to paragraph to mark it + paragraph.classList.add('no-text'); + } + + let isNewLineImage = paragraph.classList.contains('no-text'); + if (!isNewLineImage) continue; + + const hasLink = img.parentElement.tagName == 'A'; + + let el: HTMLElement = img; + /// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes + const figure = document.createElement('figure'); + figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1'); + figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0'); + if (hasLink) { + /// Wrap if it exists + el = img.parentElement; + } + el.parentElement.insertBefore(figure, el); + figure.appendChild(el); + + /// Add figcaption if it exists + if (img.hasAttribute('alt')) { + const figcaption = document.createElement('figcaption'); + figcaption.innerText = img.getAttribute('alt'); + figure.appendChild(figcaption); + } + + /// Wrap img tag with tag if image was not wrapped by tag + if (!hasLink) { + figure.className = 'gallery-image'; + + const a = document.createElement('a'); + a.href = img.src; + a.setAttribute('target', '_blank'); + img.parentNode.insertBefore(a, img); + a.appendChild(img); + } + } + const figuresEl = container.querySelectorAll('figure.gallery-image'); let currentGallery = []; diff --git a/assets/ts/main.ts b/assets/ts/main.ts index d79c127..f3160ae 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -10,6 +10,8 @@ import { getColor } from 'ts/color'; import menu from 'ts/menu'; import createElement from 'ts/createElement'; import StackColorScheme from 'ts/colorScheme'; +import { setupScrollspy } from 'ts/scrollspy'; +import { setupSmoothAnchors } from "ts/smoothAnchors"; let Stack = { init: () => { @@ -21,6 +23,8 @@ let Stack = { const articleContent = document.querySelector('.article-content') as HTMLElement; if (articleContent) { new StackGallery(articleContent); + setupSmoothAnchors(); + setupScrollspy(); } /** @@ -58,20 +62,21 @@ let Stack = { /** * Add copy button to code block */ - const codeBlocks = document.querySelectorAll('.article-content .highlight'); + const highlights = document.querySelectorAll('.article-content div.highlight'); const copyText = `Copy`, copiedText = `Copied!`; - codeBlocks.forEach(codeBlock => { + + highlights.forEach(highlight => { const copyButton = document.createElement('button'); copyButton.innerHTML = copyText; copyButton.classList.add('copyCodeButton'); - codeBlock.appendChild(copyButton); + highlight.appendChild(copyButton); - const pre = codeBlock.getElementsByTagName('pre'); - const code = pre[0].textContent; + const codeBlock = highlight.querySelector('code[data-lang]'); + if (!codeBlock) return; copyButton.addEventListener('click', () => { - navigator.clipboard.writeText(code) + navigator.clipboard.writeText(codeBlock.textContent) .then(() => { copyButton.textContent = copiedText; diff --git a/assets/ts/scrollspy.ts b/assets/ts/scrollspy.ts new file mode 100644 index 0000000..8a14085 --- /dev/null +++ b/assets/ts/scrollspy.ts @@ -0,0 +1,131 @@ +// Implements a scroll spy system for the ToC, displaying the current section with an indicator and scrolling to it when needed. + +// Inspired from https://gomakethings.com/debouncing-your-javascript-events/ +function debounced(func: Function) { + let timeout; + return () => { + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + timeout = window.requestAnimationFrame(() => func()); + } +} + +const headersQuery = ".article-content h1[id], .article-content h2[id], .article-content h3[id], .article-content h4[id], .article-content h5[id], .article-content h6[id]"; +const tocQuery = "#TableOfContents"; +const navigationQuery = "#TableOfContents li"; +const activeClass = "active-class"; + +function scrollToTocElement(tocElement: HTMLElement, scrollableNavigation: HTMLElement) { + let textHeight = tocElement.querySelector("a").offsetHeight; + let scrollTop = tocElement.offsetTop - scrollableNavigation.offsetHeight / 2 + textHeight / 2 - scrollableNavigation.offsetTop; + if (scrollTop < 0) { + scrollTop = 0; + } + scrollableNavigation.scrollTo({ top: scrollTop, behavior: "smooth" }); +} + +type IdToElementMap = { [key: string]: HTMLElement }; + +function buildIdToNavigationElementMap(navigation: NodeListOf): IdToElementMap { + const sectionLinkRef: IdToElementMap = {}; + navigation.forEach((navigationElement: HTMLElement) => { + const link = navigationElement.querySelector("a"); + const href = link.getAttribute("href"); + if (href.startsWith("#")) { + sectionLinkRef[href.slice(1)] = navigationElement; + } + }); + + return sectionLinkRef; +} + +function computeOffsets(headers: NodeListOf) { + let sectionsOffsets = []; + headers.forEach((header: HTMLElement) => { sectionsOffsets.push({ id: header.id, offset: header.offsetTop }) }); + sectionsOffsets.sort((a, b) => a.offset - b.offset); + return sectionsOffsets; +} + +function setupScrollspy() { + let headers = document.querySelectorAll(headersQuery); + if (!headers) { + console.warn("No header matched query", headers); + return; + } + + let scrollableNavigation = document.querySelector(tocQuery) as HTMLElement | undefined; + if (!scrollableNavigation) { + console.warn("No toc matched query", tocQuery); + return; + } + + let navigation = document.querySelectorAll(navigationQuery); + if (!navigation) { + console.warn("No navigation matched query", navigationQuery); + return; + } + + let sectionsOffsets = computeOffsets(headers); + + // We need to avoid scrolling when the user is actively interacting with the ToC. Otherwise, if the user clicks on a link in the ToC, + // we would scroll their view, which is not optimal usability-wise. + let tocHovered: boolean = false; + scrollableNavigation.addEventListener("mouseenter", debounced(() => tocHovered = true)); + scrollableNavigation.addEventListener("mouseleave", debounced(() => tocHovered = false)); + + let activeSectionLink: Element; + + let idToNavigationElement: IdToElementMap = buildIdToNavigationElementMap(navigation); + + function scrollHandler() { + let scrollPosition = document.documentElement.scrollTop || document.body.scrollTop; + + let newActiveSection: HTMLElement | undefined; + + // Find the section that is currently active. + // It is possible for no section to be active, so newActiveSection may be undefined. + sectionsOffsets.forEach((section) => { + if (scrollPosition >= section.offset - 20) { + newActiveSection = document.getElementById(section.id); + } + }); + + // Find the link for the active section. Once again, there are a few edge cases: + // - No active section = no link => undefined + // - No active section but the link does not exist in toc (e.g. because it is outside of the applicable ToC levels) => undefined + let newActiveSectionLink: HTMLElement | undefined + if (newActiveSection) { + newActiveSectionLink = idToNavigationElement[newActiveSection.id]; + } + + if (newActiveSection && !newActiveSectionLink) { + // The active section does not have a link in the ToC, so we can't scroll to it. + console.debug("No link found for section", newActiveSection); + } else if (newActiveSectionLink !== activeSectionLink) { + if (activeSectionLink) + activeSectionLink.classList.remove(activeClass); + if (newActiveSectionLink) { + newActiveSectionLink.classList.add(activeClass); + if (!tocHovered) { + // Scroll so that newActiveSectionLink is in the middle of scrollableNavigation, except when it's from a manual click (hence the tocHovered check) + scrollToTocElement(newActiveSectionLink, scrollableNavigation); + } + } + activeSectionLink = newActiveSectionLink; + } + } + + window.addEventListener("scroll", debounced(scrollHandler)); + + // Resizing may cause the offset values to change: recompute them. + function resizeHandler() { + sectionsOffsets = computeOffsets(headers); + scrollHandler(); + } + + window.addEventListener("resize", debounced(resizeHandler)); +} + +export { setupScrollspy }; \ No newline at end of file diff --git a/assets/ts/search.tsx b/assets/ts/search.tsx index 8e4eb6f..68db7b3 100644 --- a/assets/ts/search.tsx +++ b/assets/ts/search.tsx @@ -8,6 +8,11 @@ interface pageData { matchCount: number } +interface match { + start: number, + end: number +} + /** * Escape HTML tags as HTML entities * Edited from: @@ -53,79 +58,131 @@ class Search { this.bindSearchForm(); } - private async searchKeywords(keywords: string[]) { - const rawData = await this.getData(); - let results: pageData[] = []; - - /// Sort keywords by their length - keywords.sort((a, b) => { - return b.length - a.length + /** + * Processes search matches + * @param str original text + * @param matches array of matches + * @param ellipsis whether to add ellipsis to the end of each match + * @param charLimit max length of preview string + * @param offset how many characters before and after the match to include in preview + * @returns preview string + */ + private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string { + matches.sort((a, b) => { + return a.start - b.start; }); + let i = 0, + lastIndex = 0, + charCount = 0; + + const resultArray: string[] = []; + + while (i < matches.length) { + const item = matches[i]; + + /// item.start >= lastIndex (equal only for the first iteration) + /// because of the while loop that comes after, iterating over variable j + + if (ellipsis && item.start - offset > lastIndex) { + resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `); + resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`); + charCount += offset * 2; + } + else { + /// If the match is too close to the end of last match, don't add ellipsis + resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start))); + charCount += item.start - lastIndex; + } + + let j = i + 1, + end = item.end; + + /// Include as many matches as possible + /// [item.start, end] is the range of the match + while (j < matches.length && matches[j].start <= end) { + end = Math.max(matches[j].end, end); + ++j; + } + + resultArray.push(`${replaceHTMLEnt(str.substring(item.start, end))}`); + charCount += end - item.start; + + i = j; + lastIndex = end; + + if (ellipsis && charCount > charLimit) break; + } + + /// Add the rest of the string + if (lastIndex < str.length) { + let end = str.length; + if (ellipsis) end = Math.min(end, lastIndex + offset); + + resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`); + + if (ellipsis && end != str.length) { + resultArray.push(` [...]`); + } + } + + return resultArray.join(''); + } + + private async searchKeywords(keywords: string[]) { + const rawData = await this.getData(); + const results: pageData[] = []; + + const regex = new RegExp(keywords.filter((v, index, arr) => { + arr[index] = escapeRegExp(v); + return v.trim() !== ''; + }).join('|'), 'gi'); + for (const item of rawData) { + const titleMatches: match[] = [], + contentMatches: match[] = []; + let result = { ...item, preview: '', matchCount: 0 } - let matched = false; - - for (const keyword of keywords) { - if (keyword === '') continue; - - const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi'); - - const contentMatch = regex.exec(result.content); - regex.lastIndex = 0; /// Reset regex - - const titleMatch = regex.exec(result.title); - regex.lastIndex = 0; /// Reset regex - - if (titleMatch) { - result.title = result.title.replace(regex, Search.marker); - } - - if (titleMatch || contentMatch) { - matched = true; - ++result.matchCount; - - let start = 0, - end = 100; - - if (contentMatch) { - start = contentMatch.index - 20; - end = contentMatch.index + 80 - - if (start < 0) start = 0; - } - - if (result.preview.indexOf(keyword) !== -1) { - result.preview = result.preview.replace(regex, Search.marker); - } - else { - if (start !== 0) result.preview += `[...] `; - result.preview += `${result.content.slice(start, end).replace(regex, Search.marker)} `; - } - } + const contentMatchAll = item.content.matchAll(regex); + for (const match of Array.from(contentMatchAll)) { + contentMatches.push({ + start: match.index, + end: match.index + match[0].length + }); } - if (matched) { - result.preview += '[...]'; - results.push(result); + const titleMatchAll = item.title.matchAll(regex); + for (const match of Array.from(titleMatchAll)) { + titleMatches.push({ + start: match.index, + end: match.index + match[0].length + }); } + + if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false); + if (contentMatches.length > 0) { + result.preview = Search.processMatches(result.content, contentMatches); + } + else { + /// If there are no matches in the content, use the first 140 characters as preview + result.preview = replaceHTMLEnt(result.content.substring(0, 140)); + } + + result.matchCount = titleMatches.length + contentMatches.length; + if (result.matchCount > 0) results.push(result); } - /** Result with more matches appears first */ + /// Result with more matches appears first return results.sort((a, b) => { return b.matchCount - a.matchCount; }); } - public static marker(match) { - return '' + match + ''; - } - private async doSearch(keywords: string[]) { const startTime = performance.now(); @@ -150,6 +207,11 @@ class Search { /// Not fetched yet const jsonURL = this.form.dataset.json; this.data = await fetch(jsonURL).then(res => res.json()); + const parser = new DOMParser(); + + for (const item of this.data) { + item.content = parser.parseFromString(item.content, 'text/html').body.innerText; + } } return this.data; @@ -160,7 +222,7 @@ class Search { const eventHandler = (e) => { e.preventDefault(); - const keywords = this.input.value; + const keywords = this.input.value.trim(); Search.updateQueryString(keywords, true); @@ -225,7 +287,7 @@ class Search {

- +
{item.image &&
diff --git a/assets/ts/smoothAnchors.ts b/assets/ts/smoothAnchors.ts new file mode 100644 index 0000000..0718bf5 --- /dev/null +++ b/assets/ts/smoothAnchors.ts @@ -0,0 +1,34 @@ +// Implements smooth scrolling when clicking on an anchor link. +// This is required instead of using modern CSS because Chromium does not currently support scrolling +// one element with scrollTo while another element is scrolled because of a click on a link. This would +// thus not work with the ToC scrollspy and e.g. footnotes. + +// Here are additional links about this issue: +// - https://stackoverflow.com/questions/49318497/google-chrome-simultaneously-smooth-scrollintoview-with-more-elements-doesn +// - https://stackoverflow.com/questions/57214373/scrollintoview-using-smooth-function-on-multiple-elements-in-chrome +// - https://bugs.chromium.org/p/chromium/issues/detail?id=833617 +// - https://bugs.chromium.org/p/chromium/issues/detail?id=1043933 +// - https://bugs.chromium.org/p/chromium/issues/detail?id=1121151 + +const anchorLinksQuery = "a[href]"; + +function setupSmoothAnchors() { + document.querySelectorAll(anchorLinksQuery).forEach(aElement => { + let href = aElement.getAttribute("href"); + if (!href.startsWith("#")) { + return; + } + aElement.addEventListener("click", clickEvent => { + clickEvent.preventDefault(); + + let targetId = aElement.getAttribute("href").substring(1); + // The replace done on ':' is here for footnotes, as this character would otherwise interfere when used as a CSS selector. + let target = document.querySelector(`#${targetId.replace(":", "\\:")}`) as HTMLElement; + + window.history.pushState({}, "", aElement.getAttribute("href")); + scrollTo({ top: target.offsetTop, behavior: "smooth" }); + }); + }); +} + +export { setupSmoothAnchors }; \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..93367bb --- /dev/null +++ b/config.yaml @@ -0,0 +1,141 @@ +module: + hugoVersion: + extended: true + min: "0.87.0" + +params: + mainSections: + - post + featuredImageField: image + rssFullContent: true + favicon: + + footer: + since: + customText: + + dateFormat: + published: Jan 02, 2006 + lastUpdated: Jan 02, 2006 15:04 MST + + sidebar: + compact: false + emoji: + subtitle: + avatar: + enabled: true + local: true + src: img/avatar.png + + article: + math: false + toc: true + readingTime: true + license: + enabled: false + default: Licensed under CC BY-NC-SA 4.0 + + comments: + enabled: false + provider: disqus + + disqusjs: + shortname: + apiUrl: + apiKey: + admin: + adminLabel: + + utterances: + repo: + issueTerm: pathname + label: + + remark42: + host: + site: + locale: + + vssue: + platform: + owner: + repo: + clientId: + clientSecret: + autoCreateIssue: false + + # Waline client configuration see: https://waline.js.org/en/reference/client.html + waline: + serverURL: + lang: + visitor: + avatar: + emoji: + - https://cdn.jsdelivr.net/gh/walinejs/emojis/weibo + requiredMeta: + - name + - email + - url + placeholder: + locale: + admin: Admin + + twikoo: + envId: + region: + path: + lang: + + giscus: + repo: + repoID: + category: + categoryID: + mapping: + lightTheme: + darkTheme: + reactionsEnabled: 1 + emitMetadata: 0 + lang: + + gitalk: + owner: + admin: + repo: + clientID: + clientSecret: + + cusdis: + host: + id: + + widgets: + homepage: [] + page: [] + + opengraph: + twitter: + # Your Twitter username + site: + + # Available values: summary, summary_large_image + card: summary_large_image + + defaultImage: + opengraph: + enabled: false + local: false + src: + + colorScheme: + # Display toggle + toggle: true + + # Available values: auto, light, dark + default: auto + + imageProcessing: + cover: + enabled: true + content: + enabled: true diff --git a/data/external.yaml b/data/external.yaml index 427c8aa..fd2b665 100644 --- a/data/external.yaml +++ b/data/external.yaml @@ -36,3 +36,11 @@ KaTeX: integrity: sha384-vZTG03m+2yp6N6BNi5iM4rW4oIwk5DfcNdFfxkk9ZWpDriOkXX8voJBFrAO7MpVl type: script defer: true + +Cactus: + - src: https://latest.cactus.chat/cactus.js + integrity: + type: script + - src: https://latest.cactus.chat/style.css + integrity: + type: style diff --git a/debug.sh b/debug.sh new file mode 100755 index 0000000..4d1312e --- /dev/null +++ b/debug.sh @@ -0,0 +1 @@ +cd exampleSite && hugo server --gc --themesDir=../.. \ No newline at end of file diff --git a/exampleSite/.gitignore b/exampleSite/.gitignore index ca4d540..6db5459 100644 --- a/exampleSite/.gitignore +++ b/exampleSite/.gitignore @@ -22,6 +22,9 @@ _testmain.go *.exe *.test -/public -/themes +public +themes +resources + +assets/jsconfig.json .DS_Store diff --git a/exampleSite/config.yaml b/exampleSite/config.yaml index 37e9f97..84585b4 100644 --- a/exampleSite/config.yaml +++ b/exampleSite/config.yaml @@ -4,6 +4,21 @@ theme: hugo-theme-stack paginate: 5 title: Example Site +languages: + en: + languageName: English + title: Example Site + weight: 1 + cn: + languageName: 中文 + title: 演示站点 + weight: 2 + ar: + languageName: عربي + languagedirection: rtl + title: موقع تجريبي + weight: 3 + # Change it to your Disqus shortname before using disqusShortname: hugo-theme-stack @@ -11,9 +26,13 @@ disqusShortname: hugo-theme-stack googleAnalytics: # Theme i18n support -# Available values: en, fr, id, ja, ko, pt-br, zh-cn, es, de, nl, it +# Available values: en, fr, id, ja, ko, pt-br, zh-cn, zh-tw, es, de, nl, it, th, el, uk, ar DefaultContentLanguage: en +# Set hasCJKLanguage to true if DefaultContentLanguage is in [zh-cn ja ko] +# This will make .Summary and .WordCount behave correctly for CJK languages. +hasCJKLanguage: false + permalinks: post: /p/:slug/ page: /:slug/ @@ -100,6 +119,12 @@ params: path: lang: + # See https://cactus.chat/docs/reference/web-client/#configuration for description of the various options + cactus: + defaultHomeserverUrl: "https://matrix.cactus.chat:8448" + serverName: "cactus.chat" + siteName: "" # You must insert a unique identifier here matching the one you registered (See https://cactus.chat/docs/getting-started/quick-start/#register-your-site) + giscus: repo: repoID: @@ -111,17 +136,30 @@ params: reactionsEnabled: 1 emitMetadata: 0 + gitalk: + owner: + admin: + repo: + clientID: + clientSecret: + + cusdis: + host: + id: widgets: - enabled: - - search - - archives - - tag-cloud - - archives: - limit: 5 - - tagCloud: - limit: 10 + homepage: + - type: search + - type: archives + params: + limit: 5 + - type: categories + params: + limit: 10 + - type: tag-cloud + params: + limit: 10 + page: + - type: toc opengraph: twitter: @@ -154,15 +192,20 @@ params: ### See https://docs.stack.jimmycai.com/configuration/custom-menu.html ### To remove about, archive and search page menu item, remove `menu` field from their FrontMatter menu: - main: - - identifier: home - name: Home - url: / - weight: -100 - pre: home + main: [] + + social: + - identifier: github + name: GitHub + url: https://github.com/CaiJimmy/hugo-theme-stack params: - ### For demonstration purpose, the home link will be open in a new tab - newTab: true + icon: brand-github + + - identifier: twitter + name: Twitter + url: https://twitter.com + params: + icon: brand-twitter related: includeNewer: true @@ -176,9 +219,19 @@ related: weight: 200 markup: + goldmark: + renderer: + ## Set to true if you have HTML content inside Markdown + unsafe: false tableOfContents: endLevel: 4 ordered: true startLevel: 2 highlight: noClasses: false + codeFences: true + guessSyntax: true + lineNoStart: 1 + lineNos: true + lineNumbersInTable: true + tabWidth: 4 diff --git a/exampleSite/content/_index.cn.md b/exampleSite/content/_index.cn.md new file mode 100644 index 0000000..e4b59a8 --- /dev/null +++ b/exampleSite/content/_index.cn.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: 主页 + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/exampleSite/content/_index.md b/exampleSite/content/_index.md new file mode 100644 index 0000000..530dfe8 --- /dev/null +++ b/exampleSite/content/_index.md @@ -0,0 +1,8 @@ +--- +menu: + main: + name: Home + weight: -100 + params: + icon: home +--- \ No newline at end of file diff --git a/exampleSite/content/page/about/index.cn.md b/exampleSite/content/page/about/index.cn.md new file mode 100644 index 0000000..060c006 --- /dev/null +++ b/exampleSite/content/page/about/index.cn.md @@ -0,0 +1,5 @@ +--- +title: 关于 +--- + +This is a test page for i18n support. \ No newline at end of file diff --git a/exampleSite/content/page/about.md b/exampleSite/content/page/about/index.md similarity index 97% rename from exampleSite/content/page/about.md rename to exampleSite/content/page/about/index.md index 79898de..494cb46 100644 --- a/exampleSite/content/page/about.md +++ b/exampleSite/content/page/about/index.md @@ -11,7 +11,8 @@ lastmod: '2020-10-09' menu: main: weight: -90 - pre: user + params: + icon: user --- Written in Go, Hugo is an open source static site generator available under the [Apache Licence 2.0.](https://github.com/gohugoio/hugo/blob/master/LICENSE) Hugo supports TOML, YAML and JSON data file types, Markdown and HTML content files and uses shortcodes to add rich content. Other notable features are taxonomies, multilingual mode, image processing, custom output formats, HTML/CSS/JS minification and support for Sass SCSS workflows. diff --git a/exampleSite/content/page/archives.md b/exampleSite/content/page/archives/index.md similarity index 72% rename from exampleSite/content/page/archives.md rename to exampleSite/content/page/archives/index.md index 027dfc6..2862deb 100644 --- a/exampleSite/content/page/archives.md +++ b/exampleSite/content/page/archives/index.md @@ -6,5 +6,6 @@ slug: "archives" menu: main: weight: -70 - pre: archives + params: + icon: archives --- diff --git a/exampleSite/content/page/links/index.md b/exampleSite/content/page/links/index.md new file mode 100644 index 0000000..9e29bf3 --- /dev/null +++ b/exampleSite/content/page/links/index.md @@ -0,0 +1,37 @@ +--- +title: Links +links: + - title: GitHub + description: GitHub is the world's largest software development platform. + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +menu: + main: + weight: -50 + params: + icon: link + +comments: false +--- + +To use this feature, add `links` section to frontmatter. + +This page's frontmatter: + +```yaml +links: + - title: GitHub + description: GitHub is the world's largest software development platform. + website: https://github.com + image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png + - title: TypeScript + description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. + website: https://www.typescriptlang.org + image: ts-logo-128.jpg +``` + +`image` field accepts both local and external images. \ No newline at end of file diff --git a/exampleSite/content/page/links/ts-logo-128.jpg b/exampleSite/content/page/links/ts-logo-128.jpg new file mode 100644 index 0000000..85e3323 Binary files /dev/null and b/exampleSite/content/page/links/ts-logo-128.jpg differ diff --git a/exampleSite/content/page/search.md b/exampleSite/content/page/search/index.md similarity index 74% rename from exampleSite/content/page/search.md rename to exampleSite/content/page/search/index.md index f54cdb5..b2a5943 100644 --- a/exampleSite/content/page/search.md +++ b/exampleSite/content/page/search/index.md @@ -8,5 +8,6 @@ outputs: menu: main: weight: -60 - pre: search + params: + icon: search --- \ No newline at end of file diff --git a/exampleSite/content/post/chinese-test/index.md b/exampleSite/content/post/chinese-test/index.cn.md similarity index 100% rename from exampleSite/content/post/chinese-test/index.md rename to exampleSite/content/post/chinese-test/index.cn.md diff --git a/exampleSite/content/post/markdown-syntax/index.md b/exampleSite/content/post/markdown-syntax/index.md index 8c53bc2..0254cca 100644 --- a/exampleSite/content/post/markdown-syntax/index.md +++ b/exampleSite/content/post/markdown-syntax/index.md @@ -69,6 +69,10 @@ Tables aren't part of the core Markdown spec, but Hugo supports supports them ou | -------- | -------- | ------ | | *italics* | **bold** | `code` | +| A | B | C | D | E | F | +|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------| +| Lorem ipsum dolor sit amet, consectetur adipiscing elit. | Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. | Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus | Proin sit amet velit nec enim imperdiet vehicula. | Ut bibendum vestibulum quam, eu egestas turpis gravida nec | Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien | + ## Code Blocks #### Code block with backticks @@ -113,6 +117,16 @@ Tables aren't part of the core Markdown spec, but Hugo supports supports them ou {{< /highlight >}} +#### Diff code block + +```diff +[dependencies.bevy] +git = "https://github.com/bevyengine/bevy" +rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13" +- features = ["dynamic"] ++ features = ["jpeg", "dynamic"] +``` + ## List Types #### Ordered List @@ -148,3 +162,7 @@ Xn + Yn = Zn Press CTRL+ALT+Delete to end the session. Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures. + +## Hyperlinked image + +[![Google](https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png)](https://google.com) \ No newline at end of file diff --git a/exampleSite/content/post/placeholder-text/index.ar.md b/exampleSite/content/post/placeholder-text/index.ar.md new file mode 100644 index 0000000..b0328f4 --- /dev/null +++ b/exampleSite/content/post/placeholder-text/index.ar.md @@ -0,0 +1,32 @@ ++++ +author = "Hugo Authors" +title = "مثال نص" +date = "2019-03-09" +description = "هذا النص هو مثال لنص يمكن أن يستبدل في نفس المساحة" +categories = [ + "تجربة", + "تجربة مع فراغات" +] +tags = [ + "ماركداون", + "نص", + "وسم مع فراغات" +] +image = "matt-le-SJSpo9hQf7s-unsplash.jpg" ++++ +## فقرة 1 + +هذا النص هو مثال لنص يمكن أن يستبدل في نفس المساحة، لقد تم توليد هذا النص من [مولد النص العربى](https://colorslab.com/textgator/)، حيث يمكنك أن تولد مثل هذا النص أو العديد من النصوص الأخرى إضافة إلى زيادة عدد الحروف التى يولدها التطبيق. +إذا كنت تحتاج إلى عدد أكبر من الفقرات يتيح لك مولد النص العربى زيادة عدد الفقرات كما تريد، النص لن يبدو مقسما ولا يحوي أخطاء لغوية، مولد النص العربى مفيد لمصممي المواقع على وجه الخصوص، حيث يحتاج العميل فى كثير من الأحيان أن يطلع على صورة حقيقية لتصميم الموقع. +ومن هنا وجب على المصمم أن يضع نصوصا مؤقتة على التصميم ليظهر للعميل الشكل كاملاً،دور مولد النص العربى أن يوفر على المصمم عناء البحث عن نص بديل لا علاقة له بالموضوع الذى يتحدث عنه التصميم فيظهر بشكل لا يليق. +هذا النص يمكن أن يتم تركيبه على أي تصميم دون مشكلة فلن يبدو وكأنه نص منسوخ، غير منظم، غير منسق، أو حتى غير مفهوم. لأنه مازال نصاً بديلاً ومؤقتاً. + +## فقرة 2 + +هذا النص هو مثال لنص يمكن أن يستبدل في نفس المساحة، لقد تم توليد هذا النص من [مولد النص العربى](https://colorslab.com/textgator/)، حيث يمكنك أن تولد مثل هذا النص أو العديد من النصوص الأخرى إضافة إلى زيادة عدد الحروف التى يولدها التطبيق. +إذا كنت تحتاج إلى عدد أكبر من الفقرات يتيح لك مولد النص العربى زيادة عدد الفقرات كما تريد، النص لن يبدو مقسما ولا يحوي أخطاء لغوية، مولد النص العربى مفيد لمصممي المواقع على وجه الخصوص، حيث يحتاج العميل فى كثير من الأحيان أن يطلع على صورة حقيقية لتصميم الموقع. +ومن هنا وجب على المصمم أن يضع نصوصا مؤقتة على التصميم ليظهر للعميل الشكل كاملاً،دور مولد النص العربى أن يوفر على المصمم عناء البحث عن نص بديل لا علاقة له بالموضوع الذى يتحدث عنه التصميم فيظهر بشكل لا يليق. +هذا النص يمكن أن يتم تركيبه على أي تصميم دون مشكلة فلن يبدو وكأنه نص منسوخ، غير منظم، غير منسق، أو حتى غير مفهوم. لأنه مازال نصاً بديلاً ومؤقتاً. + +## تجربة RTL +كلمة 1 Text كلمة 2 diff --git a/exampleSite/content/post/rich-content/index.md b/exampleSite/content/post/rich-content/index.md index 407c539..f2b45db 100644 --- a/exampleSite/content/post/rich-content/index.md +++ b/exampleSite/content/post/rich-content/index.md @@ -32,3 +32,11 @@ Hugo ships with several [Built-in Shortcodes](https://gohugo.io/content-manageme ## Vimeo Simple Shortcode {{< vimeo_simple 48912912 >}} + +## bilibilibi Shortcode + +{{< bilibili av498363026 >}} + +## Gist Shortcode + +{{< gist spf13 7896402 >}} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..63df635 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/CaiJimmy/hugo-theme-stack/v3 + +go 1.17 diff --git a/i18n/ar.yaml b/i18n/ar.yaml new file mode 100644 index 0000000..dab3c74 --- /dev/null +++ b/i18n/ar.yaml @@ -0,0 +1,70 @@ +toggleMenu: + other: اخفي القائمة + +darkMode: + other: الوضع الداكن + +list: + page: + one: "{{ .Count }} صفحه" + other: "{{ .Count }} صفحات" + + section: + other: قسم + + subsection: + one: قسم فرعي + other: اقسام فرعية + +article: + back: + other: خلف + + tableOfContents: + other: جدول المحتويات + + relatedContents: + other: محتوى مشابهه + + lastUpdatedOn: + other: التعديل الاخير + + readingTime: + one: "قرائة {{ .Count }} دقيقة" + other: "قرائة {{ .Count }} دقائق" + +notFound: + title: + other: غير موجود + + subtitle: + other: تعذر العثور على الصفحة المطلوبة. + +widget: + archives: + title: + other: الارشيفات + + more: + other: اكثر + + tagCloud: + title: + other: وسوم + +search: + title: + other: بحث + + placeholder: + other: اكتب... + + resultTitle: + other: "#PAGES_COUNT نتيجة (#TIME_SECONDS ثواني)" + +footer: + builtWith: + other: "مبني بستخدام {{ .Generator }}" + + designedBy: + other: "قالب {{ .Theme }} مصمم من {{ .DesignedBy }}" diff --git a/i18n/el.yaml b/i18n/el.yaml new file mode 100644 index 0000000..38d867f --- /dev/null +++ b/i18n/el.yaml @@ -0,0 +1,70 @@ +toggleMenu: + other: Εναλλαγή Μενού + +darkMode: + other: Σκοτεινό θέμα + +list: + page: + one: "{{ .Count }} σελιδα" + other: "{{ .Count }} σελιδες" + + section: + other: Ενότητα + + subsection: + one: Υποενότητα + other: Υποενότητες + +article: + back: + other: Πισω + + tableOfContents: + other: Πινακας περιεχομενων + + relatedContents: + other: Σχετικο περιεχομενο + + lastUpdatedOn: + other: Τελευταια τροποποιηση στις + + readingTime: + one: "{{ .Count }} λεπτό ανάγνωσης" + ### Seems that there's no need to add 's' even if it's plural in English + other: "{{ .Count }} λεπτά ανάγνωσης" + +notFound: + title: + other: Δε βρέθηκε + subtitle: + other: Η σελίδα δε βρέθηκε. + +widget: + archives: + title: + other: Αρχειο + + more: + other: Περισσότερα + + tagCloud: + title: + other: Tags + +search: + title: + other: Αναζήτηση + + placeholder: + other: Πληκτρολογήστε κάτι... + + resultTitle: + other: "#PAGES_COUNT σελιδες (#TIME_SECONDS δευτερολεπτα)" + +footer: + builtWith: + other: Δημιουργήθηκε με τη χρήση {{ .Generator }} + + designedBy: + other: Το θέμα {{ .Theme }} σχεδιάστηκε από το {{ .DesignedBy }} diff --git a/i18n/en.yaml b/i18n/en.yaml index 0197d89..3d0ed03 100644 --- a/i18n/en.yaml +++ b/i18n/en.yaml @@ -51,6 +51,9 @@ widget: tagCloud: title: other: Tags + categoriesCloud: + title: + other: Categories search: title: diff --git a/i18n/fr.yaml b/i18n/fr.yaml index 9217ea3..ac2e51a 100644 --- a/i18n/fr.yaml +++ b/i18n/fr.yaml @@ -29,6 +29,10 @@ article: lastUpdatedOn: other: Dernière mise à jour le + readingTime: + one: "{{ .Count }} minute de lecture" + other: "{{ .Count }} minutes de lecture" + notFound: title: other: Page non trouvée diff --git a/i18n/ja.yaml b/i18n/ja.yaml index 3f9978d..ec70d18 100644 --- a/i18n/ja.yaml +++ b/i18n/ja.yaml @@ -38,6 +38,10 @@ widget: title: other: タグ + categoriesCloud: + title: + other: カテゴリ + search: title: other: 検索 @@ -47,3 +51,10 @@ search: resultTitle: other: "#PAGES_COUNT 件 (#TIME_SECONDS 秒)" + +footer: + builtWith: + other: Built with {{ .Generator }} + + designedBy: + other: テーマ {{ .Theme }} は {{ .DesignedBy }} によって設計されています。 diff --git a/i18n/ko.yaml b/i18n/ko.yaml index d9916cd..b2202e6 100644 --- a/i18n/ko.yaml +++ b/i18n/ko.yaml @@ -1,15 +1,42 @@ toggleMenu: other: 메뉴 여닫기 +darkMode: + other: 다크 모드 + +list: + page: + one: "{{ .Count }} 페이지" + other: "{{ .Count }} 페이지" + + section: + other: 섹션 + + subsection: + one: 서브섹션 + other: 서브섹션 + article: + back: + other: 뒤로가기 + + tableOfContents: + other: 목차 + relatedContents: other: 관련 글 + lastUpdatedOn: other: "마지막 수정: " + + readingTime: + one: "{{ .Count }} 분 정도" + other: "{{ .Count }} 분 정도" notFound: title: other: 찾을 수 없음 + subtitle: other: 페이지를 찾을 수 없습니다. @@ -19,6 +46,7 @@ widget: other: 보관함 more: other: 더보기 + tagCloud: title: other: 태그 @@ -26,8 +54,10 @@ widget: search: title: other: 검색 + placeholder: other: 검색어를 입력하세요... + resultTitle: other: "#PAGES_COUNT 페이지 (#TIME_SECONDS 초)" diff --git a/i18n/pt-BR.yaml b/i18n/pt-BR.yaml index 8240755..ffdbe41 100644 --- a/i18n/pt-BR.yaml +++ b/i18n/pt-BR.yaml @@ -17,11 +17,22 @@ list: other: Subseções article: + back: + other: Voltar + + tableOfContents: + other: Índice + relatedContents: - other: Conteúdos Relacionados + other: Conteúdo relacionado + lastUpdatedOn: other: Última atualização em + readingTime: + one: "{{ .Count }} minuto de leitura" + other: "{{ .Count }} minutos de leitura" + notFound: title: other: Não Encontrado diff --git a/i18n/th.yaml b/i18n/th.yaml new file mode 100644 index 0000000..b2ae1cd --- /dev/null +++ b/i18n/th.yaml @@ -0,0 +1,70 @@ +toggleMenu: + other: สลับเมนู + +darkMode: + other: ธีมมืด + +list: + page: + one: "{{ .Count }} หน้า" + other: "{{ .Count }} หน้า" + + section: + other: หมวดหมู่ + + subsection: + one: หมวดหมู่ย่อย + other: หมวดหมู่ย่อยอื่นๆ + +article: + back: + other: กลับไป + + tableOfContents: + other: สารบัญ + + relatedContents: + other: เนื้อหาคล้ายคลึงกัน + + lastUpdatedOn: + other: อัปเดตล่าสุดเมื่อ + + readingTime: + one: "น่าจะใช้เวลา {{ .Count }} นาทีในการอ่าน" + other: "น่าจะใช้เวลา {{ .Count }} นาทีในการอ่าน" + +notFound: + title: + other: ไม่พบหัวข้อ + + subtitle: + other: ไม่พบหน้านี้ในระบบ + +widget: + archives: + title: + other: เนื้อหาที่เก็บถาวรแล้ว + + more: + other: อื่นๆ นอกจากนี้ + + tagCloud: + title: + other: แท็ก + +search: + title: + other: ค้นหา + + placeholder: + other: พิมพ์เพื่อค้นหา ... + + resultTitle: + other: "#PAGES_COUNT pages (#TIME_SECONDS seconds)" + +footer: + builtWith: + other: ถูกสร้างด้วย {{ .Generator }} + + designedBy: + other: ธีม {{ .Theme }} ออกแบบโดย {{ .DesignedBy }} diff --git a/i18n/uk.yaml b/i18n/uk.yaml new file mode 100644 index 0000000..966e141 --- /dev/null +++ b/i18n/uk.yaml @@ -0,0 +1,71 @@ +toggleMenu: + other: Показати меню + +darkMode: + other: Темна тема + +list: + page: + one: "{{ .Count }} сторінка" + few: "{{ .Count }} сторінки" + other: "{{ .Count }} сторінок" + + section: + other: Секція + + subsection: + one: Підсекція + other: Підсекції + +article: + back: + other: Назад + + tableOfContents: + other: Зміст + + relatedContents: + other: Схожі матеріали + + lastUpdatedOn: + other: Востаннє оновлено + + readingTime: + one: "Час читання: {{ .Count }} хв" + other: "Час читання: {{ .Count }} хв" + +notFound: + title: + other: Не знайдено + + subtitle: + other: Ця сторінка не існує + +widget: + archives: + title: + other: Архіви + + more: + other: Більше + + tagCloud: + title: + other: Теґи + +search: + title: + other: Пошук + + placeholder: + other: Напишіть що-небудь... + + resultTitle: + other: "#PAGES_COUNT сторінок (#TIME_SECONDS секунд)" + +footer: + builtWith: + other: Створено з {{ .Generator }} + + designedBy: + other: Тема {{ .Theme }}, дизайн {{ .DesignedBy }} diff --git a/i18n/zh-CN.yaml b/i18n/zh-CN.yaml index 0f08310..3579945 100644 --- a/i18n/zh-CN.yaml +++ b/i18n/zh-CN.yaml @@ -38,6 +38,10 @@ widget: title: other: 标签云 + categoriesCloud: + title: + other: 分类 + search: title: other: 搜索 @@ -47,3 +51,10 @@ search: resultTitle: other: "#PAGES_COUNT 个结果 (用时 #TIME_SECONDS 秒)" + +footer: + builtWith: + other: Built with {{ .Generator }} + + designedBy: + other: 主题 {{ .Theme }} 由 {{ .DesignedBy }} 设计 diff --git a/i18n/zh-TW.yaml b/i18n/zh-TW.yaml new file mode 100644 index 0000000..871d937 --- /dev/null +++ b/i18n/zh-TW.yaml @@ -0,0 +1,49 @@ +toggleMenu: + other: 切換選單 + +darkMode: + other: 夜晚模式 + +article: + back: + other: 返回 + + tableOfContents: + other: 目錄 + + relatedContents: + other: 相關文章 + + lastUpdatedOn: + other: 最後更新 + + readingTime: + other: "閱讀時間: {{ .Count }} 分鐘" + +notFound: + title: + other: 404 錯誤 + subtitle: + other: 頁面不存在 + +widget: + archives: + title: + other: 紀錄 + + more: + other: 更多 + + tagCloud: + title: + other: 標籤雲 + +search: + title: + other: 搜尋 + + placeholder: + other: 輸入關鍵字... + + resultTitle: + other: "#PAGES_COUNT 個結果 (用時 #TIME_SECONDS 秒)" diff --git a/layouts/_default/_markup/render-image.html b/layouts/_default/_markup/render-image.html index 0325458..0ed5584 100644 --- a/layouts/_default/_markup/render-image.html +++ b/layouts/_default/_markup/render-image.html @@ -25,22 +25,17 @@ {{- end -}} {{- end -}} - \ No newline at end of file +> \ No newline at end of file diff --git a/layouts/_default/archives.html b/layouts/_default/archives.html index 321aa6d..5d5243c 100644 --- a/layouts/_default/archives.html +++ b/layouts/_default/archives.html @@ -1,17 +1,19 @@ {{ define "body-class" }}template-archives{{ end }} {{ define "main" }} - {{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}} - {{- $terms := $taxonomy.Pages -}} - {{ if $terms }} -

{{ $taxonomy.Title }}

-
-
- {{ range $terms }} - {{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }} - {{ end }} +
+ {{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}} + {{- $terms := $taxonomy.Pages -}} + {{ if $terms }} +

{{ $taxonomy.Title }}

+
+
+ {{ range $terms }} + {{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }} + {{ end }} +
-
- {{ end }} + {{ end }} + {{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }} {{ $notHidden := where .Site.RegularPages "Params.hidden" "!=" true }} diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 8a5ff95..081277b 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -1,12 +1,20 @@ - + {{- partial "head/head.html" . -}} {{- block "head" . -}}{{ end }} {{- partial "head/colorScheme" . -}} -
+ + {{/* The container is wider when there's any activated widget */}} + {{- $hasWidget := false -}} + {{- range .Site.Params.widgets -}} + {{- if gt (len .) 0 -}} + {{- $hasWidget = true -}} + {{- end -}} + {{- end -}} +
{{- block "left-sidebar" . -}} {{ partial "sidebar/left.html" . }} {{- end -}} diff --git a/layouts/_default/list.html b/layouts/_default/list.html index 90e5b66..9bc618d 100644 --- a/layouts/_default/list.html +++ b/layouts/_default/list.html @@ -1,46 +1,48 @@ {{ define "main" }} -

- {{ if eq .Parent (.GetPage "/") }} - {{ T "list.section" }} - {{ else }} - {{ .Parent.Title }} - {{ end }} -

- -
-
-

{{ T "list.page" (len .Pages) }}

-

{{ .Title }}

- {{ with .Params.description }} -

{{ . }}

+
+

+ {{ if eq .Parent (.GetPage "/") }} + {{ T "list.section" }} + {{ else }} + {{ .Parent.Title }} {{ end }} -

+ - {{- $image := partialCached "helper/image" (dict "Context" . "Type" "section") .RelPermalink "section" -}} - {{ if $image.exists }} -
- {{ if $image.resource }} - {{- $Permalink := $image.resource.RelPermalink -}} - {{- $Width := $image.resource.Width -}} - {{- $Height := $image.resource.Height -}} - - {{- if (default true .Page.Site.Params.imageProcessing.cover.enabled) -}} - {{- $thumbnail := $image.resource.Fill "120x120" -}} - {{- $Permalink = $thumbnail.RelPermalink -}} - {{- $Width = $thumbnail.Width -}} - {{- $Height = $thumbnail.Height -}} - {{- end -}} - - - {{ else }} - +
+
+

{{ T "list.page" (len .Pages) }}

+

{{ .Title }}

+ {{ with .Params.description }} +

{{ . }}

{{ end }}
- {{ end }} -
+ + {{- $image := partialCached "helper/image" (dict "Context" . "Type" "section") .RelPermalink "section" -}} + {{ if $image.exists }} +
+ {{ if $image.resource }} + {{- $Permalink := $image.resource.RelPermalink -}} + {{- $Width := $image.resource.Width -}} + {{- $Height := $image.resource.Height -}} + + {{- if (default true .Page.Site.Params.imageProcessing.cover.enabled) -}} + {{- $thumbnail := $image.resource.Fill "120x120" -}} + {{- $Permalink = $thumbnail.RelPermalink -}} + {{- $Width = $thumbnail.Width -}} + {{- $Height = $thumbnail.Height -}} + {{- end -}} + + + {{ else }} + + {{ end }} +
+ {{ end }} +
+ {{- $subsections := .Sections -}} {{- $pages := .Pages | complement $subsections -}} @@ -53,14 +55,16 @@ {{- end -}} {{- with $subsections -}} -

{{ T "list.subsection" (len $subsections) }}

-
-
- {{ range . }} - {{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "section") }} - {{ end }} +
+ {{- end -}} {{/* List only pages that are not a subsection */}} @@ -77,5 +81,5 @@ {{ end }} {{ define "right-sidebar" }} - {{ partialCached "sidebar/right.html" . }} + {{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }} {{ end }} \ No newline at end of file diff --git a/layouts/rss.xml b/layouts/_default/rss.xml similarity index 86% rename from layouts/rss.xml rename to layouts/_default/rss.xml index 178f997..e9f3f1a 100644 --- a/layouts/rss.xml +++ b/layouts/_default/rss.xml @@ -1,9 +1,15 @@ -{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}} -{{- $notHidden := where .Site.RegularPages "Params.hidden" "!=" true -}} -{{- $filtered := ($pages | intersect $notHidden) -}} +{{- $pctx := . -}} +{{- if .IsHome -}}{{ $pctx = .Site }}{{- end -}} +{{- $pages := slice -}} +{{- if or $.IsHome $.IsSection -}} +{{- $pages = $pctx.RegularPages -}} +{{- else -}} +{{- $pages = $pctx.Pages -}} +{{- end -}} +{{- $pages := where $pages "Params.hidden" "!=" true -}} {{- $limit := .Site.Config.Services.RSS.Limit -}} {{- if ge $limit 1 -}} -{{- $filtered = $filtered | first $limit -}} +{{- $pages = $pages | first $limit -}} {{- end -}} {{- printf "" | safeHTML }} @@ -20,7 +26,7 @@ {{- with .OutputFormats.Get "RSS" -}} {{ printf "" .Permalink .MediaType | safeHTML }} {{- end -}} - {{ range $filtered }} + {{ range $pages }} {{- $content := safeHTML (.Summary | html) -}} {{- if .Site.Params.rssFullContent -}} {{- $content = safeHTML (.Content | html) -}} diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 84169e7..c70df20 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -1,23 +1,38 @@ {{ define "body-class" }} - {{ $TOCEnabled := default (default false .Site.Params.article.toc) .Params.toc }} - {{- .Scratch.Set "hasTOC" (and (ge (len .TableOfContents) 100) $TOCEnabled) -}} - article-page {{ if (.Scratch.Get "hasTOC") }}has-toc{{ end }} -{{ end }} + article-page + {{/* + Enable the right sidebar if + - Widget different from 'TOC' is enabled + - TOC is enabled and not empty + */}} + {{- $HasWidgetNotTOC := false -}} + {{- $TOCWidgetEnabled := false -}} + {{- range .Site.Params.widgets.page -}} + {{- if ne .type "toc" -}} + {{ $HasWidgetNotTOC = true -}} + {{- else -}} + {{ $TOCWidgetEnabled = true -}} + {{- end -}} + {{- end -}} -{{ define "container-class" }} - {{ if (.Scratch.Get "hasTOC") }} - extended - {{ else }} - on-phone--column {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }} - {{ end }} + {{- $TOCManuallyDisabled := eq .Params.toc false -}} + {{- $TOCEnabled := and (not $TOCManuallyDisabled) $TOCWidgetEnabled -}} + {{- $hasTOC := ge (len .TableOfContents) 100 -}} + {{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}} + + {{- .Scratch.Set "hasWidget" (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}} {{ end }} {{ define "main" }} {{ partial "article/article.html" . }} + {{ if .Params.links }} + {{ partial "article/components/links" . }} + {{ end }} + {{ partial "article/components/related-contents" . }} - {{ if or (not (isset .Params "comments")) (eq .Params.comments "true")}} + {{ if not (eq .Params.comments false) }} {{ partial "comments/include" . }} {{ end }} @@ -26,34 +41,6 @@ {{ partialCached "article/components/photoswipe" . }} {{ end }} -{{ define "left-sidebar" }} - {{ if (.Scratch.Get "hasTOC") }} -
- - {{ (resources.Get "icons/back.svg").Content | safeHTML }} - {{ T "article.back" }} - -
- {{ else }} - {{ partial "sidebar/left.html" . }} - {{ end }} -{{ end }} - {{ define "right-sidebar" }} - {{ if (.Scratch.Get "hasTOC") }} - - {{ end }} -{{ end }} \ No newline at end of file + {{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}} +{{ end }} diff --git a/layouts/index.html b/layouts/index.html index 576faef..0cd0b88 100644 --- a/layouts/index.html +++ b/layouts/index.html @@ -15,5 +15,5 @@ {{ end }} {{ define "right-sidebar" }} - {{ partial "sidebar/right.html" . }} + {{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }} {{ end }} \ No newline at end of file diff --git a/layouts/page/search.html b/layouts/page/search.html index 935b384..7c424a6 100644 --- a/layouts/page/search.html +++ b/layouts/page/search.html @@ -16,8 +16,10 @@ -

-
+
+

+
+
+{{- end -}} diff --git a/layouts/partials/comments/provider/cusdis.html b/layouts/partials/comments/provider/cusdis.html new file mode 100644 index 0000000..a89746c --- /dev/null +++ b/layouts/partials/comments/provider/cusdis.html @@ -0,0 +1,21 @@ +{{- $host := default "https://cusdis.com" .Site.Params.comments.cusdis.host -}} +
+ + + diff --git a/layouts/partials/comments/provider/giscus.html b/layouts/partials/comments/provider/giscus.html index f36ead9..abe345f 100644 --- a/layouts/partials/comments/provider/giscus.html +++ b/layouts/partials/comments/provider/giscus.html @@ -9,41 +9,41 @@ data-reactions-enabled="{{- default 1 .reactionsEnabled -}}" data-emit-metadata="{{- default 0 .emitMetadata -}}" data-theme="{{- default `light` .lightTheme -}}" + data-lang="{{- default `en` .lang -}}" crossorigin="anonymous" async > {{- end -}} - diff --git a/layouts/partials/comments/provider/gitalk.html b/layouts/partials/comments/provider/gitalk.html new file mode 100644 index 0000000..95ca133 --- /dev/null +++ b/layouts/partials/comments/provider/gitalk.html @@ -0,0 +1,30 @@ +{{- with .Site.Params.comments.gitalk -}} +
+ + + + +{{ end }} diff --git a/layouts/partials/comments/provider/twikoo.html b/layouts/partials/comments/provider/twikoo.html index 4dbf976..7dd3e50 100644 --- a/layouts/partials/comments/provider/twikoo.html +++ b/layouts/partials/comments/provider/twikoo.html @@ -1,4 +1,4 @@ - +