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

1
.gitignore vendored
View File

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

View File

@ -5,7 +5,7 @@
## 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)
@ -35,7 +35,10 @@ It's necessary to use **Hugo Extended ≥ 0.87.0**.
## Installation
Clone / Download this repository to `theme` folder, and edit your site config following `exampleSite/config.yaml`.
* Route 1: Clone / Download this repository to `theme` folder
* Route 2: Turn your site into a hugo module and add this theme as a module dependency
Edit your site config following `exampleSite/config.yaml`.
*Note: Remove `config.toml` if there is one in the site folder.*

View File

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

View File

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

View File

@ -71,7 +71,8 @@
text-transform: unset;
}
.article-copyright, .article-lastmod {
.article-copyright,
.article-lastmod {
a {
color: var(--body-text-color);
}
@ -122,7 +123,6 @@
}
.article-page.has-toc {
scroll-behavior: smooth;
.left-sidebar {
display: none;
@ -193,6 +193,10 @@
color: var(--card-text-color-main);
overflow: hidden;
::-webkit-scrollbar-thumb {
background-color: var(--card-separator-color);
}
#TableOfContents {
overflow-x: auto;
max-height: 75vh;
@ -207,7 +211,7 @@
list-style-type: none;
counter-reset: item;
li:before {
li a::before {
counter-increment: item;
content: counters(item, ".") ". ";
font-weight: bold;
@ -220,7 +224,7 @@
}
li {
margin: 15px 20px;
margin: 15px 0 15px 20px;
padding: 5px;
& > 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 {
width: 100%;
border-collapse: collapse;
@ -407,9 +449,10 @@
/// Negative margins
blockquote,
figure,
.gallery,
pre,
.gallery,
.video-wrapper,
.table-wrapper,
.s_video_simple {
margin-left: 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"] {
color-scheme: dark;
--pre-text-color: #f8f8f2;
--pre-background-color: #272822;
@import "partials/highlight/dark.scss";

View File

@ -10,6 +10,8 @@ import { getColor } from 'ts/color';
import menu from 'ts/menu';
import createElement from 'ts/createElement';
import StackColorScheme from 'ts/colorScheme';
import { setupScrollspy } from 'ts/scrollspy';
import { setupSmoothAnchors } from "ts/smoothAnchors";
let Stack = {
init: () => {
@ -21,6 +23,8 @@ let Stack = {
const articleContent = document.querySelector('.article-content') as HTMLElement;
if (articleContent) {
new StackGallery(articleContent);
setupSmoothAnchors();
setupScrollspy();
}
/**
@ -58,7 +62,7 @@ let Stack = {
/**
* Add copy button to code block
*/
const codeBlocks = document.querySelectorAll('.article-content .highlight');
const codeBlocks = document.querySelectorAll('.article-content > div.highlight');
const copyText = `Copy`,
copiedText = `Copied!`;
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
}
interface match {
start: number,
end: number
}
/**
* Escape HTML tags as HTML entities
* Edited from:
@ -53,79 +58,131 @@ class Search {
this.bindSearchForm();
}
private async searchKeywords(keywords: string[]) {
const rawData = await this.getData();
let results: pageData[] = [];
/// Sort keywords by their length
keywords.sort((a, b) => {
return b.length - a.length
/**
* Processes search matches
* @param str original text
* @param matches array of matches
* @param ellipsis whether to add ellipsis to the end of each match
* @param charLimit max length of preview string
* @param offset how many characters before and after the match to include in preview
* @returns preview string
*/
private static processMatches(str: string, matches: match[], ellipsis: boolean = true, charLimit = 140, offset = 20): string {
matches.sort((a, b) => {
return a.start - b.start;
});
let i = 0,
lastIndex = 0,
charCount = 0;
const resultArray: string[] = [];
while (i < matches.length) {
const item = matches[i];
/// item.start >= lastIndex (equal only for the first iteration)
/// because of the while loop that comes after, iterating over variable j
if (ellipsis && item.start - offset > lastIndex) {
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, lastIndex + offset))} [...] `);
resultArray.push(`${replaceHTMLEnt(str.substring(item.start - offset, item.start))}`);
charCount += offset * 2;
}
else {
/// If the match is too close to the end of last match, don't add ellipsis
resultArray.push(replaceHTMLEnt(str.substring(lastIndex, item.start)));
charCount += item.start - lastIndex;
}
let j = i + 1,
end = item.end;
/// Include as many matches as possible
/// [item.start, end] is the range of the match
while (j < matches.length && matches[j].start <= end) {
end = Math.max(matches[j].end, end);
++j;
}
resultArray.push(`<mark>${replaceHTMLEnt(str.substring(item.start, end))}</mark>`);
charCount += end - item.start;
i = j;
lastIndex = end;
if (ellipsis && charCount > charLimit) break;
}
/// Add the rest of the string
if (lastIndex < str.length) {
let end = str.length;
if (ellipsis) end = Math.min(end, lastIndex + offset);
resultArray.push(`${replaceHTMLEnt(str.substring(lastIndex, end))}`);
if (ellipsis && end != str.length) {
resultArray.push(` [...]`);
}
}
return resultArray.join('');
}
private async searchKeywords(keywords: string[]) {
const rawData = await this.getData();
const results: pageData[] = [];
const regex = new RegExp(keywords.filter((v, index, arr) => {
arr[index] = escapeRegExp(v);
return v.trim() !== '';
}).join('|'), 'gi');
for (const item of rawData) {
const titleMatches: match[] = [],
contentMatches: match[] = [];
let result = {
...item,
preview: '',
matchCount: 0
}
let matched = false;
for (const keyword of keywords) {
if (keyword === '') continue;
const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi');
const contentMatch = regex.exec(result.content);
regex.lastIndex = 0; /// Reset regex
const titleMatch = regex.exec(result.title);
regex.lastIndex = 0; /// Reset regex
if (titleMatch) {
result.title = result.title.replace(regex, Search.marker);
}
if (titleMatch || contentMatch) {
matched = true;
++result.matchCount;
let start = 0,
end = 100;
if (contentMatch) {
start = contentMatch.index - 20;
end = contentMatch.index + 80
if (start < 0) start = 0;
}
if (result.preview.indexOf(keyword) !== -1) {
result.preview = result.preview.replace(regex, Search.marker);
}
else {
if (start !== 0) result.preview += `[...] `;
result.preview += `${result.content.slice(start, end).replace(regex, Search.marker)} `;
}
}
const contentMatchAll = item.content.matchAll(regex);
for (const match of Array.from(contentMatchAll)) {
contentMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
if (matched) {
result.preview += '[...]';
results.push(result);
const titleMatchAll = item.title.matchAll(regex);
for (const match of Array.from(titleMatchAll)) {
titleMatches.push({
start: match.index,
end: match.index + match[0].length
});
}
if (titleMatches.length > 0) result.title = Search.processMatches(result.title, titleMatches, false);
if (contentMatches.length > 0) {
result.preview = Search.processMatches(result.content, contentMatches);
}
else {
/// If there are no matches in the content, use the first 140 characters as preview
result.preview = replaceHTMLEnt(result.content.substring(0, 140));
}
result.matchCount = titleMatches.length + contentMatches.length;
if (result.matchCount > 0) results.push(result);
}
/** Result with more matches appears first */
/// Result with more matches appears first
return results.sort((a, b) => {
return b.matchCount - a.matchCount;
});
}
public static marker(match) {
return '<mark>' + match + '</mark>';
}
private async doSearch(keywords: string[]) {
const startTime = performance.now();
@ -150,6 +207,11 @@ class Search {
/// Not fetched yet
const jsonURL = this.form.dataset.json;
this.data = await fetch(jsonURL).then(res => res.json());
const parser = new DOMParser();
for (const item of this.data) {
item.content = parser.parseFromString(item.content, 'text/html').body.innerText;
}
}
return this.data;
@ -160,7 +222,7 @@ class Search {
const eventHandler = (e) => {
e.preventDefault();
const keywords = this.input.value;
const keywords = this.input.value.trim();
Search.updateQueryString(keywords, true);
@ -225,7 +287,7 @@ class Search {
<a href={item.permalink}>
<div class="article-details">
<h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2>
<secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion>
<section class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></section>
</div>
{item.image &&
<div class="article-image">

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` |
| A | B | C | D | E | F |
|----------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------|
| Lorem ipsum dolor sit amet, consectetur adipiscing elit. | Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. | Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus | Proin sit amet velit nec enim imperdiet vehicula. | Ut bibendum vestibulum quam, eu egestas turpis gravida nec | Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien |
## Code Blocks
#### Code block with backticks
@ -113,6 +117,16 @@ Tables aren't part of the core Markdown spec, but Hugo supports supports them ou
</html>
{{< /highlight >}}
#### Diff code block
```diff
[dependencies.bevy]
git = "https://github.com/bevyengine/bevy"
rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13"
- features = ["dynamic"]
+ features = ["jpeg", "dynamic"]
```
## List Types
#### 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" }}
{{ partial "article/article.html" . }}
{{ if .Params.links }}
{{ partial "article/components/links" . }}
{{ end }}
{{ partial "article/components/related-contents" . }}
{{ if not (eq .Params.comments false) }}

View File

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

View File

@ -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>
<style>
.twikoo {

View File

@ -4,3 +4,9 @@
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}
<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">
<section class="copyright">
&copy;

View File

@ -1,3 +1,3 @@
{{ $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 }}">

View File

@ -9,6 +9,7 @@
{{ with .Site.Params.sidebar.avatar }}
{{ if (default true .enabled) }}
<figure class="site-avatar">
<a href="{{ .Site.BaseURL | relLangURL }}">
{{ if not .local }}
<img src="{{ .src }}" width="300" height="300" class="site-logo" loading="lazy" alt="Avatar">
{{ else }}
@ -22,7 +23,7 @@
{{ errorf "Failed loading avatar from %q" . }}
{{ end }}
{{ end }}
</a>
{{ with $.Site.Params.sidebar.emoji }}
<span class="emoji">{{ . }}</span>
{{ end }}