Merge branch 'master' of https://github.com/CaiJimmy/hugo-theme-stack into upstream-3.11

This commit is contained in:
nk9 2022-04-24 17:49:12 +01:00
commit 4f9788a9a0
72 changed files with 1351 additions and 606 deletions

3
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
ko_fi: jimmycai
ko_fi: jimmycai
github: CaiJimmy

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
public
resources
assets/jsconfig.json
.hugo_build.lock

View File

@ -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 `themes` 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) |
| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) |

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-hash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="5" y1="9" x2="19" y2="9" />
<line x1="5" y1="15" x2="19" y2="15" />
<line x1="11" y1="4" x2="7" y2="20" />
<line x1="17" y1="4" x2="13" y2="20" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

10
assets/icons/language.svg Normal file
View File

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-language" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 5h7" />
<path d="M9 3v2c0 4.418 -2.239 8 -5 8" />
<path d="M5 9c-.003 2.144 2.952 3.908 6.7 4" />
<path d="M12 20l4 -9l4 9" />
<path d="M19.1 18h-6.2" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@ -6,5 +6,7 @@
"*"
]
},
"lib": ["es2020", "dom"],
"jsx": "preserve"
}
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}
/**/
/**/

View File

@ -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: "";

View File

@ -1,3 +0,0 @@
.archives-group {
margin-bottom: var(--section-separation);
}

View File

@ -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);
}
@ -80,110 +77,6 @@
}
}
#article-toolbar {
display: flex;
align-items: center;
margin: 20px 0;
@include respond(md) {
display: none;
}
.back-home {
background: var(--card-background);
border-radius: var(--tag-border-radius);
color: var(--card-text-color-tertiary);
margin-right: 30px;
display: inline-flex;
align-items: center;
font-size: 1.4rem;
text-transform: uppercase;
padding: 10px 20px 10px 15px;
transition: box-shadow 0.3s ease;
box-shadow: var(--shadow-l1);
&:hover {
box-shadow: var(--shadow-l2);
}
svg {
margin-right: 5px;
width: 20px;
height: 20px;
}
span {
font-weight: 500;
white-space: nowrap;
}
}
}
.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--toc {
background-color: var(--card-background);
border-radius: var(--card-border-radius);
@ -193,6 +86,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;
@ -207,7 +104,7 @@
list-style-type: none;
counter-reset: item;
li:before {
li a::before {
counter-increment: item;
content: counters(item, ".") ". ";
font-weight: bold;
@ -220,7 +117,7 @@
}
li {
margin: 15px 20px;
margin: 15px 0 15px 20px;
padding: 5px;
& > ol,
@ -234,11 +131,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 {
@ -286,9 +211,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 {
@ -303,7 +228,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);
}
@ -331,13 +256,10 @@
flex-direction: row;
justify-content: center;
margin: 1.5em 0;
gap: 10px;
figure {
margin: 0;
& + figure {
margin-left: 10px;
}
}
}
@ -350,7 +272,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;
@ -359,6 +284,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;
@ -407,38 +376,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;
}
}

View File

@ -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 {

View File

@ -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;
}
}
}

View File

@ -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,11 +201,19 @@
}
}
}
.menu-bottom-section {
margin-top: auto;
display: flex;
flex-direction: column;
width: 100%;
}
}
.social-menu {
list-style: none;
padding: 0%;
padding: 0;
margin: 0;
display: flex;
flex-direction: row;
gap: 10px;

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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";

View File

@ -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);
}

View File

@ -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 (<a><figure><img></figure></a>)
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 <a> 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 <a> tag if image was not wrapped by <a> 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 = [];

View File

@ -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;

131
assets/ts/scrollspy.ts Normal file
View File

@ -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<Element>): 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<Element>) {
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 };

View File

@ -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(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
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 '<mark>' + match + '</mark>';
}
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 {
<a href={item.permalink}>
<div class="article-details">
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: this.displayTitle(item) }}></h2>
<secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion>
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
</div>
{item.image &&
<div class="article-image">
@ -264,4 +326,4 @@ window.addEventListener('load', () => {
}, 0);
})
export default Search;
export default Search;

View File

@ -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 };

View File

@ -19,6 +19,7 @@ params:
lastUpdated: Jan 02, 2006 15:04 MST
sidebar:
compact: false
emoji:
subtitle:
avatar:
@ -95,6 +96,7 @@ params:
darkTheme:
reactionsEnabled: 1
emitMetadata: 0
lang:
gitalk:
owner:
@ -108,16 +110,8 @@ params:
id:
widgets:
enabled:
- search
- archives
- tag-cloud
archives:
limit: 5
tagCloud:
limit: 10
homepage: []
page: []
opengraph:
twitter:

View File

@ -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

View File

@ -4,6 +4,21 @@ theme: hugo-theme-stack
paginate: 5
title: Example Site
languages:
en:
languageName: English
title: Example Site
weight: 1
zh-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,7 +26,7 @@ disqusShortname: hugo-theme-stack
googleAnalytics:
# Theme i18n support
# Available values: en, fr, id, ja, ko, pt-br, zh-cn, zh-tw, es, de, nl, it, th, el, uk
# Available values: ar, ca, de, el, en, es, fr, id, it, ja, ko, nl, pt-br, th, uk, zh-cn, zh-hk, zh-tw
DefaultContentLanguage: en
# Set hasCJKLanguage to true if DefaultContentLanguage is in [zh-cn ja ko]
@ -104,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:
@ -116,27 +137,29 @@ params:
emitMetadata: 0
gitalk:
owner:
admin:
repo:
clientID:
clientSecret:
owner:
admin:
repo:
clientID:
clientSecret:
cusdis:
host:
id:
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:
@ -169,28 +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
params:
### For demonstration purpose, the home link will be open in a new tab
newTab: true
icon: home
main: []
social:
- identifier: github
name: GitHub
url: https://github.com/CaiJimmy/hugo-theme-stack
params:
icon: brand-github
icon: brand-github
- identifier: twitter
name: Twitter
url: https://twitter.com
params:
icon: brand-twitter
icon: brand-twitter
related:
includeNewer: true
@ -214,3 +229,9 @@ markup:
startLevel: 2
highlight:
noClasses: false
codeFences: true
guessSyntax: true
lineNoStart: 1
lineNos: true
lineNumbersInTable: true
tabWidth: 4

View File

@ -0,0 +1,8 @@
---
menu:
main:
name: Home
weight: -100
params:
icon: home
---

View File

@ -0,0 +1,8 @@
---
menu:
main:
name: 主页
weight: -100
params:
icon: home
---

View File

@ -0,0 +1,10 @@
---
title: 关于
menu:
main:
weight: -90
params:
icon: user
---
This is a test page for i18n support.

View File

@ -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
@ -158,3 +162,7 @@ X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
Press <kbd><kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>Delete</kbd></kbd> to end the session.
Most <mark>salamanders</mark> 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)

View File

@ -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

View File

@ -36,3 +36,7 @@ Hugo ships with several [Built-in Shortcodes](https://gohugo.io/content-manageme
## bilibilibi Shortcode
{{< bilibili av498363026 >}}
## Gist Shortcode
{{< gist spf13 7896402 >}}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/CaiJimmy/hugo-theme-stack/v3
go 1.17

70
i18n/ar.yaml Normal file
View File

@ -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: "تُقرأ خلال دقيقة"
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 }}"

73
i18n/ca.yaml Normal file
View File

@ -0,0 +1,73 @@
toggleMenu:
other: Toggle Menu
darkMode:
other: Mode fosc
list:
page:
one: "{{ .Count }} pàgina"
other: "{{ .Count }} pàgines"
section:
other: Secció
subsection:
one: Subsecció
other: Subseccions
article:
back:
other: Tornar
tableOfContents:
other: Taula de contingut
relatedContents:
other: Continguts relacionats
lastUpdatedOn:
other: Última vegada actualitzat
readingTime:
one: "{{ .Count }} minut a llegir"
other: "{{ .Count }} minuts a llegir"
notFound:
title:
other: No Trobat
subtitle:
other: Aquesta pàgina no existeix
widget:
archives:
title:
other: Arxiu
more:
other: Més
tagCloud:
title:
other: Etiquetes
categoriesCloud:
title:
other: Categories
search:
title:
other: Cerca
placeholder:
other: Tecleja alguna cosa...
resultTitle:
other: "#PAGES_COUNT pàgines en (#TIME_SECONDS segons)"
footer:
builtWith:
other: Creat amb {{ .Generator }}
designedBy:
other: Tema {{ .Theme }} dissenyat per {{ .DesignedBy }}

View File

@ -51,6 +51,9 @@ widget:
tagCloud:
title:
other: Tags
categoriesCloud:
title:
other: Categories
search:
title:

View File

@ -51,6 +51,9 @@ widget:
tagCloud:
title:
other: Etiquetas
categoriesCloud:
title:
other: Categorías
search:
title:

View File

@ -51,6 +51,10 @@ widget:
title:
other: Tag
categoriesCloud:
title:
other: Kategori
search:
title:
other: Cari

View File

@ -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 }} によって設計されています。

View File

@ -50,6 +50,10 @@ widget:
tagCloud:
title:
other: Tagi
categoriesCloud:
title:
other: Kategorie
search:
title:

View File

@ -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 }} 设计

73
i18n/zh-hk.yaml Normal file
View File

@ -0,0 +1,73 @@
toggleMenu:
other: 切換選單
darkMode:
other: 深色模式
list:
page:
one: "第 {{ .Count }} 頁"
other: "第 {{ .Count }} 頁"
section:
other: Section
subsection:
one: Subsection
other: Subsections
article:
back:
other: 返回
tableOfContents:
other: 目錄
relatedContents:
other: 相關內容
lastUpdatedOn:
other: 上次改過於
readingTime:
one: "需要 {{ .Count }} 分鐘閱讀"
other: "需要 {{ .Count }} 分鐘閱讀"
notFound:
title:
other: Not Found
subtitle:
other: 頁面不存在
widget:
archives:
title:
other: Archives
more:
other: 更多
tagCloud:
title:
other: Tags
categoriesCloud:
title:
other: Categories
search:
title:
other: 搜尋
placeholder:
other: Type 關鍵字...
resultTitle:
other: "#PAGES_COUNT pages (#TIME_SECONDS seconds)"
footer:
builtWith:
other: Built with {{ .Generator }}
designedBy:
other: 主題 {{ .Theme }} 由 {{ .DesignedBy }} 設計

View File

@ -25,22 +25,17 @@
{{- end -}}
{{- end -}}
<figure
<img src="{{ $Permalink }}"
{{ with $Width }}width="{{ . }}"{{ end }}
{{ with $Height }}height="{{ . }}"{{ end }}
{{ with $Srcset }}srcset="{{ . }}"{{ end }}
loading="lazy"
{{ with $alt }}
alt="{{ . }}"
{{ end }}
{{ if $galleryImage }}
class="gallery-image"
style="
flex-grow: {{ div (mul $image.Width 100) $image.Height }};
flex-basis: {{ div (mul $image.Width 240) $image.Height }}px"
{{ end }}>
<a href="{{ $Permalink }}" {{ if $galleryImage }}data-size="{{ $image.Width }}x{{ $image.Height }}"{{ end }}>
<img src="{{ $Permalink }}"
{{ with $Width }}width="{{ . }}"{{ end }}
{{ with $Height }}height="{{ . }}"{{ end }}
{{ with $Srcset }}srcset="{{ . }}"{{ end }}
loading="lazy"
{{ with $alt }}alt="{{ . }}"{{ end }}>
</a>
{{ with $alt }}
<figcaption>{{ . | markdownify }}</figcaption>
data-flex-grow="{{ div (mul $image.Width 100) $image.Height }}"
data-flex-basis="{{ div (mul $image.Width 240) $image.Height }}px"
{{ end }}
</figure>
>

View File

@ -1,17 +1,19 @@
{{ define "body-class" }}template-archives{{ end }}
{{ define "main" }}
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
{{- $terms := $taxonomy.Pages -}}
{{ if $terms }}
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
<div class="subsection-list">
<div class="article-list--tile">
{{ range $terms }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
{{ end }}
<header>
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
{{- $terms := $taxonomy.Pages -}}
{{ if $terms }}
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
<div class="subsection-list">
<div class="article-list--tile">
{{ range $terms }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ end }}
</header>
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
{{ $notHidden := where .Site.RegularPages "Params.hidden" "!=" true }}

View File

@ -1,12 +1,20 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
<html lang="{{ .Site.LanguageCode }}" dir="{{ default `ltr` .Language.LanguageDirection }}">
<head>
{{- partial "head/head.html" . -}}
{{- block "head" . -}}{{ end }}
</head>
<body class="{{ block `body-class` . }}{{ end }}">
{{- partial "head/colorScheme" . -}}
<div class="container main-container flex {{ block `container-class` . }}on-phone--column {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }}{{ end }}">
{{/* The container is wider when there's any activated widget */}}
{{- $hasWidget := false -}}
{{- range .Site.Params.widgets -}}
{{- if gt (len .) 0 -}}
{{- $hasWidget = true -}}
{{- end -}}
{{- end -}}
<div class="container main-container flex on-phone--column {{ if $hasWidget }}extended{{ else }}compact{{ end }}">
{{- block "left-sidebar" . -}}
{{ partial "sidebar/left.html" . }}
{{- end -}}

View File

@ -1,46 +1,48 @@
{{ define "main" }}
<h3 class="section-title">
{{ if eq .Parent (.GetPage "/") }}
{{ T "list.section" }}
{{ else }}
{{ .Parent.Title }}
{{ end }}
</h3>
<div class="section-card">
<div class="section-details">
<h3 class="section-count">{{ T "list.page" (len .Pages) }}</h3>
<h1 class="section-term">{{ .Title | markdownify }}</h1>
{{ with .Params.description }}
<h2 class="section-description">{{ . }}</h2>
<header>
<h3 class="section-title">
{{ if eq .Parent (.GetPage "/") }}
{{ T "list.section" }}
{{ else }}
{{ .Parent.Title }}
{{ end }}
</div>
</h3>
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "section") .RelPermalink "section" -}}
{{ if $image.exists }}
<div class="section-image">
{{ 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 -}}
<img src="{{ $Permalink }}"
width="{{ $Width }}"
height="{{ $Height }}"
loading="lazy">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" />
<div class="section-card">
<div class="section-details">
<h3 class="section-count">{{ T "list.page" (len .Pages) }}</h3>
<h1 class="section-term">{{ .Title | markdownify }}</h1>
{{ with .Params.description }}
<h2 class="section-description">{{ . }}</h2>
{{ end }}
</div>
{{ end }}
</div>
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "section") .RelPermalink "section" -}}
{{ if $image.exists }}
<div class="section-image">
{{ 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 -}}
<img src="{{ $Permalink }}"
width="{{ $Width }}"
height="{{ $Height }}"
loading="lazy">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" />
{{ end }}
</div>
{{ end }}
</div>
</header>
{{- $subsections := .Sections -}}
{{- $pages := .Pages | complement $subsections -}}
@ -53,14 +55,16 @@
{{- end -}}
{{- with $subsections -}}
<h2 class="section-title">{{ T "list.subsection" (len $subsections) }}</h2>
<div class="subsection-list">
<div class="article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "section") }}
{{ end }}
<aside>
<h2 class="section-title">{{ T "list.subsection" (len $subsections) }}</h2>
<div class="subsection-list">
<div class="article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "section") }}
{{ end }}
</div>
</div>
</div>
</aside>
{{- end -}}
{{/* List only pages that are not a subsection */}}
@ -77,5 +81,5 @@
{{ end }}
{{ define "right-sidebar" }}
{{ partialCached "sidebar/right.html" . }}
{{ end }}
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
{{ end }}

View File

@ -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 "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
@ -20,7 +26,7 @@
{{- with .OutputFormats.Get "RSS" -}}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{- end -}}
{{ range $filtered }}
{{ range $pages }}
{{- $content := safeHTML (.Summary | html) -}}
{{- if .Site.Params.rssFullContent -}}
{{- $content = safeHTML (.Content | html) -}}

View File

@ -1,15 +1,26 @@
{{ 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" }}
@ -30,29 +41,6 @@
{{ partialCached "article/components/photoswipe" . }}
{{ end }}
{{ define "left-sidebar" }}
{{ if (.Scratch.Get "hasTOC") }}
<div id="article-toolbar">
<a href="{{ .Site.BaseURL | relLangURL }}" class="back-home">
{{ (resources.Get "icons/back.svg").Content | safeHTML }}
<span>{{ T "article.back" }}</span>
</a>
</div>
{{ else }}
{{ partial "sidebar/left.html" . }}
{{ end }}
{{ end }}
{{ define "right-sidebar" }}
{{ if (.Scratch.Get "hasTOC") }}
<aside class="sidebar right-sidebar sticky">
<section class="widget archives">
<h2 class="widget-title section-title">{{ T "article.tableOfContents" }}</h2>
<div class="widget--toc">
{{ .TableOfContents }}
</div>
</section>
</aside>
{{ end }}
{{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}}
{{ end }}

View File

@ -15,5 +15,5 @@
{{ end }}
{{ define "right-sidebar" }}
{{ partial "sidebar/right.html" . }}
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
{{ end }}

View File

@ -16,8 +16,10 @@
</button>
</form>
<h3 class="search-result--title section-title"></h3>
<div class="search-result--list article-list--compact"></div>
<div class="search-result">
<h3 class="search-result--title section-title"></h3>
<div class="search-result--list article-list--compact"></div>
</div>
<script>
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"

View File

@ -1,3 +1,5 @@
<section class="article-content">
{{ .Content }}
<!-- Refer to https://discourse.gohugo.io/t/responsive-tables-in-markdown/10639/5 -->
{{ $wrappedTable := printf "<div class=\"table-wrapper\">${1}</div>" }}
{{ .Content | replaceRE "(<table>(?:.|\n)+?</table>)" $wrappedTable | safeHTML }}
</section>

View File

@ -47,4 +47,15 @@
{{ end }}
</footer>
{{ end }}
</div>
{{ if .IsTranslated }}
<footer class="article-translations">
{{ partial "helper/icon" "language" }}
<div>
{{ range .Translations }}
<a href="{{ .Permalink }}" class="link">{{ .Language.LanguageName }}</a>
{{ end }}
</div>
</footer>
{{ end }}
</div>

View File

@ -1,13 +1,13 @@
{{ $related := (where (.Site.RegularPages.Related .) "Params.hidden" "!=" true) | first 5 }}
{{ with $related }}
<aside class="related-contents--wrapper">
{{ $related := (where (.Site.RegularPages.Related .) "Params.hidden" "!=" true) | first 5 }}
{{ with $related }}
<h2 class="section-title">{{ T "article.relatedContents" }}</h2>
<div class="related-contents">
<div class="flex article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "articleList") }}
{{ end }}
</div>
<h2 class="section-title">{{ T "article.relatedContents" }}</h2>
<div class="related-contents">
<div class="flex article-list--tile">
{{ range . }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "articleList") }}
{{ end }}
</div>
{{ end }}
</div>
</aside>
{{ end }}

View File

@ -0,0 +1,29 @@
{{- with .Site.Params.comments.cactus -}}
{{- partial "helper/external" (dict "Context" $ "Namespace" "Cactus") -}}
<style>
.cactus-editor-textarea {
color: var(--body-text-color);
}
.cactus-comment-header {
color: var(--card-text-color-main);
}
.cactus-message-text > p {
color: var(--body-text-color);
}
</style>
<div id="comment-section"></div>
<script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "{{ .defaultHomeserverUrl | safeJS }}",
serverName: "{{ .serverName }}",
siteName: "{{ .siteName }}",
commentSectionId: "{{ $.File.UniqueID }}"
})
</script>
{{- end -}}

View File

@ -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
></script>
<script>
function setGiscusTheme(theme) {
let giscus = document.querySelector('iframe.giscus-frame');
let giscus = document.querySelector("iframe.giscus-frame");
if (giscus) {
giscus.contentWindow.postMessage(
{
{
giscus: {
setConfig: {
theme: theme
}
}
setConfig: {
theme: theme,
},
},
},
"https://giscus.app"
);
};
};
}
}
(function(){
addEventListener('message', (e) => {
if (event.origin !== 'https://giscus.app') return;
handler()
(function () {
addEventListener("message", (e) => {
if (event.origin !== "https://giscus.app") return;
handler();
});
window.addEventListener('onColorSchemeChange', handler);
window.addEventListener("onColorSchemeChange", handler);
function handler() {
if (document.documentElement.dataset.scheme === "light") {
setGiscusTheme('{{- default "light" .lightTheme -}}');
} else {
setGiscusTheme('{{- default "dark_dimmed" .darkTheme -}}');
};
};
}());
}
}
})();
</script>
{{- end -}}

View File

@ -1,4 +1,4 @@
<script src="//cdn.jsdelivr.net/npm/twikoo@1.4.3/dist/twikoo.all.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/twikoo@1.4.18/dist/twikoo.all.min.js"></script>
<div id="tcomment"></div>
<style>
.twikoo {

View File

@ -3,7 +3,7 @@
<div id="vssue"></div>
<script src="https://unpkg.com/vue/dist/vue.runtime.min.js"></script>
<script src="https://unpkg.com/vue@2/dist/vue.runtime.min.js"></script>
<script src="https://unpkg.com/vssue/dist/vssue.{{ .platform }}.min.js"></script>
<script>

View File

@ -3,4 +3,10 @@
{{- $opts := dict "minify" hugo.IsProduction -}}
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
{{- with resources.Get "ts/custom.ts" -}}
{{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}}
{{- $customScript := . | js.Build $opts -}}
<script type="text/javascript" src="{{ $customScript.RelPermalink }}" defer></script>
{{- end -}}

View File

@ -1,4 +1,4 @@
{{- $ThemeVersion := "3.6.0" -}}
{{- $ThemeVersion := "3.11.0" -}}
<footer class="site-footer">
<section class="copyright">
&copy;

View File

@ -8,7 +8,7 @@
integrity="{{ . }}"
{{- end -}}
crossorigin="anonymous"
defer="{{ default false .defer }}"
{{ if .defer }}defer{{ end }}
>
</script>
{{- else if eq .type "style" -}}

View File

@ -1,14 +1,15 @@
<aside class="sidebar left-sidebar sticky">
<aside class="sidebar left-sidebar sticky {{ if .Site.Params.sidebar.compact }}compact{{ end }}">
<button class="hamburger hamburger--spin" type="button" id="toggle-menu" aria-label="{{ T `toggleMenu` }}">
<span class="hamburger-box">
<span class="hamburger-inner"></span>
</span>
</button>
<header class="site-info">
<header>
{{ with .Site.Params.sidebar.avatar }}
{{ if (default true .enabled) }}
<figure class="site-avatar">
<a href="{{ .Site.BaseURL | relLangURL }}">
{{ if not .local }}
<img src="{{ .src }}" width="300" height="300" class="site-logo" loading="lazy" alt="Avatar">
{{ else }}
@ -22,7 +23,7 @@
{{ errorf "Failed loading avatar from %q" . }}
{{ end }}
{{ end }}
</a>
{{ with $.Site.Params.sidebar.emoji }}
<span class="emoji">{{ . }}</span>
{{ end }}
@ -30,29 +31,31 @@
{{ end }}
{{ end }}
<h1 class="site-name"><a href="{{ .Site.BaseURL | relLangURL }}">{{ .Site.Title }}</a></h1>
<h2 class="site-description">{{ .Site.Params.sidebar.subtitle }}</h2>
{{- with .Site.Menus.social -}}
<ol class="social-menu">
{{ range . }}
<li>
<a
href='{{ .URL }}'
{{ if eq (default true .Params.newTab) true }}target="_blank"{{ end }}
{{ with .Name }}title="{{ . }}"{{ end }}
>
{{ $icon := default "link" .Params.Icon }}
{{ with $icon }}
{{ partial "helper/icon" . }}
{{ end }}
</a>
</li>
{{ end }}
</ol>
{{- end -}}
<div class="site-meta">
<h1 class="site-name"><a href="{{ .Site.BaseURL | relLangURL }}">{{ .Site.Title }}</a></h1>
<h2 class="site-description">{{ .Site.Params.sidebar.subtitle }}</h2>
</div>
</header>
{{- with .Site.Menus.social -}}
<ol class="social-menu">
{{ range . }}
<li>
<a
href='{{ .URL }}'
{{ if eq (default true .Params.newTab) true }}target="_blank"{{ end }}
{{ with .Name }}title="{{ . }}"{{ end }}
>
{{ $icon := default "link" .Params.Icon }}
{{ with $icon }}
{{ partial "helper/icon" . }}
{{ end }}
</a>
</li>
{{ end }}
</ol>
{{- end -}}
<ol class="menu" id="main-menu">
{{ $currentPage := . }}
{{ range .Site.Menus.main }}
@ -72,12 +75,26 @@
</li>
{{ end }}
{{ if (default false .Site.Params.colorScheme.toggle) }}
<li id="dark-mode-toggle">
{{ partial "helper/icon" "toggle-left" }}
{{ partial "helper/icon" "toggle-right" }}
<span>{{ T "darkMode" }}</span>
</li>
{{ end }}
<div class="menu-bottom-section">
{{- $currentLanguageCode := .Language.Lang -}}
{{ with .Site.Home.AllTranslations }}
<li id="i18n-switch">
{{ partial "helper/icon" "language" }}
<select name="language" onchange="window.location.href = this.selectedOptions[0].value">
{{ range . }}
<option value="{{ .Permalink }}" {{ if eq .Language.Lang $currentLanguageCode }}selected{{ end }}>{{ .Language.LanguageName }}</option>
{{ end }}
</select>
</li>
{{ end }}
{{ if (default false .Site.Params.colorScheme.toggle) }}
<li id="dark-mode-toggle">
{{ partial "helper/icon" "toggle-left" }}
{{ partial "helper/icon" "toggle-right" }}
<span>{{ T "darkMode" }}</span>
</li>
{{ end }}
</div>
</ol>
</aside>

View File

@ -1,8 +1,13 @@
{{ if .Site.Params.widgets.enabled }}
{{ $context := . }}
{{- $scope := default "homepage" .Scope -}}
{{- $context := .Context -}}
{{- with (index .Context.Site.Params.widgets $scope) -}}
<aside class="sidebar right-sidebar sticky">
{{ range $widget := .Site.Params.widgets.enabled }}
{{ partial (printf "widget/%s" $widget) $context }}
{{ range $widget := . }}
{{ if templates.Exists (printf "partials/widget/%s.html" .type) }}
{{ partial (printf "widget/%s" .type) (dict "Context" $context "Params" .params) }}
{{ else }}
{{ warnf "Widget %s not found" .type }}
{{ end }}
{{ end }}
</aside>
{{ end }}

View File

@ -1,20 +1,22 @@
{{- $query := first 1 (where .Site.Pages "Layout" "==" "archives") -}}
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}}
{{- $context := .Context -}}
{{- $limit := default 5 .Params.limit -}}
{{- if $query -}}
{{- $archivesPage := index $query 0 -}}
<section class="widget archives">
<h2 class="widget-title section-title">{{ T "widget.archives.title" }}</h2>
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
{{ $notHidden := where .Site.RegularPages "Params.hidden" "!=" true }}
{{ $pages := where $context.Site.RegularPages "Type" "in" $context.Site.Params.mainSections }}
{{ $notHidden := where $context.Site.RegularPages "Params.hidden" "!=" true }}
{{ $filtered := ($pages | intersect $notHidden) }}
{{ $archives := $filtered.GroupByDate "2006" }}
<div class="widget-archive--list">
{{ range $index, $item := first (add .Site.Params.widgets.archives.limit 1) ($archives) }}
{{ range $index, $item := first (add $limit 1) ($archives) }}
{{- $id := lower (replace $item.Key " " "-") -}}
<div class="archives-year">
<a href="{{ $archivesPage.RelPermalink }}#{{ $id }}">
{{ if eq $index $.Site.Params.widgets.archives.limit }}
{{ if eq $index $limit }}
<span class="year">{{ T "widget.archives.more" }}</span>
{{ else }}
<span class="year">{{ .Key }}</span>

View File

@ -0,0 +1,16 @@
{{- $context := .Context -}}
{{- $limit := default 10 .Params.limit -}}
<section class="widget tagCloud">
<div class="widget-icon">
{{ partial "helper/icon" "categories" }}
</div>
<h2 class="widget-title section-title">{{ T "widget.categoriesCloud.title" }}</h2>
<div class="tagCloud-tags">
{{ range first $limit $context.Site.Taxonomies.categories.ByCount }}
<a href="{{ .Page.RelPermalink }}" class="font_size_{{ .Count }}">
{{ .Page.Title }}
</a>
{{ end }}
</div>
</section>

View File

@ -1,4 +1,4 @@
{{- $query := first 1 (where .Site.Pages "Layout" "==" "search") -}}
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}}
{{- if $query -}}
{{- $searchPage := index $query 0 -}}
<form action="{{ $searchPage.RelPermalink }}" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>

View File

@ -1,3 +1,5 @@
{{- $context := .Context -}}
{{- $limit := default 10 .Params.limit -}}
<section class="widget tagCloud">
<div class="widget-icon">
{{ partial "helper/icon" "tag" }}
@ -5,7 +7,7 @@
<h2 class="widget-title section-title">{{ T "widget.tagCloud.title" }}</h2>
<div class="tagCloud-tags">
{{ range first .Site.Params.widgets.tagCloud.limit .Site.Taxonomies.tags.ByCount }}
{{ range first $limit $context.Site.Taxonomies.tags.ByCount }}
<a href="{{ .Page.RelPermalink }}" class="font_size_{{ .Count }}">
{{ .Page.Title }}
</a>

View File

@ -0,0 +1,12 @@
{{ if (.Context.Scratch.Get "TOCEnabled") }}
<section class="widget archives">
<div class="widget-icon">
{{ partial "helper/icon" "hash" }}
</div>
<h2 class="widget-title section-title">{{ T "article.tableOfContents" }}</h2>
<div class="widget--toc">
{{ .Context.TableOfContents }}
</div>
</section>
{{ end }}