Merge branch 'master' into toggle-dark-mode

This commit is contained in:
Jimmy Cai 2020-12-19 11:37:14 +01:00
parent 7e1c7b94bf
commit 8d018a703f
No known key found for this signature in database
GPG Key ID: 3EA408E527F37B18
69 changed files with 1377 additions and 532 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/jimmycai']

31
.github/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name-template: 'v$RESOLVED_VERSION 🌈'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🚀 Features'
labels:
- 'feature'
- 'enhancement'
- title: '🐛 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## Changes
$CHANGES

16
.github/workflows/release-drafter.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -11,7 +11,7 @@
## Documentation & more information
[Documentation](https://www.notion.so/jimmycai/Hugo-Theme-Stack-511aec5b9ed845ce9b6e3ae0bf7fb6d4) | [中文文档](https://www.notion.so/jimmycai/Hugo-Theme-Stack-511aec5b9ed845ce9b6e3ae0bf7fb6d4)
[Documentation](https://docs.stack.jimmycai.com/) | [中文文档](https://docs.stack.jimmycai.com/v/zh-cn/)
## Introduction
@ -35,7 +35,12 @@ The only JavaScript library being used is [node-vibrant](https://github.com/Vibr
This theme uses SCSS and TypeScript. For that reason, it's necessary to use **Hugo ≥ 0.74.0**.
**Note**: You'll need Hugo Extended version to edit SCSS files
Use Hugo Extended version if you want to:
* Use the latest feature/fix from `master` branch
* Edit SCSS files
**Compiled CSS are updated once per release.**
## Installation
@ -51,6 +56,14 @@ Please do not remove the "*Theme Stack designed by Jimmy*" text and link.
If you want to port this theme to another blogging platform, please let me know🙏.
## Sponsoring
If you like this theme, consider supporting its development:
<a href="https://www.buymeacoffee.com/jimmycai" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-green.png" alt="Buy Me A Coffee" height="60px" width="217px"></a>
Your support is greatly appreciated :)
## Thanks to
- [Vibrant-Colors/node-vibrant](https://github.com/Vibrant-Colors/node-vibrant)

7
assets/icons/search.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" 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"/>
<circle cx="10" cy="10" r="7" />
<line x1="21" y1="21" x2="15" y2="15" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

1
assets/scss/custom.scss Normal file
View File

@ -0,0 +1 @@
/* Place your custom SCSS in HUGO_SITE_FOLDER/assets/scss/custom.scss */

119
assets/scss/grid.scss Normal file
View File

@ -0,0 +1,119 @@
.container {
margin-left: auto;
margin-right: auto;
&.extended {
@media (min-width: $on-phone) {
max-width: 800px;
.left-sidebar {
width: 25%;
}
}
@media (min-width: $on-tablet) {
max-width: 972px;
.right-sidebar {
width: 25%;
}
}
@media (min-width: $on-desktop) {
max-width: 1200px;
.left-sidebar {
width: 20%;
}
.right-sidebar {
width: 25%;
}
}
@media (min-width: $on-desktop-large) {
max-width: 1536px;
.left-sidebar {
width: 15%;
}
}
}
&.compact {
@media (min-width: $on-phone) {
max-width: 800px;
.left-sidebar {
width: 25%;
}
}
@media (min-width: $on-tablet) {
max-width: 972px;
}
@media (min-width: $on-desktop) {
max-width: 1050px;
.left-sidebar {
width: 20%;
}
}
@media (min-width: $on-desktop-large) {
max-width: 1300px;
}
}
}
.flex {
display: flex;
flex-direction: row;
&.column {
flex-direction: column;
}
&.on-phone--column {
@media (max-width: $on-phone) {
flex-direction: column;
}
}
&.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%;
}
}
main.main {
min-width: 0;
padding: 0 15px;
max-width: 100%;
flex-grow: 1;
padding-top: var(--main-top-padding);
}
.main-grid {
@media (max-width: $on-phone) {
flex-direction: column;
}
}

View File

@ -11,7 +11,7 @@
border-radius: var(--card-border-radius);
overflow: hidden;
transition: box-shadow .3s ease;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: var(--shadow-l2);
@ -53,7 +53,7 @@
flex-direction: column;
justify-content: center;
padding: var(--content-padding);
padding: var(--card-padding);
}
.article-category {
@ -75,10 +75,14 @@
}
.article-title {
font-size: 2.4rem;
font-weight: 600;
margin: 10px 0;
color: var(--card-text-color-main);
font-size: 2.2rem;
@media (min-width: $on-desktop-large) {
font-size: 2.4rem;
}
a {
color: var(--card-text-color-main);
@ -88,14 +92,6 @@
}
}
@media (min-width: $on-desktop-large) {
font-size: 2.4rem;
}
@media (max-width: $on-desktop) {
font-size: 2rem;
}
& + .article-subtitle {
margin-top: 0;
}
@ -103,18 +99,14 @@
.article-subtitle {
font-weight: normal;
font-size: 1.8rem;
color: var(--card-text-color-secondary);
margin: 5px 0;
line-height: 1.5;
font-size: 1.75rem;
@media (min-width: $on-desktop-large) {
font-size: 2rem;
}
@media (max-width: $on-desktop) {
font-size: 1.6rem;
}
}
.article-time {
@ -125,9 +117,10 @@
svg {
vertical-align: middle;
margin-right: 8px;
margin-right: 15px;
width: 20px;
height: 20px;
stroke-width: 1.33;
}
time {
@ -159,24 +152,32 @@
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
background-color: var(--card-background);
--image-size: 60px;
@media (max-width: $on-tablet) {
--image-size: 50px;
}
& + .pagination {
margin-top: var(--section-separation);
}
article {
display: flex;
align-items: center;
padding: var(--small-card-padding);
& > a {
display: flex;
align-items: center;
padding: var(--small-card-padding);
}
&:not(:last-of-type) {
border-bottom: 2px solid var(--card-separator-color);
border-bottom: 1.5px solid var(--card-separator-color);
}
.article-details {
flex-grow: 1;
padding: 0;
padding-right: 15px;
min-height: var(--image-size);
}
.article-title {
@ -190,10 +191,21 @@
.article-image {
img {
width: 60px;
height: 60px;
width: var(--image-size);
height: var(--image-size);
}
}
.article-time {
font-size: 1.4rem;
}
.article-preview{
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
margin-top: 10px;
line-height: 1.5;
}
}
}

View File

@ -2,5 +2,5 @@
background-color: var(--card-background);
box-shadow: var(--shadow-l1);
border-radius: var(--card-border-radius);
padding: var(--content-padding);
padding: var(--card-padding);
}

View File

@ -1,15 +1,5 @@
.archives-group {
margin-bottom: var(--section-separation);
.archives-date {
text-transform: uppercase;
margin-bottom: 10px;
font-size: 1.6rem;
font-weight: bold;
a {
color: var(--body-text-color);
}
}
}
.template-archives {

View File

@ -43,7 +43,7 @@
}
}
article {
.main-article {
background: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
@ -64,13 +64,13 @@
}
.article-details {
padding: var(--content-padding);
padding: var(--card-padding);
padding-bottom: 0;
}
}
.article-content {
margin: var(--content-padding) 0;
margin: var(--card-padding) 0;
color: var(--card-text-color-main);
img {
@ -80,11 +80,11 @@
}
.article-footer {
padding: var(--content-padding);
padding-top: 0;
margin: var(--card-padding);
margin-top: 0;
section:not(:first-child) {
margin-top: var(--content-padding);
margin-top: var(--card-padding);
}
section {
@ -104,6 +104,7 @@
.article-tags {
flex-wrap: wrap;
text-transform: unset;
}
}
}
@ -192,7 +193,7 @@
& > p {
margin: 1.5em 0;
padding: 0 var(--content-padding);
padding: 0 var(--card-padding);
}
h1,
@ -201,7 +202,7 @@
h4,
h5,
h6 {
padding: 0 calc(var(--content-padding) - var(--heading-border-size));
padding: 0 calc(var(--card-padding) - var(--heading-border-size));
border-left: var(--heading-border-size) solid var(--accent-color);
}
@ -219,13 +220,13 @@
position: relative;
margin: 10px 0;
border-left: var(--blockquote-border-size) solid var(--card-separator-color);
padding: 15px calc(var(--content-padding) - var(--blockquote-border-size));
padding: 15px calc(var(--card-padding) - var(--blockquote-border-size));
background-color: var(--blockquote-background-color);
}
& > ul,
& > ol {
margin: 1em var(--content-padding);
margin: 1em var(--card-padding);
}
hr {
@ -270,7 +271,7 @@
font-family: var(--code-font-family);
line-height: 1.428571429;
word-break: break-all;
padding: var(--content-padding);
padding: var(--card-padding);
code {
color: unset;
@ -281,9 +282,9 @@
}
table {
margin: 0 var(--content-padding);
margin: 0 var(--card-padding);
width: 100%;
max-width: calc(100% - var(--content-padding) * 2);
max-width: calc(100% - var(--card-padding) * 2);
border-collapse: collapse;
border-spacing: 0;
margin-bottom: 1.5em;

View File

@ -0,0 +1,82 @@
.search-form {
margin-bottom: var(--section-separation);
position: relative;
--button-size: 80px;
&.widget {
--button-size: 60px;
label {
font-size: 1.3rem;
top: 10px;
}
input {
font-size: 1.5rem;
padding: 30px 20px 15px 20px;
}
}
p {
position: relative;
margin: 0;
}
label {
position: absolute;
top: 15px;
left: 20px;
font-size: 1.4rem;
color: var(--card-text-color-tertiary);
}
input {
padding: 40px 20px 20px;
border-radius: var(--card-border-radius);
background-color: var(--card-background);
box-shadow: var(--shadow-l1);
color: var(--card-text-color-main);
width: 100%;
border: 0;
-webkit-appearance: none;
transition: box-shadow 0.3s ease;
font-size: 1.8rem;
&:focus {
outline: 0;
box-shadow: var(--shadow-l2);
}
}
button {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: var(--button-size);
cursor: pointer;
background-color: transparent;
border: 0;
padding: 0 10px;
&:focus {
outline: 0;
svg {
stroke-width: 2;
color: var(--accent-color);
}
}
svg {
color: var(--card-text-color-secondary);
stroke-width: 1.33;
transition: all 0.3s ease;
width: 20px;
height: 20px;
}
}
}

View File

@ -1,11 +1,3 @@
.taxonomy-type {
text-transform: uppercase;
color: var(--body-text-color);
font-weight: bold;
margin-bottom: 5px;
font-size: 1.6rem;
}
.taxonomy-card {
border-radius: var(--card-border-radius);
background-color: var(--card-background);

View File

@ -126,9 +126,11 @@
list-style: none;
display: flex;
flex-direction: column;
margin-top: 25px;
margin-top: var(--sidebar-element-separation);
margin-bottom: 0;
overflow-y: auto;
flex-grow: 1;
font-size: 1.5rem;
@media (min-width: $on-desktop-large) {
margin-top: 30px;
@ -180,6 +182,11 @@
height: 25px;
stroke-width: 1.33;
margin-right: 40px;
@media (max-width: $on-desktop-large) {
width: 20px;
height: 20px;
}
}
a {
@ -187,9 +194,8 @@
display: inline-flex;
align-items: center;
color: var(--body-text-color);
font-size: 1.5rem;
@media (max-width: $on-desktop) {
@media (max-width: $on-desktop-large) {
font-size: 1.4rem;
}
}

View File

@ -12,6 +12,14 @@
flex-direction: column;
flex-shrink: 0;
--sidebar-avatar-size: 150px;
--sidebar-element-separation: 25px;
@media (max-width: $on-desktop-large) {
--sidebar-avatar-size: 120px;
--sidebar-element-separation: 20px;
}
@media (max-width: $on-phone) {
width: 100%;
padding: 30px 0;
@ -23,19 +31,10 @@
}
@media (min-width: $on-phone + 1) {
width: 25%;
margin-right: 1%;
padding: var(--main-top-padding) 15px;
height: 100vh;
}
@media (min-width: $on-desktop) {
width: 20%;
}
@media (min-width: $on-desktop-large) {
width: 15%;
}
}
.right-sidebar {
@ -50,19 +49,15 @@
}
@media (min-width: $on-tablet) {
width: 25%;
margin-left: 1%;
padding-top: 50px;
}
@media (min-width: $on-desktop + 1) {
width: 25%;
padding-top: var(--main-top-padding);
}
}
.site-info {
z-index: 1;
transition: box-shadow 0.5s ease;
@media (max-width: $on-phone) {
padding: 15px 30px;
}
@ -70,14 +65,10 @@
.site-avatar {
position: relative;
margin: 0;
margin-bottom: 25px;
width: 150px;
height: 150px;
width: var(--sidebar-avatar-size);
height: var(--sidebar-avatar-size);
@media (max-width: $on-desktop-large) {
height: 120px;
width: 120px;
}
margin-bottom: var(--sidebar-element-separation);
.site-logo {
width: 100%;
@ -131,16 +122,15 @@
.sidebar {
.widget {
&:not(:last-of-type) {
margin-bottom: var(--section-separation);
&:after {
content: "";
width: 100px;
height: 2px;
background-color: var(--body-text-color);
display: block;
margin-top: var(--section-separation);
}
margin-bottom: var(--section-separation);
&:not(:last-of-type):after {
content: "";
width: 100px;
height: 2px;
background-color: var(--body-text-color);
display: block;
margin-top: var(--section-separation);
}
}
}

View File

@ -1,13 +1,4 @@
.widget {
.widget-title {
text-transform: uppercase;
color: var(--body-text-color);
font-weight: bold;
margin: 0;
margin-bottom: 10px;
font-size: 1.6rem;
}
.widget-icon {
svg {
width: 32px;
@ -45,28 +36,26 @@
/* Archives widget */
.widget.archives {
.widget-archive--list {
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
background-color: var(--card-background);
}
.archives-year {
margin-bottom: 10px;
&:not(:last-of-type) {
border-bottom: 1.5px solid var(--card-separator-color);
}
a {
background-color: var(--card-background);
padding: 15px 25px;
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
font-size: 1.4rem;
padding: 18px 25px;
display: flex;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: var(--shadow-l2);
}
@media (max-width: $on-desktop-large) {
padding: 12px 20px;
font-size: 1.4rem;
}
span.year {
flex: 1;
color: var(--card-text-color-main);
font-weight: bold;
}
span.count {

View File

@ -8,6 +8,7 @@
@import "breakpoints.scss";
@import "variables.scss";
@import "grid.scss";
@import "external/normalize.scss";
@ -22,6 +23,9 @@
@import "partials/layout/article.scss";
@import "partials/layout/taxonomy.scss";
@import "partials/layout/404.scss";
@import "partials/layout/search.scss";
@import "custom.scss";
a {
text-decoration: none;
@ -41,96 +45,16 @@ a {
}
}
.container {
margin-left: auto;
margin-right: auto;
.section-title {
text-transform: uppercase;
margin-top: 0;
margin-bottom: 10px;
display: block;
font-size: 1.6rem;
font-weight: bold;
color: var(--body-text-color);
&.extended {
@media (min-width: $on-phone) {
max-width: 800px;
}
@media (min-width: $on-tablet) {
max-width: 972px;
}
@media (min-width: $on-desktop) {
max-width: 1200px;
}
@media (min-width: $on-desktop-large) {
max-width: 1536px;
}
}
}
main.main {
min-width: 0;
padding: 0 15px;
max-width: 100%;
flex-grow: 1;
padding-top: var(--main-top-padding);
}
.main-grid {
@media (max-width: $on-phone) {
flex-direction: column;
}
}
.flex {
display: flex;
flex-direction: row;
&.column {
flex-direction: column;
}
&.on-phone--column {
@media (max-width: $on-phone) {
flex-direction: column;
}
}
&.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%;
}
}
.alert {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 5;
background: var(--card-background);
max-width: 400px;
padding: 15px 20px;
border-radius: var(--card-border-radius);
line-height: 1.75;
color: var(--card-text-color-secondary);
box-shadow: var(--shadow-l2);
@media (max-width: $on-phone) {
max-width: 100vw;
width: calc(100% - 30px);
left: 15px;
a {
color: var(--body-text-color);
}
}

View File

@ -6,9 +6,14 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff;
*/
:root {
@media (min-width: $on-phone + 1) {
--main-top-padding: 35px;
}
@media (min-width: $on-desktop-large) {
--main-top-padding: 50px;
}
--body-background: #f5f5fa;
--accent-color: #34495e;
@ -54,15 +59,18 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff;
--card-border-radius: 10px;
--content-padding: 30px;
--card-padding: 30px;
@media (max-width: $on-desktop-large) {
--content-padding: 25px;
--card-padding: 25px;
}
@media (max-width: $on-tablet) {
--content-padding: 20px;
--card-padding: 20px;
}
--small-card-padding: 25px;
@media (max-width: $on-tablet) {
--small-card-padding: 25px 20px;
}
.theme-dark {
--card-background: #424242;

View File

@ -0,0 +1,34 @@
/**
* createElement
* Edited from:
* @link https://stackoverflow.com/a/42405694
*/
function createElement(tag, attrs, children) {
var element = document.createElement(tag);
for (let name in attrs) {
if (name && attrs.hasOwnProperty(name)) {
let value = attrs[name];
if (name == "dangerouslySetInnerHTML") {
element.innerHTML = value.__html;
}
else if (value === true) {
element.setAttribute(name, name);
} else if (value !== false && value != null) {
element.setAttribute(name, value.toString());
}
}
}
for (let i = 2; i < arguments.length; i++) {
let child = arguments[i];
if (child) {
element.appendChild(
child.nodeType == null ?
document.createTextNode(child.toString()) : child);
}
}
return element;
}
export default createElement;

View File

@ -236,10 +236,10 @@ function wrap(gallery: HTMLElement[]) {
*/
function loadPhotoSwipe() {
const tasks = [
loadScript("https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js"),
loadScript("https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js"),
loadStyle("https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css"),
loadStyle("https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css")
loadScript("https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.js"),
loadScript("https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe-ui-default.min.js"),
loadStyle("https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/photoswipe.min.css"),
loadStyle("https://cdn.jsdelivr.net/npm/photoswipe@4.1.3/dist/default-skin/default-skin.min.css")
];
return Promise.all(tasks);

View File

@ -10,6 +10,7 @@ import { createGallery } from "./gallery"
import { getColor } from './color';
import menu from './menu';
import darkmode from "./darkmode";
import createElement from './createElement';
let Stack = {
init: () => {
@ -68,29 +69,6 @@ let Stack = {
observer.observe(articleTile)
}
},
alert: (content, time = 5000, animationSpeed = 500) => {
const alert = document.createElement('div');
alert.innerHTML = content;
alert.className = 'alert';
alert.style.visibility = 'hidden';
document.body.appendChild(alert);
alert.style.transform = `translateY(${alert.clientHeight + 50}px)`;
alert.style.transition = `transform ${animationSpeed / 1000}s ease`;
setTimeout(() => {
alert.style.removeProperty('visibility');
alert.style.transform = `translateY(0)`;
}, animationSpeed);
setTimeout(() => {
alert.style.transform = `translateY(${alert.clientHeight + 50}px)`;
}, animationSpeed + time);
setTimeout(() => {
alert.remove();
}, 2 * animationSpeed + time);
}
}
@ -100,4 +78,12 @@ window.addEventListener('load', () => {
}, 0);
})
declare global {
interface Window {
createElement: any;
Stack: any
}
}
window.Stack = Stack;
window.createElement = createElement;

263
assets/ts/search.tsx Normal file
View File

@ -0,0 +1,263 @@
interface pageData {
title: string,
date: string,
permalink: string,
content: string,
image?: string,
preview: string,
matchCount: number
}
/**
* Escape HTML tags as HTML entities
* Edited from:
* @link https://stackoverflow.com/a/5499821
*/
const tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'…': '&hellip;'
};
function replaceTag(tag) {
return tagsToReplace[tag] || tag;
}
function replaceHTMLEnt(str) {
return str.replace(/[&<>"]/g, replaceTag);
}
function escapeRegExp(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
class Search {
private data: pageData[];
private form: HTMLFormElement;
private input: HTMLInputElement;
private list: HTMLDivElement;
private resultTitle: HTMLHeadElement;
private resultTitleTemplate: string;
constructor({ form, input, list, resultTitle, resultTitleTemplate }) {
this.form = form;
this.input = input;
this.list = list;
this.resultTitle = resultTitle;
this.resultTitleTemplate = resultTitleTemplate;
this.handleQueryString();
this.bindQueryStringChange();
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
});
for (const item of rawData) {
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)} `;
}
}
}
if (matched) {
result.preview += '[...]';
results.push(result);
}
}
/** 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();
const results = await this.searchKeywords(keywords);
this.clear();
for (const item of results) {
this.list.append(Search.render(item));
}
const endTime = performance.now();
this.resultTitle.innerText = this.generateResultTitle(results.length, ((endTime - startTime) / 1000).toPrecision(1));
}
private generateResultTitle(resultLen, time) {
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
}
public async getData() {
if (!this.data) {
/// Not fetched yet
const jsonURL = this.form.dataset.json;
this.data = await fetch(jsonURL).then(res => res.json());
}
return this.data;
}
private bindSearchForm() {
let lastSearch = '';
const eventHandler = (e) => {
e.preventDefault();
const keywords = this.input.value;
Search.updateQueryString(keywords, true);
if (keywords === '') {
return this.clear();
}
if (lastSearch === keywords) return;
lastSearch = keywords;
this.doSearch(keywords.split(' '));
}
this.input.addEventListener('input', eventHandler);
this.input.addEventListener('compositionend', eventHandler);
}
private clear() {
this.list.innerHTML = '';
this.resultTitle.innerText = '';
}
private bindQueryStringChange() {
window.addEventListener('popstate', (e) => {
this.handleQueryString()
})
}
private handleQueryString() {
const pageURL = new URL(window.location.toString());
const keywords = pageURL.searchParams.get('keyword');
this.input.value = keywords;
if (keywords) {
this.doSearch(keywords.split(' '));
}
else {
this.clear()
}
}
private static updateQueryString(keywords: string, replaceState = false) {
const pageURL = new URL(window.location.toString());
if (keywords === '') {
pageURL.searchParams.delete('keyword')
}
else {
pageURL.searchParams.set('keyword', keywords);
}
if (replaceState) {
window.history.replaceState('', '', pageURL.toString());
}
else {
window.history.pushState('', '', pageURL.toString());
}
}
public static render(item: pageData) {
return <article>
<a href={item.permalink}>
<div class="article-details">
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
<secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion>
</div>
{item.image &&
<div class="article-image">
<img src={item.image} loading="lazy" />
</div>
}
</a>
</article>;
}
}
declare global {
interface Window {
searchResultTitleTemplate: string;
}
}
window.addEventListener('load', () => {
setTimeout(function () {
const searchForm = document.querySelector('.search-form') as HTMLFormElement,
searchInput = searchForm.querySelector('input') as HTMLInputElement,
searchResultList = document.querySelector('.search-result--list') as HTMLDivElement,
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
new Search({
form: searchForm,
input: searchInput,
list: searchResultList,
resultTitle: searchResultTitle,
resultTitleTemplate: window.searchResultTitleTemplate
});
}, 0);
})
export default Search;

View File

@ -1,92 +0,0 @@
baseurl = "https://example.com"
languageCode = "en-us"
theme = "hugo-theme-stack"
paginate = 5
title = "Example Site"
disqusShortname = "hugo-theme-stack" # Change it to your Disqus shortname before using
DefaultContentLanguage = "en" # Theme i18n support
[permalinks]
post = "/p/:slug/"
page = "/:slug/"
[params]
mainSections = ["post"]
featuredImageField = "image"
[params.dateFormat]
published = "Jan 02, 2006"
lastUpdated = "Jan 02, 2006 15:04 MST"
[params.sidebar]
emoji = "🍥"
avatar = "img/avatar.png"
subtitle = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
[params.article]
[params.article.license]
enabled = true
default = "Licenced under CC BY-NC-SA 4.0"
[params.comments]
enabled = true
# Only Disqus is available so far
provider = "disqus"
[params.widgets]
enabled = ['archives', 'tag-cloud']
[params.widgets.archives]
limit = 5
### Archives page relative URL
path = "archives"
[params.widgets.tagCloud]
limit = 10
[params.opengraph]
[params.opengraph.twitter]
site = ""
card = "summary_large_image"
[params.defaultImage]
[params.defaultImage.article]
enabled = false
local = false
src = ""
[params.defaultImage.articleList]
enabled = false
local = true
src = ""
[params.defaultImage.opengraph]
enabled = false
local = false
src = ""
[menu]
[[menu.main]]
identifier = "home"
name = "Home"
url = "/"
weight = -100
pre = "home"
[[menu.main]]
identifier = "about-cn"
name = "About"
url = "about"
weight = -90
pre = "user"
[[menu.main]]
identifier = "archives"
name = "Archives"
url = "archives"
weight = -70
pre = "archives"
[related]
includeNewer = true
threshold = 60
toLower = false
[[related.indices]]
name = "tags"
weight = 100
[[related.indices]]
name = "categories"
weight = 200
[markup]
[markup.highlight]
noClasses = false

113
exampleSite/config.yaml Normal file
View File

@ -0,0 +1,113 @@
baseurl: https://example.com
languageCode: en-us
theme: hugo-theme-stack
paginate: 5
title: Example Site
# Change it to your Disqus shortname before using
disqusShortname: hugo-theme-stack
# Theme i18n support
# Available values: en, fr, id, ja, ko, pt-br, zh-cn
DefaultContentLanguage: en
permalinks:
post: /p/:slug/
page: /:slug/
params:
mainSections:
- post
featuredImageField: image
rssFullContent: true
dateFormat:
published: Jan 02, 2006
lastUpdated: Jan 02, 2006 15:04 MST
sidebar:
emoji: 🍥
subtitle: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
avatar:
local: true
src: img/avatar.png
article:
math: false
license:
enabled: true
default: Licensed under CC BY-NC-SA 4.0
comments:
enabled: true
provider: disqus
utterances:
repo:
issueTerm: pathname
label:
theme: preferred-color-scheme
widgets:
enabled:
- search
- archives
- tag-cloud
archives:
limit: 5
path: archives
tagCloud:
limit: 10
opengraph:
twitter:
site:
card: summary_large_image
defaultImage:
opengraph:
enabled: false
local: false
src:
menu:
main:
- identifier: home
name: Home
url: /
weight: -100
pre: home
- identifier: about
name: About
url: about
weight: -90
pre: user
- identifier: archives
name: Archives
url: archives
weight: -70
pre: archives
- identifier: search
name: Search
url: search
weight: -60
pre: search
related:
includeNewer: true
threshold: 60
toLower: false
indices:
- name: tags
weight: 100
- name: categories
weight: 200
markup:
highlight:
noClasses: false

View File

@ -5,6 +5,7 @@ date = "2019-02-28"
aliases = ["about-us", "about-hugo", "contact"]
author = "Hugo Authors"
license = "CC BY-NC-ND"
lastmod = "2020-10-09"
+++
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.

View File

@ -0,0 +1,8 @@
---
title: "Search"
slug: "search"
layout: "search"
outputs:
- html
- json
---

View File

@ -1,11 +1,12 @@
---
title: 中文文章内容测试
title: Chinese Test
description: 这是一个副标题
date: 2020-09-09
slug: test-chinese
image: helena-hertz-wWZzXlDpMog-unsplash.jpg
categories:
- Test
- 测试
---
## 正文测试

View File

@ -4,11 +4,13 @@ title = "Placeholder Text"
date = "2019-03-09"
description = "Lorem Ipsum Dolor Si Amet"
categories = [
"Test"
"Test",
"Test with whitespaces"
]
tags = [
"markdown",
"text",
"tag with whitespaces"
]
image = "matt-le-SJSpo9hQf7s-unsplash.jpg"
+++

View File

@ -13,14 +13,6 @@ Hugo ships with several [Built-in Shortcodes](https://gohugo.io/content-manageme
<!--more-->
---
## Instagram Simple Shortcode
{{< instagram_simple BGvuInzyFAe hidecaption >}}
<br>
---
## YouTube Privacy Enhanced Shortcode
{{< youtube ZJthWmvUzzc >}}

View File

@ -1,26 +0,0 @@
[toggleMenu]
other = "Toggle Menu"
[relatedContents]
other = "Related contents"
[lastUpdatedOn]
other ="Last updated on {{ .Count }}"
[widgetArchivesTitle]
other = "Archives"
[widgetArchivesMore]
other = "More"
[widgetTagCloudTitle]
other = "Tags"
[notFoundTitle]
other = "Not Found"
[notFoundSubtitle]
other = "This page does not exist."
[darkModeToggle]
other = "Dark Mode"

46
i18n/en.yaml Normal file
View File

@ -0,0 +1,46 @@
toggleMenu:
other: Toggle Menu
darkMode:
toggle:
other: Dark Mode
archives:
categories:
other: Categories
article:
relatedContents:
other: Related contents
lastUpdatedOn:
other: Last updated on
notFound:
title:
other: Not Found
subtitle:
other: This page does not exist.
widget:
archives:
title:
other: Archives
more:
other: More
tagCloud:
title:
other: Tags
search:
title:
other: Search
placeholder:
other: Type something...
resultTitle:
other: "#PAGES_COUNT pages (#TIME_SECONDS seconds)"
footer:
builtWith:
other: Built with {{ .Generator }}
designedBy:
other: Theme {{ .Theme }} designed by {{ .DesignedBy }}

38
i18n/fr.yaml Normal file
View File

@ -0,0 +1,38 @@
toggleMenu:
other: Afficher le menu
article:
relatedContents:
other: Contenus liés
lastUpdatedOn:
other: Dernière mise à jour le
notFound:
title:
other: Page non trouvée
subtitle:
other: Cette page n'existe pas.
widget:
archives:
title:
other: Archives
more:
other: Autres
tagCloud:
title:
other: Mots clés
search:
title:
other: Rechercher
placeholder:
other: Cherchez un article, une publication, etc.
resultTitle:
other: "#PAGES_COUNT pages (#TIME_SECONDS secondes)"
footer:
builtWith:
other: Généré avec {{ .Generator }}
designedBy:
other: Thème {{ .Theme }} conçu par {{ .DesignedBy }}

38
i18n/id.yaml Normal file
View File

@ -0,0 +1,38 @@
toggleMenu:
other: Tampilkan Menu
article:
relatedContents:
other: Konten terkait
lastUpdatedOn:
other: Terakhir diperbarui pada
notFound:
title:
other: Not Found
subtitle:
other: Halaman ini tidak ada.
widget:
archives:
title:
other: Arsip
more:
other: Lebih
tagCloud:
title:
other: Tag
search:
title:
other: Cari
placeholder:
other: Ketik sesuatu...
resultTitle:
other: "#PAGES_COUNT halaman (#TIME_SECONDS detik)"
footer:
builtWith:
other: Dibangun dengan {{ .Generator }}
designedBy:
other: Tema {{ .Theme }} dirancang oleh {{ .DesignedBy }}

24
i18n/ja.yaml Normal file
View File

@ -0,0 +1,24 @@
toggleMenu:
other: メニューを開く・閉じる
article:
relatedContents:
other: 関連するコンテンツ
lastUpdatedOn:
other: 最終更新
notFound:
title:
other: 404 Not Found
subtitle:
other: 指定されたページは存在しません。
widget:
archives:
title:
other: アーカイブ
more:
other: さらに見る
tagCloud:
title:
other: タグ

38
i18n/ko.yaml Normal file
View File

@ -0,0 +1,38 @@
toggleMenu:
other: 메뉴 여닫기
article:
relatedContents:
other: 관련 글
lastUpdatedOn:
other: "마지막 수정: "
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: "{{ .DesignedBy }}의 {{ .Theme }} 테마 사용 중"

42
i18n/pt-BR.yaml Normal file
View File

@ -0,0 +1,42 @@
toggleMenu:
other: Alternar Menu
archives:
categories:
other: Categorias
article:
relatedContents:
other: Conteúdos Relacionados
lastUpdatedOn:
other: Última atualização em
notFound:
title:
other: Não Encontrado
subtitle:
other: Esta página não existe.
widget:
archives:
title:
other: Arquivos
more:
other: Mais
tagCloud:
title:
other: Tags
search:
title:
other: Busca
placeholder:
other: Digite algo...
resultTitle:
other: "#PAGES_COUNT páginas (#TIME_SECONDS segundos)"
footer:
builtWith:
other: Criado com {{ .Generator }}
designedBy:
other: Tema {{ .Theme }} desenvolvido por {{ .DesignedBy }}

View File

@ -1,26 +0,0 @@
[toggleMenu]
other = "切换菜单"
[relatedContents]
other = "相关文章"
[lastUpdatedOn]
other ="最后更新于 {{ .Count }}"
[widgetArchivesTitle]
other = "归档"
[widgetArchivesMore]
other = "更多"
[widgetTagCloudTitle]
other = "标签云"
[notFoundTitle]
other = "404 错误"
[notFoundSubtitle]
other = "页面不存在"
[darkModeToggle]
other = "暗色模式"

40
i18n/zh-CN.yaml Normal file
View File

@ -0,0 +1,40 @@
toggleMenu:
other: 切换菜单
darkMode:
toggle:
other: 暗色模式
archives:
categories:
other: 分类
article:
relatedContents:
other: 相关文章
lastUpdatedOn:
other: 最后更新于
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 秒)"

View File

@ -1,7 +1,7 @@
{{ define "main" }}
<div class="not-found-card">
<h1 class="article-title">{{ T "notFoundTitle" }}</h1>
<h2 class="article-subtitle">{{ T "notFoundSubtitle" }}</h2>
<h1 class="article-title">{{ T "notFound.title" }}</h1>
<h2 class="article-subtitle">{{ T "notFound.subtitle" }}</h2>
</div>
{{ partialCached "footer/footer" . }}
{{ end }}

View File

@ -2,14 +2,12 @@
{{ define "main" }}
{{ $categories := ($.Site.GetPage "taxonomyTerm" "categories").Pages }}
{{ if $categories }}
<div class="widget">
<h1 class="widget-title">Categories</h1>
<div class="category-list">
<div class="article-list--tile">
{{ range $categories }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
{{ end }}
</div>
<h2 class="section-title">{{ T "archives.categories" }}</h2>
<div class="category-list">
<div class="article-list--tile">
{{ range $categories }}
{{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
{{ end }}
</div>
</div>
{{ end }}
@ -21,7 +19,7 @@
{{ range $filtered.GroupByDate "2006" }}
{{ $id := lower (replace .Key " " "-") }}
<div class="archives-group" id="{{ $id }}">
<h3 class="archives-date"><a href="{{ $.Permalink }}#{{ $id }}">{{ .Key }}</a></h3>
<h2 class="archives-date section-title"><a href="{{ $.RelPermalink }}#{{ $id }}">{{ .Key }}</a></h2>
<div class="article-list--compact">
{{ range .Pages }}
{{ partial "article-list/compact" . }}

View File

@ -1,6 +1,9 @@
<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">
{{- partial "head/head.html" . -}}
<head>
{{- partial "head/head.html" . -}}
{{- block "head" . -}}{{ end }}
</head>
<body class="{{ block `body-class` . }}{{ end }}">
<script>
(function() {
@ -15,7 +18,7 @@
})();
</script>
<div class="container extended flex on-phone--column align-items--flex-start {{ block `container-class` . }}{{end}}">
<div class="container flex on-phone--column align-items--flex-start {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }} {{ block `container-class` . }}{{end}}">
{{ partial "sidebar/left.html" . }}
<main class="main full-width">
{{- block "main" . }}{{- end }}

View File

@ -1,5 +1,5 @@
{{ define "main" }}
<h3 class="taxonomy-type">{{ .Type | singularize | humanize }}</h3>
<h3 class="taxonomy-type section-title">{{ .Type | singularize | humanize }}</h3>
<div class="taxonomy-card">
<div class="taxonomy-details">
<h3 class="taxonomy-count">{{ len .Pages }} post{{ if gt (len .Pages) 1 }}s{{ end }}</h3>

View File

@ -15,5 +15,5 @@
{{ end }}
{{ define "right-sidebar" }}
{{ partialCached "sidebar/right.html" . }}
{{ partial "sidebar/right.html" . }}
{{ end }}

31
layouts/page/search.html Normal file
View File

@ -0,0 +1,31 @@
{{ define "body-class" }}template-search{{ end }}
{{ define "head" }}
{{- with .OutputFormats.Get "json" -}}
<link rel="preload" href="{{ .Permalink }}" as="fetch" crossorigin="anonymous">
{{- end -}}
{{ end }}
{{ define "main" }}
<form action="{{ .Permalink }}" class="search-form"{{ with .OutputFormats.Get "json" -}} data-json="{{ .Permalink }}"{{- end }}>
<p>
<label>{{ T "search.title" }}</label>
<input name="keyword" placeholder="{{ T `search.placeholder` }}" />
</p>
<button title="Search">
{{ partial "helper/icon" "search" }}
</button>
</form>
<h3 class="search-result--title section-title"></h3>
<div class="search-result--list article-list--compact"></div>
<script>
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"
</script>
{{- $opts := dict "minify" hugo.IsProduction "JSXFactory" "createElement" -}}
{{- $searchScript := resources.Get "ts/search.tsx" | js.Build $opts -}}
<script type="text/javascript" src="{{ $searchScript.RelPermalink }}" defer></script>
{{ partialCached "footer/footer" . }}
{{ end }}

20
layouts/page/search.json Normal file
View File

@ -0,0 +1,20 @@
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
{{- $notHidden := where .Site.RegularPages "Params.hidden" "!=" true -}}
{{- $filtered := ($pages | intersect $notHidden) -}}
{{- $result := slice -}}
{{- range $filtered -}}
{{- $data := dict "title" .Title "date" .Date "permalink" .Permalink "content" (.Plain) -}}
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}}
{{- if and $image.exists $image.resource -}}
{{- $thumbnail := $image.resource.Fill "120x120" -}}
{{- $image := dict "image" (absURL $thumbnail.Permalink) -}}
{{- $data = merge $data $image -}}
{{ end }}
{{- $result = $result | append $data -}}
{{- end -}}
{{ jsonify $result }}

View File

@ -1,27 +1,27 @@
<article>
<div class="article-details">
<h2 class="article-title">
<a href="{{ .Permalink }}">
<a href="{{ .RelPermalink }}">
<div class="article-details">
<h2 class="article-title">
{{- .Title -}}
</a>
</h2>
<footer class="article-time">
<time datetime='{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}'>
{{- .Date.Format (or .Site.Params.dateFormat.published "Jan 02, 2006") -}}
</time>
</footer>
</div>
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}}
{{ if $image.exists }}
<div class="article-image">
{{ if $image.resource }}
{{- $thumbnail := $image.resource.Fill "120x120" -}}
<img src="{{ $thumbnail.RelPermalink }}" width="{{ $thumbnail.Width }}"
height="{{ $thumbnail.Height }}" loading="lazy">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" alt="Featured image of post {{ .Title }}" />
{{ end }}
</h2>
<footer class="article-time">
<time datetime='{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}'>
{{- .Date.Format (or .Site.Params.dateFormat.published "Jan 02, 2006") -}}
</time>
</footer>
</div>
{{ end }}
{{- $image := partialCached "helper/image" (dict "Context" . "Type" "articleList") .RelPermalink "articleList" -}}
{{ if $image.exists }}
<div class="article-image">
{{ if $image.resource }}
{{- $thumbnail := $image.resource.Fill "120x120" -}}
<img src="{{ $thumbnail.RelPermalink }}" width="{{ $thumbnail.Width }}"
height="{{ $thumbnail.Height }}" loading="lazy">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" alt="Featured image of post {{ .Title }}" />
{{ end }}
</div>
{{ end }}
</a>
</article>

View File

@ -2,7 +2,7 @@
<article class="{{ if $image.exists }}has-image{{ end }}">
{{ if $image.exists }}
<div class="article-image">
<a href="{{ .Permalink }}">
<a href="{{ .RelPermalink }}">
{{ if $image.resource }}
{{- $thumbnail := $image.resource.Fill "800x250" -}}
{{- $thumbnailRetina := $image.resource.Fill "1600x500" -}}

View File

@ -1,6 +1,6 @@
{{ $image := partialCached "helper/image" (dict "Context" .context "Type" .Type) .context.RelPermalink .Type }}
<article class="{{ if $image.exists }}has-image{{ end }}">
<a href="{{ .context.Permalink }}">
<a href="{{ .context.RelPermalink }}">
{{ if $image.exists }}
<div class="article-image">

View File

@ -4,4 +4,8 @@
{{ partial "article/components/content" . }}
{{ partial "article/components/footer" . }}
{{ if or .Params.math .Site.Params.article.math }}
{{ partialCached "article/components/math.html" . }}
{{ end }}
</article>

View File

@ -1,25 +1,28 @@
{{ $image := partialCached "helper/image" (dict "Context" . "Type" "article") .RelPermalink "article" }}
{{- $context := . -}}
{{- $categories := .Params.categories -}}
<div class="article-details">
{{ if $categories }}
{{ if .Params.categories }}
<header class="article-category">
{{ range $category := $categories }}
{{ $term := $.Site.GetPage (printf "/categories/%s" $category) }}
{{ range (.GetTerms "categories") }}
{{ if and $image.exists $image.resource }}
{{- $imageRaw := $image.resource | resources.Fingerprint "md5" -}}
{{- $20x := $imageRaw.Fill "20x20 smart" -}}
<a href="{{ $term.Permalink }}" class="color-tag"
data-image="{{ $20x.RelPermalink }}" data-key="{{ $context.Slug }}" data-hash="{{ $imageRaw.Data.Integrity }}">{{ $term.Title | humanize }}</a>
<a href="{{ .RelPermalink }}"
class="color-tag"
data-image="{{ $20x.RelPermalink }}"
data-key="{{ $context.Slug }}"
data-hash="{{ $imageRaw.Data.Integrity }}">
{{ .LinkTitle }}
</a>
{{ else }}
<a href="{{ $term.Permalink }}">{{ $term.Title | humanize }}</a>
<a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
{{ end }}
{{ end }}
</header>
{{ end }}
<h2 class="article-title">
<a href="{{ .Permalink }}">
<a href="{{ .RelPermalink }}">
{{- .Title -}}
</a>
</h2>
@ -32,7 +35,7 @@
{{- if not .Date.IsZero -}}
<footer class="article-time">
{{ (resources.Get "icons/clock.svg").Content | safeHTML }}
{{ partial "helper/icon" "clock" }}
<time class="article-time--published">
{{- .Date.Format (or .Site.Params.dateFormat.published "Jan 02, 2006") -}}
</time>

View File

@ -3,16 +3,16 @@
{{ if and (.Site.Params.article.license.enabled) (not (eq .Params.license false)) }}
<section class="article-copyright">
{{ (resources.Get "icons/copyright.svg").Content | safeHTML }}
{{ partial "helper/icon" "copyright" }}
<span>{{ default .Site.Params.article.license.default .Params.license }}</span>
</section>
{{ end }}
{{- if ne .Lastmod .Date -}}
<section class="article-time">
{{ (resources.Get "icons/clock.svg").Content | safeHTML }}
{{ partial "helper/icon" "clock" }}
<span class="article-time--modified">
{{ T "lastUpdatedOn" (.Lastmod.Format ( or .Site.Params.dateFormat.lastUpdated "Jan 02, 2006 15:04 MST" )) }}
{{ T "article.lastUpdatedOn" }} {{ .Lastmod.Format ( or .Site.Params.dateFormat.lastUpdated "Jan 02, 2006 15:04 MST" ) }}
</span>
</section>
{{- end -}}

View File

@ -0,0 +1,8 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css"
integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js"
integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4"
crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js"
integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa" crossorigin="anonymous"
onload="renderMathInElement(document.querySelector(`.article-content`));"></script>

View File

@ -1,7 +1,7 @@
<aside class="widget related-contents--wrapper">
{{ $related := .Site.RegularPages.Related . | first 5 }}
<aside class="related-contents--wrapper">
{{ $related := (where (.Site.RegularPages.Related .) "Params.hidden" "!=" true) | first 5 }}
{{ with $related }}
<h1 class="widget-title">{{ T "relatedContents" }}</h1>
<h2 class="section-title">{{ T "article.relatedContents" }}</h2>
<div class="related-contents">
<div class="flex article-list--tile">
{{ range . }}

View File

@ -1,10 +1,7 @@
{{- $tags := .Params.Tags -}}
{{ if $tags }}
{{ if .Params.Tags }}
<section class="article-tags">
{{ range $tag := $tags }}
{{ with $.Site.GetPage (printf "/tags/%s" $tag) }}
<a href="{{ .Permalink }}">{{ .Title | humanize }}</a>
{{ end }}
{{ range (.GetTerms "tags") }}
<a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
{{ end }}
</section>
{{ end }}

View File

@ -7,6 +7,6 @@
background-color: var(--card-background);
border-radius: var(--card-border-radius);
box-shadow: var(--shadow-l1);
padding: var(--content-padding);
padding: var(--card-padding);
}
</style>

View File

@ -0,0 +1,16 @@
<script src="https://utteranc.es/client.js"
repo="{{ .Site.Params.comments.utterances.repo }}"
issue-term="{{ .Site.Params.comments.utterances.issueTerm }}"
theme="{{ .Site.Params.comments.utterances.theme }}"
{{ with .Site.Params.comments.utterances.label }}
label="{{ . }}"
{{ end }}
crossorigin="anonymous"
async>
</script>
<style>
.utterances {
max-width: unset;
}
</style>

View File

@ -1,4 +1,4 @@
{{- $light := resources.Get "css/highlight/light.css" | minify -}}
{{- $dark := resources.Get "css/highlight/dark.css" | minify -}}
<link rel="stylesheet" href="{{ $light.Permalink }}" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="{{ $dark.Permalink }}" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="{{ $light.RelPermalink }}" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="{{ $dark.RelPermalink }}" media="(prefers-color-scheme: dark)">

View File

@ -1,8 +1,12 @@
{{- $ThemeVersion := "1.1.0" -}}
<footer class="site-footer">
<section class="copyright">&copy; {{ now.Format "2006" }} {{ .Site.Title }}</section>
<section class="powerby">
Built with <a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a> <br />
Theme <b><a href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener">Stack</a></b> designed by
<a href="https://jimmycai.com" target="_blank" rel="noopener">Jimmy</a>
{{- $Generator := `<a href="https://gohugo.io/" target="_blank" rel="noopener">Hugo</a>` -}}
{{- $Theme := printf `<b><a href="https://github.com/CaiJimmy/hugo-theme-stack" target="_blank" rel="noopener" data-version="%s">Stack</a></b>` $ThemeVersion -}}
{{- $DesignedBy := `<a href="https://jimmycai.com" target="_blank" rel="noopener">Jimmy</a>` -}}
{{ T "footer.builtWith" (dict "Generator" $Generator) | safeHTML }} <br />
{{ T "footer.designedBy" (dict "Theme" $Theme "DesignedBy" $DesignedBy) | safeHTML }}
</section>
</footer>

View File

@ -1,22 +1,20 @@
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
{{- $description := partialCached "data/description" . .RelPermalink -}}
<meta name='description' content='{{ $description }}'>
{{- $description := partialCached "data/description" . .RelPermalink -}}
<meta name='description' content='{{ $description }}'>
{{- $title := partialCached "data/title" . .RelPermalink -}}
<title>{{ $title }}</title>
{{- $title := partialCached "data/title" . .RelPermalink -}}
<title>{{ $title }}</title>
<link rel='canonical' href='{{ .Permalink }}'>
<link rel='canonical' href='{{ .Permalink }}'>
{{- partial "head/style.html" . -}}
{{- partial "head/script.html" . -}}
{{- partial "head/opengraph/include.html" . -}}
{{- partial "head/style.html" . -}}
{{- partial "head/script.html" . -}}
{{- partial "head/opengraph/include.html" . -}}
{{- range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{- end -}}
{{- range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{- end -}}
{{- partial "head/custom.html" . -}}
</head>
{{- partial "head/custom.html" . -}}

View File

@ -1,3 +1,3 @@
{{ $sass := resources.Get "scss/style.scss" }}
{{ $style := $sass | resources.ToCSS | minify }}
<link rel="stylesheet" href="{{ $style.Permalink }}">
<link rel="stylesheet" href="{{ $style.RelPermalink }}">

View File

@ -0,0 +1,6 @@
{{- $iconFile := resources.GetMatch (printf "icons/%s.svg" .) -}}
{{- if $iconFile -}}
{{- $iconFile.Content | safeHTML -}}
{{- else -}}
{{- errorf "Error: icon '%s.svg' is not found under 'assets/icons' folder" . -}}
{{- end -}}

View File

@ -8,17 +8,23 @@
<header class="site-info">
{{ with .Site.Params.sidebar.avatar }}
<figure class="site-avatar">
{{ $avatar := resources.Get (.) }}
{{ if $avatar }}
{{ $avatarResized := $avatar.Resize "300x300" }}
<img src="{{ $avatarResized.RelPermalink }}" width="{{ $avatarResized.Width }}"
height="{{ $avatarResized.Height }}" class="site-logo" loading="lazy" alt="Avatar">
{{ if not .local }}
<img src="{{ .src }}" width="300" height="300" class="site-logo" loading="lazy" alt="Avatar">
{{ else }}
{{ errorf "Failed loading avatar from %q" . }}
{{ $avatar := resources.Get (.src) }}
{{ if $avatar }}
{{ $avatarResized := $avatar.Resize "300x" }}
<img src="{{ $avatarResized.RelPermalink }}" width="{{ $avatarResized.Width }}"
height="{{ $avatarResized.Height }}" class="site-logo" loading="lazy" alt="Avatar">
{{ else }}
{{ errorf "Failed loading avatar from %q" . }}
{{ end }}
{{ end }}
<span class="emoji">{{ $.Site.Params.sidebar.emoji }}</span>
{{ with $.Site.Params.sidebar.emoji }}
<span class="emoji">{{ . }}</span>
{{ end }}
</figure>
{{ end }}
<h1 class="site-name"><a href="{{ .Site.BaseURL }}">{{ .Site.Title }}</a></h1>
@ -31,9 +37,9 @@
{{ $active := or (eq $currentPage.Title .Name) (or ($currentPage.HasMenuCurrent "main" .) ($currentPage.IsMenuCurrent "main" .)) }}
<li {{ if $active }} class='current' {{ end }}>
<a href='{{ .URL | absLangURL }}'>
<a href='{{ .URL | relURL }}'>
{{ if .Pre }}
{{ (resources.Get (delimit (slice "icons/" .Pre ".svg") "")).Content | safeHTML }}
{{ partial "helper/icon" .Pre }}
{{ end }}
<span>{{- .Name -}}</span>
</a>
@ -43,7 +49,7 @@
<li id="dark-mode-toggle">
{{ (resources.Get "icons/toggle-left.svg").Content | safeHTML }}
{{ (resources.Get "icons/toggle-right.svg").Content | safeHTML }}
<span>{{ T "darkModeToggle" }}</span>
<span>{{ T "darkMode.toggle" }}</span>
</li>
</ol>
</aside>

View File

@ -1,29 +1,27 @@
<section class="widget archives">
<div class="widget-icon">
{{ (resources.Get "icons/infinity.svg").Content | safeHTML }}
{{ partial "helper/icon" "infinity" }}
</div>
<h1 class="widget-title">{{ T "widgetArchivesTitle" }}</h1>
<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 }}
{{ $filtered := ($pages | intersect $notHidden) }}
{{ $archives := $filtered.GroupByDate "2006" }}
{{ range first .Site.Params.widgets.archives.limit ($archives) }}
{{ $id := lower (replace .Key " " "-") }}
<div class="archives-year">
<a href="{{ $.Site.Params.widgets.archives.path | relLangURL }}#{{ $id }}">
<span class="year">{{ .Key }}</span>
<span class="count">{{ len .Pages }}</span>
</a>
</div>
{{ end }}
{{ if gt (len $archives) .Site.Params.widgets.archives.limit }}
<div class="archives-year">
<a href="{{ $.Site.Params.widgets.archives.path | relLangURL }}">
<span class="year">{{ T "widgetArchivesMore" }}</span>
</a>
</div>
{{ end }}
<div class="widget-archive--list">
{{ range $index, $item := first (add .Site.Params.widgets.archives.limit 1) ($archives) }}
{{- $id := lower (replace $item.Key " " "-") -}}
<div class="archives-year">
<a href="{{ $.Site.Params.widgets.archives.path | relLangURL }}#{{ $id }}">
{{ if eq $index $.Site.Params.widgets.archives.limit }}
<span class="year">{{ T "widget.archives.more" }}</span>
{{ else }}
<span class="year">{{ .Key }}</span>
<span class="count">{{ len $item.Pages }}</span>
{{ end }}
</a>
</div>
{{ end }}
</div>
</section>

View File

@ -0,0 +1,10 @@
<form action="/search" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>
<p>
<label>{{ T "search.title" }}</label>
<input name="keyword" required placeholder="{{ T `search.placeholder` }}" />
<button title="Search">
{{ partial "helper/icon" "search" }}
</button>
</p>
</form>

View File

@ -1,16 +1,13 @@
{{ $tags := .Site.Taxonomies.tags.ByCount }}
<section class="widget tagCloud">
<div class="widget-icon">
{{ (resources.Get "icons/tag.svg").Content | safeHTML }}
{{ partial "helper/icon" "tag" }}
</div>
<h1 class="widget-title">{{ T "widgetTagCloudTitle" }}</h1>
<h2 class="widget-title section-title">{{ T "widget.tagCloud.title" }}</h2>
<div class="tagCloud-tags">
{{ range first .Site.Params.widgets.tagCloud.limit $tags }}
{{ $term := $.Site.GetPage (printf "/tags/%s" .Term) }}
<a href="{{ $term.Permalink }}" class="font_size_{{ .Count }}">
{{ $term.Title | humanize }}
{{ range first .Site.Params.widgets.tagCloud.limit .Site.Taxonomies.tags.ByCount }}
<a href="{{ .Page.RelPermalink }}" class="font_size_{{ .Count }}">
{{ .Page.Title }}
</a>
{{ end }}
</div>

42
layouts/rss.xml Normal file
View File

@ -0,0 +1,42 @@
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
{{- $notHidden := where .Site.RegularPages "Params.hidden" "!=" true -}}
{{- $filtered := ($pages | intersect $notHidden) -}}
{{- $limit := .Site.Config.Services.RSS.Limit -}}
{{- if ge $limit 1 -}}
{{- $filtered = $filtered | 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">
<channel>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>
<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}
<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
<lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
{{- with .OutputFormats.Get "RSS" -}}
{{ printf "<atom:link href=%q rel=\"self\" type=%q />" .Permalink .MediaType | safeHTML }}
{{- end -}}
{{ range $filtered }}
{{- $content := safeHTML (.Summary | html) -}}
{{- if .Site.Params.rssFullContent -}}
{{- $content = safeHTML (.Content | html) -}}
{{- end -}}
<item>
<title>{{ .Title }}</title>
<link>{{ .Permalink }}</link>
<pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}
<guid>{{ .Permalink }}</guid>
<description>
{{- $image := partial "helper/image" (dict "Context" . "Type" "rss") -}}
{{- if $image.exists -}}
{{ "<" | html }}img src="{{ $image.permalink | absURL }}" alt="Featured image of post {{ .Title }}" {{ "/>" | html}}
{{- end -}}{{ $content }}</description>
</item>
{{ end }}
</channel>
</rss>

View File

@ -2,7 +2,7 @@
publish = "exampleSite/public"
[build.environment]
HUGO_VERSION = "0.74.3"
HUGO_VERSION = "0.79.0"
HUGO_THEME = "repo"
[context.production]

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@ name = "Stack"
license = "GPL-3.0-only"
licenselink = "https://github.com/CaiJimmy/hugo-theme-stack/blob/master/LICENSE"
description = "Card-style Hugo theme designed for bloggers"
homepage = "https://blog.jimmycai.com/p/hugo-theme-stack"
homepage = "https://theme-stack.jimmycai.com"
tags = [
"blog",
"responsive",