mirror of
https://github.com/CaiJimmy/hugo-theme-stack.git
synced 2025-04-30 04:23:30 +08:00
update local changes
This commit is contained in:
parent
d165621568
commit
90959c505b
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
[Example Site](https://theme-stack.jimmycai.com/)
|
[Example Site](https://demo.stack.jimmycai.com/)
|
||||||
|
|
||||||
[](https://app.netlify.com/sites/hugo-theme-stack/deploys)
|
[](https://app.netlify.com/sites/hugo-theme-stack/deploys)
|
||||||
|
|
||||||
@ -35,7 +35,10 @@ It's necessary to use **Hugo Extended ≥ 0.87.0**.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Clone / Download this repository to `theme` folder, and edit your site config following `exampleSite/config.yaml`.
|
* Route 1: Clone / Download this repository to `theme` folder
|
||||||
|
* Route 2: Turn your site into a hugo module and add this theme as a module dependency
|
||||||
|
|
||||||
|
Edit your site config following `exampleSite/config.yaml`.
|
||||||
|
|
||||||
*Note: Remove `config.toml` if there is one in the site folder.*
|
*Note: Remove `config.toml` if there is one in the site folder.*
|
||||||
|
|
||||||
|
@ -5,6 +5,6 @@
|
|||||||
"*": [
|
"*": [
|
||||||
"*"
|
"*"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -23,17 +23,3 @@ body {
|
|||||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||||
}
|
}
|
||||||
/**/
|
/**/
|
||||||
|
|
||||||
/* scrollbar styles for Chromium */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--scrollbar-thumb);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
/**/
|
|
||||||
|
@ -71,7 +71,8 @@
|
|||||||
text-transform: unset;
|
text-transform: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article-copyright, .article-lastmod {
|
.article-copyright,
|
||||||
|
.article-lastmod {
|
||||||
a {
|
a {
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
}
|
}
|
||||||
@ -359,6 +360,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
padding: 0 var(--card-padding);
|
||||||
|
overflow-x: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@ -407,9 +414,10 @@
|
|||||||
/// Negative margins
|
/// Negative margins
|
||||||
blockquote,
|
blockquote,
|
||||||
figure,
|
figure,
|
||||||
.gallery,
|
|
||||||
pre,
|
pre,
|
||||||
|
.gallery,
|
||||||
.video-wrapper,
|
.video-wrapper,
|
||||||
|
.table-wrapper,
|
||||||
.s_video_simple {
|
.s_video_simple {
|
||||||
margin-left: calc((var(--card-padding)) * -1);
|
margin-left: calc((var(--card-padding)) * -1);
|
||||||
margin-right: calc((var(--card-padding)) * -1);
|
margin-right: calc((var(--card-padding)) * -1);
|
||||||
|
@ -8,6 +8,7 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff;
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-scheme="dark"] {
|
[data-scheme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
--pre-text-color: #f8f8f2;
|
--pre-text-color: #f8f8f2;
|
||||||
--pre-background-color: #272822;
|
--pre-background-color: #272822;
|
||||||
@import "partials/highlight/dark.scss";
|
@import "partials/highlight/dark.scss";
|
||||||
|
@ -58,7 +58,7 @@ let Stack = {
|
|||||||
/**
|
/**
|
||||||
* Add copy button to code block
|
* Add copy button to code block
|
||||||
*/
|
*/
|
||||||
const codeBlocks = document.querySelectorAll('.article-content .highlight');
|
const codeBlocks = document.querySelectorAll('.article-content > div.highlight');
|
||||||
const copyText = `Copy`,
|
const copyText = `Copy`,
|
||||||
copiedText = `Copied!`;
|
copiedText = `Copied!`;
|
||||||
codeBlocks.forEach(codeBlock => {
|
codeBlocks.forEach(codeBlock => {
|
||||||
|
@ -8,6 +8,11 @@ interface pageData {
|
|||||||
matchCount: number
|
matchCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface match {
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape HTML tags as HTML entities
|
* Escape HTML tags as HTML entities
|
||||||
* Edited from:
|
* Edited from:
|
||||||
@ -53,77 +58,129 @@ class Search {
|
|||||||
this.bindSearchForm();
|
this.bindSearchForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async searchKeywords(keywords: string[]) {
|
/**
|
||||||
const rawData = await this.getData();
|
* Processes search matches
|
||||||
let results: pageData[] = [];
|
* @param str original text
|
||||||
|
* @param matches array of matches
|
||||||
/// Sort keywords by their length
|
* @param ellipsis whether to add ellipsis to the end of each match
|
||||||
keywords.sort((a, b) => {
|
* @param charLimit max length of preview string
|
||||||
return b.length - a.length
|
* @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) {
|
for (const item of rawData) {
|
||||||
|
const titleMatches: match[] = [],
|
||||||
|
contentMatches: match[] = [];
|
||||||
|
|
||||||
let result = {
|
let result = {
|
||||||
...item,
|
...item,
|
||||||
preview: '',
|
preview: '',
|
||||||
matchCount: 0
|
matchCount: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let matched = false;
|
const contentMatchAll = item.content.matchAll(regex);
|
||||||
|
for (const match of Array.from(contentMatchAll)) {
|
||||||
for (const keyword of keywords) {
|
contentMatches.push({
|
||||||
if (keyword === '') continue;
|
start: match.index,
|
||||||
|
end: match.index + match[0].length
|
||||||
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) {
|
const titleMatchAll = item.title.matchAll(regex);
|
||||||
return '<mark>' + match + '</mark>';
|
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
|
||||||
|
return results.sort((a, b) => {
|
||||||
|
return b.matchCount - a.matchCount;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doSearch(keywords: string[]) {
|
private async doSearch(keywords: string[]) {
|
||||||
@ -150,6 +207,11 @@ class Search {
|
|||||||
/// Not fetched yet
|
/// Not fetched yet
|
||||||
const jsonURL = this.form.dataset.json;
|
const jsonURL = this.form.dataset.json;
|
||||||
this.data = await fetch(jsonURL).then(res => res.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;
|
return this.data;
|
||||||
@ -160,7 +222,7 @@ class Search {
|
|||||||
|
|
||||||
const eventHandler = (e) => {
|
const eventHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const keywords = this.input.value;
|
const keywords = this.input.value.trim();
|
||||||
|
|
||||||
Search.updateQueryString(keywords, true);
|
Search.updateQueryString(keywords, true);
|
||||||
|
|
||||||
@ -225,7 +287,7 @@ class Search {
|
|||||||
<a href={item.permalink}>
|
<a href={item.permalink}>
|
||||||
<div class="article-details">
|
<div class="article-details">
|
||||||
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
|
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
|
||||||
<secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion>
|
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
|
||||||
</div>
|
</div>
|
||||||
{item.image &&
|
{item.image &&
|
||||||
<div class="article-image">
|
<div class="article-image">
|
||||||
|
@ -69,6 +69,10 @@ Tables aren't part of the core Markdown spec, but Hugo supports supports them ou
|
|||||||
| -------- | -------- | ------ |
|
| -------- | -------- | ------ |
|
||||||
| *italics* | **bold** | `code` |
|
| *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 Blocks
|
||||||
|
|
||||||
#### Code block with backticks
|
#### Code block with backticks
|
||||||
|
@ -4,6 +4,18 @@ toggleMenu:
|
|||||||
darkMode:
|
darkMode:
|
||||||
other: 夜晚模式
|
other: 夜晚模式
|
||||||
|
|
||||||
|
list:
|
||||||
|
page:
|
||||||
|
one: "{{ .Count }} 個頁面"
|
||||||
|
other: "{{ .Count }} 個頁面"
|
||||||
|
|
||||||
|
section:
|
||||||
|
other: Section
|
||||||
|
|
||||||
|
subsection:
|
||||||
|
one: Subsection
|
||||||
|
other: Subsections
|
||||||
|
|
||||||
article:
|
article:
|
||||||
back:
|
back:
|
||||||
other: 返回
|
other: 返回
|
||||||
@ -18,13 +30,15 @@ article:
|
|||||||
other: 最後更新
|
other: 最後更新
|
||||||
|
|
||||||
readingTime:
|
readingTime:
|
||||||
|
one: "閱讀時間: {{ .Count }} 分鐘"
|
||||||
other: "閱讀時間: {{ .Count }} 分鐘"
|
other: "閱讀時間: {{ .Count }} 分鐘"
|
||||||
|
|
||||||
notFound:
|
notFound:
|
||||||
title:
|
title:
|
||||||
other: 404 錯誤
|
other: 404 錯誤:(
|
||||||
|
|
||||||
subtitle:
|
subtitle:
|
||||||
other: 頁面不存在
|
other: 頁面並不存在:(
|
||||||
|
|
||||||
widget:
|
widget:
|
||||||
archives:
|
archives:
|
||||||
@ -43,7 +57,14 @@ search:
|
|||||||
other: 搜尋
|
other: 搜尋
|
||||||
|
|
||||||
placeholder:
|
placeholder:
|
||||||
other: 輸入關鍵字...
|
other: 請輸入關鍵字...
|
||||||
|
|
||||||
resultTitle:
|
resultTitle:
|
||||||
other: "#PAGES_COUNT 個結果 (用時 #TIME_SECONDS 秒)"
|
other: "#PAGES_COUNT 個結果 (花了 #TIME_SECONDS 秒)"
|
||||||
|
|
||||||
|
footer:
|
||||||
|
builtWith:
|
||||||
|
other: 使用 {{ .Generator }} 建造
|
||||||
|
|
||||||
|
designedBy:
|
||||||
|
other: 自豪的使用由 {{ .DesignedBy }} 設計的 {{ .Theme }}
|
@ -1,3 +1,5 @@
|
|||||||
<section class="article-content">
|
<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>
|
</section>
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
data-reactions-enabled="{{- default 1 .reactionsEnabled -}}"
|
data-reactions-enabled="{{- default 1 .reactionsEnabled -}}"
|
||||||
data-emit-metadata="{{- default 0 .emitMetadata -}}"
|
data-emit-metadata="{{- default 0 .emitMetadata -}}"
|
||||||
data-theme="{{- default `light` .lightTheme -}}"
|
data-theme="{{- default `light` .lightTheme -}}"
|
||||||
|
data-lang="zh-TW"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
async
|
async
|
||||||
></script>
|
></script>
|
||||||
|
@ -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.15/dist/twikoo.all.min.js"></script>
|
||||||
<div id="tcomment"></div>
|
<div id="tcomment"></div>
|
||||||
<style>
|
<style>
|
||||||
.twikoo {
|
.twikoo {
|
||||||
|
@ -4,3 +4,9 @@
|
|||||||
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}
|
{{- $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 -}}
|
@ -1,4 +1,4 @@
|
|||||||
{{- $ThemeVersion := "3.6.0" -}}
|
{{- $ThemeVersion := "3.7.0" -}}
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<section class="copyright">
|
<section class="copyright">
|
||||||
©
|
©
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
{{ with .Site.Params.sidebar.avatar }}
|
{{ with .Site.Params.sidebar.avatar }}
|
||||||
{{ if (default true .enabled) }}
|
{{ if (default true .enabled) }}
|
||||||
<figure class="site-avatar">
|
<figure class="site-avatar">
|
||||||
|
<a href="{{ .Site.BaseURL | relLangURL }}">
|
||||||
{{ if not .local }}
|
{{ if not .local }}
|
||||||
<img src="{{ .src }}" width="300" height="300" class="site-logo" loading="lazy" alt="Avatar">
|
<img src="{{ .src }}" width="300" height="300" class="site-logo" loading="lazy" alt="Avatar">
|
||||||
{{ else }}
|
{{ else }}
|
||||||
@ -22,7 +23,7 @@
|
|||||||
{{ errorf "Failed loading avatar from %q" . }}
|
{{ errorf "Failed loading avatar from %q" . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
</a>
|
||||||
{{ with $.Site.Params.sidebar.emoji }}
|
{{ with $.Site.Params.sidebar.emoji }}
|
||||||
<span class="emoji">{{ . }}</span>
|
<span class="emoji">{{ . }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
Loading…
Reference in New Issue
Block a user