Merge branch 'master' into MTRNord/cactus

This commit is contained in:
Jimmy Cai 2022-01-23 12:58:45 +01:00 committed by GitHub
commit d6ab5ed3c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 539 additions and 177 deletions

3
.gitignore vendored
View File

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

View File

@ -5,7 +5,7 @@
## Demo ## 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) [![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)
@ -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.*
@ -77,4 +80,4 @@ Some references that I took while building this theme:
| Project | Licence| | Project | Licence|
| ------- | ------| | ------- | ------|
| [artchen/hexo-theme-element](https://github.com/artchen/hexo-theme-element) | [MIT](https://github.com/artchen/hexo-theme-element/blob/master/LICENSE) | | [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

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

View File

@ -1,7 +1,6 @@
html { html {
font-size: 62.5%; font-size: 62.5%;
overflow-y: scroll; overflow-y: scroll;
scroll-behavior: smooth;
} }
* { * {
@ -23,17 +22,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;
}
/**/

View File

@ -10,7 +10,8 @@
} }
/* Other */ /* Other */
.chroma .x {} .chroma .x {
}
/* Error */ /* Error */
.chroma .err { .chroma .err {
@ -40,367 +41,369 @@
.chroma .hl { .chroma .hl {
display: block; display: block;
width: 100%; width: 100%;
background-color: #ffffcc background-color: #ffffcc;
} }
/* LineNumbersTable */ /* LineNumbersTable */
.chroma .lnt { .chroma .lnt {
margin-right: 0.4em; margin-right: 0.4em;
padding: 0 0.4em 0 0.4em; padding: 0 0.4em 0 0.4em;
color: #7f7f7f color: #7f7f7f;
} }
/* LineNumbers */ /* LineNumbers */
.chroma .ln { .chroma .ln {
margin-right: 0.4em; margin-right: 0.4em;
padding: 0 0.4em 0 0.4em; padding: 0 0.4em 0 0.4em;
color: #7f7f7f color: #7f7f7f;
} }
/* Keyword */ /* Keyword */
.chroma .k { .chroma .k {
color: #00a8c8 color: #00a8c8;
} }
/* KeywordConstant */ /* KeywordConstant */
.chroma .kc { .chroma .kc {
color: #00a8c8 color: #00a8c8;
} }
/* KeywordDeclaration */ /* KeywordDeclaration */
.chroma .kd { .chroma .kd {
color: #00a8c8 color: #00a8c8;
} }
/* KeywordNamespace */ /* KeywordNamespace */
.chroma .kn { .chroma .kn {
color: #f92672 color: #f92672;
} }
/* KeywordPseudo */ /* KeywordPseudo */
.chroma .kp { .chroma .kp {
color: #00a8c8 color: #00a8c8;
} }
/* KeywordReserved */ /* KeywordReserved */
.chroma .kr { .chroma .kr {
color: #00a8c8 color: #00a8c8;
} }
/* KeywordType */ /* KeywordType */
.chroma .kt { .chroma .kt {
color: #00a8c8 color: #00a8c8;
} }
/* Name */ /* Name */
.chroma .n { .chroma .n {
color: #111111 color: #111111;
} }
/* NameAttribute */ /* NameAttribute */
.chroma .na { .chroma .na {
color: #75af00 color: #75af00;
} }
/* NameBuiltin */ /* NameBuiltin */
.chroma .nb { .chroma .nb {
color: #111111 color: #111111;
} }
/* NameBuiltinPseudo */ /* NameBuiltinPseudo */
.chroma .bp { .chroma .bp {
color: #111111 color: #111111;
} }
/* NameClass */ /* NameClass */
.chroma .nc { .chroma .nc {
color: #75af00 color: #75af00;
} }
/* NameConstant */ /* NameConstant */
.chroma .no { .chroma .no {
color: #00a8c8 color: #00a8c8;
} }
/* NameDecorator */ /* NameDecorator */
.chroma .nd { .chroma .nd {
color: #75af00 color: #75af00;
} }
/* NameEntity */ /* NameEntity */
.chroma .ni { .chroma .ni {
color: #111111 color: #111111;
} }
/* NameException */ /* NameException */
.chroma .ne { .chroma .ne {
color: #75af00 color: #75af00;
} }
/* NameFunction */ /* NameFunction */
.chroma .nf { .chroma .nf {
color: #75af00 color: #75af00;
} }
/* NameFunctionMagic */ /* NameFunctionMagic */
.chroma .fm { .chroma .fm {
color: #111111 color: #111111;
} }
/* NameLabel */ /* NameLabel */
.chroma .nl { .chroma .nl {
color: #111111 color: #111111;
} }
/* NameNamespace */ /* NameNamespace */
.chroma .nn { .chroma .nn {
color: #111111 color: #111111;
} }
/* NameOther */ /* NameOther */
.chroma .nx { .chroma .nx {
color: #75af00 color: #75af00;
} }
/* NameProperty */ /* NameProperty */
.chroma .py { .chroma .py {
color: #111111 color: #111111;
} }
/* NameTag */ /* NameTag */
.chroma .nt { .chroma .nt {
color: #f92672 color: #f92672;
} }
/* NameVariable */ /* NameVariable */
.chroma .nv { .chroma .nv {
color: #111111 color: #111111;
} }
/* NameVariableClass */ /* NameVariableClass */
.chroma .vc { .chroma .vc {
color: #111111 color: #111111;
} }
/* NameVariableGlobal */ /* NameVariableGlobal */
.chroma .vg { .chroma .vg {
color: #111111 color: #111111;
} }
/* NameVariableInstance */ /* NameVariableInstance */
.chroma .vi { .chroma .vi {
color: #111111 color: #111111;
} }
/* NameVariableMagic */ /* NameVariableMagic */
.chroma .vm { .chroma .vm {
color: #111111 color: #111111;
} }
/* Literal */ /* Literal */
.chroma .l { .chroma .l {
color: #ae81ff color: #ae81ff;
} }
/* LiteralDate */ /* LiteralDate */
.chroma .ld { .chroma .ld {
color: #d88200 color: #d88200;
} }
/* LiteralString */ /* LiteralString */
.chroma .s { .chroma .s {
color: #d88200 color: #d88200;
} }
/* LiteralStringAffix */ /* LiteralStringAffix */
.chroma .sa { .chroma .sa {
color: #d88200 color: #d88200;
} }
/* LiteralStringBacktick */ /* LiteralStringBacktick */
.chroma .sb { .chroma .sb {
color: #d88200 color: #d88200;
} }
/* LiteralStringChar */ /* LiteralStringChar */
.chroma .sc { .chroma .sc {
color: #d88200 color: #d88200;
} }
/* LiteralStringDelimiter */ /* LiteralStringDelimiter */
.chroma .dl { .chroma .dl {
color: #d88200 color: #d88200;
} }
/* LiteralStringDoc */ /* LiteralStringDoc */
.chroma .sd { .chroma .sd {
color: #d88200 color: #d88200;
} }
/* LiteralStringDouble */ /* LiteralStringDouble */
.chroma .s2 { .chroma .s2 {
color: #d88200 color: #d88200;
} }
/* LiteralStringEscape */ /* LiteralStringEscape */
.chroma .se { .chroma .se {
color: #8045ff color: #8045ff;
} }
/* LiteralStringHeredoc */ /* LiteralStringHeredoc */
.chroma .sh { .chroma .sh {
color: #d88200 color: #d88200;
} }
/* LiteralStringInterpol */ /* LiteralStringInterpol */
.chroma .si { .chroma .si {
color: #d88200 color: #d88200;
} }
/* LiteralStringOther */ /* LiteralStringOther */
.chroma .sx { .chroma .sx {
color: #d88200 color: #d88200;
} }
/* LiteralStringRegex */ /* LiteralStringRegex */
.chroma .sr { .chroma .sr {
color: #d88200 color: #d88200;
} }
/* LiteralStringSingle */ /* LiteralStringSingle */
.chroma .s1 { .chroma .s1 {
color: #d88200 color: #d88200;
} }
/* LiteralStringSymbol */ /* LiteralStringSymbol */
.chroma .ss { .chroma .ss {
color: #d88200 color: #d88200;
} }
/* LiteralNumber */ /* LiteralNumber */
.chroma .m { .chroma .m {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberBin */ /* LiteralNumberBin */
.chroma .mb { .chroma .mb {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberFloat */ /* LiteralNumberFloat */
.chroma .mf { .chroma .mf {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberHex */ /* LiteralNumberHex */
.chroma .mh { .chroma .mh {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberInteger */ /* LiteralNumberInteger */
.chroma .mi { .chroma .mi {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberIntegerLong */ /* LiteralNumberIntegerLong */
.chroma .il { .chroma .il {
color: #ae81ff color: #ae81ff;
} }
/* LiteralNumberOct */ /* LiteralNumberOct */
.chroma .mo { .chroma .mo {
color: #ae81ff color: #ae81ff;
} }
/* Operator */ /* Operator */
.chroma .o { .chroma .o {
color: #f92672 color: #f92672;
} }
/* OperatorWord */ /* OperatorWord */
.chroma .ow { .chroma .ow {
color: #f92672 color: #f92672;
} }
/* Punctuation */ /* Punctuation */
.chroma .p { .chroma .p {
color: #111111 color: #111111;
} }
/* Comment */ /* Comment */
.chroma .c { .chroma .c {
color: #75715e color: #75715e;
} }
/* CommentHashbang */ /* CommentHashbang */
.chroma .ch { .chroma .ch {
color: #75715e color: #75715e;
} }
/* CommentMultiline */ /* CommentMultiline */
.chroma .cm { .chroma .cm {
color: #75715e color: #75715e;
} }
/* CommentSingle */ /* CommentSingle */
.chroma .c1 { .chroma .c1 {
color: #75715e color: #75715e;
} }
/* CommentSpecial */ /* CommentSpecial */
.chroma .cs { .chroma .cs {
color: #75715e color: #75715e;
} }
/* CommentPreproc */ /* CommentPreproc */
.chroma .cp { .chroma .cp {
color: #75715e color: #75715e;
} }
/* CommentPreprocFile */ /* CommentPreprocFile */
.chroma .cpf { .chroma .cpf {
color: #75715e color: #75715e;
} }
/* Generic */ /* Generic */
.chroma .g {} .chroma .g {
}
/* GenericDeleted */ /* GenericDeleted */
.chroma .gd {} .chroma .gd {
color: #f92672;
}
/* GenericEmph */ /* GenericEmph */
.chroma .ge { .chroma .ge {
font-style: italic font-style: italic;
} }
/* GenericError */ /* GenericError */
.chroma .gr {} .chroma .gr {
}
/* GenericHeading */ /* GenericHeading */
.chroma .gh {} .chroma .gh {
}
/* GenericInserted */ /* GenericInserted */
.chroma .gi {} .chroma .gi {
color: #7ca727;
}
/* GenericOutput */ /* GenericOutput */
.chroma .go {} .chroma .go {
}
/* GenericPrompt */ /* GenericPrompt */
.chroma .gp {} .chroma .gp {
}
/* GenericStrong */ /* GenericStrong */
.chroma .gs { .chroma .gs {
font-weight: bold font-weight: bold;
} }
/* GenericSubheading */ /* GenericSubheading */
.chroma .gu {} .chroma .gu {
color: #75715e;
}
/* GenericTraceback */ /* GenericTraceback */
.chroma .gt {} .chroma .gt {
}
/* GenericUnderline */ /* GenericUnderline */
.chroma .gl {} .chroma .gl {
}
/* TextWhitespace */ /* TextWhitespace */
.chroma .w {} .chroma .w {
}

View File

@ -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);
} }
@ -122,7 +123,6 @@
} }
.article-page.has-toc { .article-page.has-toc {
scroll-behavior: smooth;
.left-sidebar { .left-sidebar {
display: none; display: none;
@ -193,6 +193,10 @@
color: var(--card-text-color-main); color: var(--card-text-color-main);
overflow: hidden; overflow: hidden;
::-webkit-scrollbar-thumb {
background-color: var(--card-separator-color);
}
#TableOfContents { #TableOfContents {
overflow-x: auto; overflow-x: auto;
max-height: 75vh; max-height: 75vh;
@ -207,7 +211,7 @@
list-style-type: none; list-style-type: none;
counter-reset: item; counter-reset: item;
li:before { li a::before {
counter-increment: item; counter-increment: item;
content: counters(item, ".") ". "; content: counters(item, ".") ". ";
font-weight: bold; font-weight: bold;
@ -220,7 +224,7 @@
} }
li { li {
margin: 15px 20px; margin: 15px 0 15px 20px;
padding: 5px; padding: 5px;
& > ol, & > ol,
@ -234,6 +238,38 @@
} }
} }
} }
li.active-class > a {
border-left: var(--heading-border-size) solid var(--accent-color);
font-weight: bold;
}
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;
}
}
} }
} }
@ -359,6 +395,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 +449,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);

View File

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

View File

@ -10,6 +10,8 @@ import { getColor } from 'ts/color';
import menu from 'ts/menu'; import menu from 'ts/menu';
import createElement from 'ts/createElement'; import createElement from 'ts/createElement';
import StackColorScheme from 'ts/colorScheme'; import StackColorScheme from 'ts/colorScheme';
import { setupScrollspy } from 'ts/scrollspy';
import { setupSmoothAnchors } from "ts/smoothAnchors";
let Stack = { let Stack = {
init: () => { init: () => {
@ -21,6 +23,8 @@ let Stack = {
const articleContent = document.querySelector('.article-content') as HTMLElement; const articleContent = document.querySelector('.article-content') as HTMLElement;
if (articleContent) { if (articleContent) {
new StackGallery(articleContent); new StackGallery(articleContent);
setupSmoothAnchors();
setupScrollspy();
} }
/** /**
@ -58,7 +62,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 => {

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 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,79 +58,131 @@ 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) { const titleMatchAll = item.title.matchAll(regex);
result.preview += '[...]'; for (const match of Array.from(titleMatchAll)) {
results.push(result); 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 results.sort((a, b) => {
return b.matchCount - a.matchCount; return b.matchCount - a.matchCount;
}); });
} }
public static marker(match) {
return '<mark>' + match + '</mark>';
}
private async doSearch(keywords: string[]) { private async doSearch(keywords: string[]) {
const startTime = performance.now(); const startTime = performance.now();
@ -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">

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

@ -0,0 +1,37 @@
---
title: Links
links:
- title: GitHub
description: GitHub is the world's largest software development platform.
website: https://github.com
image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
- title: TypeScript
description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
website: https://www.typescriptlang.org
image: ts-logo-128.jpg
menu:
main:
weight: -50
params:
icon: link
comments: false
---
To use this feature, add `links` section to frontmatter.
This page's frontmatter:
```yaml
links:
- title: GitHub
description: GitHub is the world's largest software development platform.
website: https://github.com
image: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
- title: TypeScript
description: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
website: https://www.typescriptlang.org
image: ts-logo-128.jpg
```
`image` field accepts both local and external images.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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
@ -113,6 +117,16 @@ Tables aren't part of the core Markdown spec, but Hugo supports supports them ou
</html> </html>
{{< /highlight >}} {{< /highlight >}}
#### Diff code block
```diff
[dependencies.bevy]
git = "https://github.com/bevyengine/bevy"
rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13"
- features = ["dynamic"]
+ features = ["jpeg", "dynamic"]
```
## List Types ## List Types
#### Ordered List #### Ordered List

3
go.mod Normal file
View File

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

View File

@ -15,6 +15,10 @@
{{ define "main" }} {{ define "main" }}
{{ partial "article/article.html" . }} {{ partial "article/article.html" . }}
{{ if .Params.links }}
{{ partial "article/components/links" . }}
{{ end }}
{{ partial "article/components/related-contents" . }} {{ partial "article/components/related-contents" . }}
{{ if not (eq .Params.comments false) }} {{ if not (eq .Params.comments false) }}

View File

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

View File

@ -0,0 +1,26 @@
<div class="article-list--compact links">
{{ range $i, $link := .Params.links }}
<article>
<a href="{{ $link.website }}" target="_blank" rel="noopener">
<div class="article-details">
<h2 class="article-title">
{{- $link.title -}}
</h2>
<footer class="article-time">
{{ with $link.description }}
{{ . }}
{{ else }}
{{ $link.website }}
{{ end }}
</footer>
</div>
{{ with $link.image }}
<div class="article-image">
<img src="{{ . }}" loading="lazy">
</div>
{{ end }}
</a>
</article>
{{ end }}
</div>

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.15/dist/twikoo.all.min.js"></script>
<div id="tcomment"></div> <div id="tcomment"></div>
<style> <style>
.twikoo { .twikoo {

View File

@ -3,4 +3,10 @@
{{- $opts := dict "minify" hugo.IsProduction -}} {{- $opts := dict "minify" hugo.IsProduction -}}
{{- $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 -}}

View File

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

View File

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

View File

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