Compare commits

...
24 Commits
Author SHA1 Message Date
Jimmy CaiandGitHub 9f36a956ca release: 3.8.0 2022-02-06 19:36:10 +00:00
Jimmy CaiandGitHub d75dbe2b6e fix: copy code button does not work when line number is enabled (#487)
* fix: copy code button does not work when line number is enabled

* fix pre style

* Add gist shortcode to exampleSite
2022-02-06 20:32:37 +01:00
Jimmy CaiandGitHub 88beecd101 fix: hyperlinked image (#485)
* fix: hyperlinked image

closes https://github.com/CaiJimmy/hugo-theme-stack/issues/410

* feat: add support to inline images

* Remove unused code

* Remove data-alt-html
2022-02-06 19:58:10 +01:00
Jimmy CaiandGitHub 67d5156507 fix: disqus dark mode (#478)
* fix: disqus dark mode

* remove color-scheme

add custom chrome scrollbar style (removed in https://github.com/CaiJimmy/hugo-theme-stack/pull/428)
2022-01-30 10:57:16 +01:00
Jimmy CaiandGitHub 6474b9dfd8 fix(comments): fix cactus comments
closes https://github.com/CaiJimmy/hugo-theme-stack/issues/470
2022-01-23 12:59:21 +00:00
MarcelandGitHub 7dc5def217 feat(comments): add https://cactus.chat support (#298)
* feat: Add https://cactus.chat as comments provider

* Add cactus comments config fields to exampleSite

* Add cactus css and js to external.yaml and move custom cactus css to html

* Remove unneeded cactus comments css

* Load external dependencies for cactus comments in the comments provider

* Use safeJS instead of safeHTML
2022-01-23 13:10:00 +01:00
ZoroarkandGitHub 2b40a32d47 feat(article): Scrollspy for the table of contents (#425)
* Add first try at scrollspy (broken right now)

* Scrollspy actually works now

* Fix VS Code errors by setting JS version

* Recompute offsets when window size changes

* Improve list compatibility for toc active selection

Support up to 6 levels of indentation, properly support <ol>

* Remove debug string

* Add more docs in smoothAnchors

* Use a map to match ids to navigation elements
2022-01-22 10:35:08 +01:00
Jimmy CaiandGitHub 9e08854681 release: 3.7.0 2022-01-20 11:12:16 +00:00
zhixuanandGitHub 0af9d23e49 feat(article): add scroll for table (#428)
* feat: table can't scroll & delete scrollbar style for Chromium

* Undo some line breaks

* Add a table with overflow to exampleSite
2022-01-20 12:09:25 +01:00
Jimmy CaiandGitHub 3b98d3ba05 feat(sidebar): link avatar to homepage (#465)
closes https://github.com/CaiJimmy/hugo-theme-stack/issues/399
2022-01-20 11:38:16 +01:00
Andreas DeiningerandGitHub fe661c8c38 feat: turn stack theme into a hugo module (#460)
* Turn stack theme into a hugo module
2022-01-20 11:16:28 +01:00
bfd1eb8400 feat: add custom.ts (#461)
* feat: add custom.ts

* Load custom.ts only if it exists

This change avoids loading an empty .js file

Co-authored-by: Jimmy Cai <github@jimmycai.com>
2022-01-19 16:35:15 +01:00
tanmxandGitHub eebaea81e8 feat(comment): update twikoo version to 1.4.15 (#464) 2022-01-19 15:22:51 +01:00
zhixuanandGitHub ca260db07e fix(shortcode): conflict with gist shortcode (#455) 2022-01-19 01:12:29 +01:00
Jimmy CaiandGitHub 4764a92df3 refactor(search): avoid issue with one character keyword (#447)
* refactor(search): avoid issue with one character keyword

closes https://github.com/CaiJimmy/hugo-theme-stack/issues/184

* Remove keyword sorts

* fix typo: secion -> section

* fix(search): avoid matching html entity

* Use | operator to concatenate keywords

Idea from https://github.com/CaiJimmy/hugo-theme-stack/pull/436

* Add missing `matchCount`

* Limit preview length

* Don't add ellipsis to title

* add comment to `processMatches`

* Initialize DOMParser only once

* Remove marker function

* Deal with blank search

* Use const keyword for constant arrays
2022-01-18 23:19:30 +01:00
Jimmy CaiandGitHub 86cbc1b682 fix(README): demo site link
closes #449
2022-01-03 14:15:05 +01:00
Jimmy CaiandGitHub d165621568 release: 3.6.0 2021-12-29 00:32:08 +01:00
Nathan HarkenriderandGitHub dde6a10cab feat: implement cache busting for CSS (#444) 2021-12-28 23:28:33 +01:00
Jimmy CaiandGitHub a669b60cdd chore(exampleSite): make default pages use page bundle feature
To avoid confusions
2021-12-28 10:33:48 +00:00
Jimmy CaiandGitHub 455e23f22b feat: page links (#446) 2021-12-28 11:28:20 +01:00
Jimmy CaiandGitHub 4e2c8e74be fix: add missing diff syntax highlight (#445)
closes: https://github.com/CaiJimmy/hugo-theme-stack/issues/434
2021-12-28 10:48:22 +01:00
HAHWULandGitHub 359e4b34c1 Update i18n(ko) (#418)
Add dark mode, list-section, list-subsction, article-back, article-tableOfContents, article-readingTime
2021-12-04 21:16:22 +01:00
Paul-Henri ZimmerlinandGitHub 195b8d8076 fix(i18n/fr): add missing translations for reading time (#416) 2021-12-04 21:10:42 +01:00
zhixuanandGitHub 497643b363 feat: add alt message for archive & related contents images (#411) 2021-12-04 21:05:53 +01:00
38 changed files with 757 additions and 230 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
public
resources
assets/jsconfig.json
assets/jsconfig.json
.hugo_build.lock
+6 -3
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.*
@@ -77,4 +80,4 @@ Some references that I took while building this theme:
| Project | Licence|
| ------- | ------|
| [artchen/hexo-theme-element](https://github.com/artchen/hexo-theme-element) | [MIT](https://github.com/artchen/hexo-theme-element/blob/master/LICENSE) |
| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) |
| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) |
+2
View File
@@ -6,5 +6,7 @@
"*"
]
},
"lib": ["es2020", "dom"],
"jsx": "preserve"
}
}
-1
View File
@@ -1,7 +1,6 @@
html {
font-size: 62.5%;
overflow-y: scroll;
scroll-behavior: smooth;
}
* {
+93 -90
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 {
}
+84 -32
View File
@@ -71,7 +71,8 @@
text-transform: unset;
}
.article-copyright, .article-lastmod {
.article-copyright,
.article-lastmod {
a {
color: var(--body-text-color);
}
@@ -122,8 +123,6 @@
}
.article-page.has-toc {
scroll-behavior: smooth;
.left-sidebar {
display: none;
}
@@ -193,6 +192,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 +210,7 @@
list-style-type: none;
counter-reset: item;
li:before {
li a::before {
counter-increment: item;
content: counters(item, ".") ". ";
font-weight: bold;
@@ -220,7 +223,7 @@
}
li {
margin: 15px 20px;
margin: 15px 0 15px 20px;
padding: 5px;
& > ol,
@@ -234,6 +237,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 +394,47 @@
}
}
.highlight {
background-color: var(--pre-background-color);
padding: var(--card-padding);
position: relative;
&:hover {
.copyCodeButton {
opacity: 1;
}
}
pre {
margin: initial;
padding: 0;
margin: 0;
width: auto;
}
}
.copyCodeButton {
position: absolute;
top: calc(var(--card-padding));
right: calc(var(--card-padding));
background: var(--card-background);
border: none;
box-shadow: var(--shadow-l2);
border-radius: var(--tag-border-radius);
padding: 8px 16px;
color: var(--card-text-color-main);
cursor: pointer;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
}
.table-wrapper {
padding: 0 var(--card-padding);
overflow-x: auto;
display: block;
}
table {
width: 100%;
border-collapse: collapse;
@@ -407,38 +483,14 @@
/// Negative margins
blockquote,
figure,
.gallery,
.highlight,
pre,
.gallery,
.video-wrapper,
.table-wrapper,
.s_video_simple {
margin-left: calc((var(--card-padding)) * -1);
margin-right: calc((var(--card-padding)) * -1);
width: calc(100% + var(--card-padding) * 2);
}
.highlight {
position: relative;
&:hover {
.copyCodeButton {
opacity: 1;
}
}
}
.copyCodeButton {
position: absolute;
top: calc(var(--card-padding));
right: 0;
background: var(--card-background);
border: none;
box-shadow: var(--shadow-l2);
border-radius: var(--tag-border-radius);
padding: 8px 16px;
color: var(--card-text-color-main);
cursor: pointer;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s ease;
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff;
--accent-color-darker: #bdc3c7;
--accent-color-text: #000;
--body-text-color: rgba(255, 255, 255, 0.7);
--scrollbar-thumb: #424242;
--scrollbar-thumb: hsl(0, 0%, 40%);
--scrollbar-track: var(--body-background);
}
}
+52
View File
@@ -57,6 +57,58 @@ class StackGallery {
}
public static createGallery(container: HTMLElement) {
/// The process of wrapping image with figure tag is done using JavaScript instead of only Hugo markdown render hook
/// because it can not detect whether image is being wrapped by a link or not
/// and it lead to a invalid HTML construction (<a><figure><img></figure></a>)
const images = container.querySelectorAll('img');
for (const img of Array.from(images)) {
/// Images are wrapped with figure tag if the paragraph has only images without texts
/// This is done to allow inline images within paragraphs
const paragraph = img.closest('p');
if (paragraph.textContent.trim() == '') {
/// Once we insert figcaption, this check no longer works
/// So we add a class to paragraph to mark it
paragraph.classList.add('no-text');
}
let isNewLineImage = paragraph.classList.contains('no-text');
if (!isNewLineImage) continue;
const hasLink = img.parentElement.tagName == 'A';
let el: HTMLElement = img;
/// Wrap image with figure tag, with flex-grow and flex-basis values extracted from img's data attributes
const figure = document.createElement('figure');
figure.style.setProperty('flex-grow', img.getAttribute('data-flex-grow') || '1');
figure.style.setProperty('flex-basis', img.getAttribute('data-flex-basis') || '0');
if (hasLink) {
/// Wrap <a> if it exists
el = img.parentElement;
}
el.parentElement.insertBefore(figure, el);
figure.appendChild(el);
/// Add figcaption if it exists
if (img.hasAttribute('alt')) {
const figcaption = document.createElement('figcaption');
figcaption.innerText = img.getAttribute('alt');
figure.appendChild(figcaption);
}
/// Wrap img tag with <a> tag if image was not wrapped by <a> tag
if (!hasLink) {
figure.className = 'gallery-image';
const a = document.createElement('a');
a.href = img.src;
a.setAttribute('target', '_blank');
img.parentNode.insertBefore(a, img);
a.appendChild(img);
}
}
const figuresEl = container.querySelectorAll('figure.gallery-image');
let currentGallery = [];
+11 -6
View File
@@ -10,6 +10,8 @@ import { getColor } from 'ts/color';
import menu from 'ts/menu';
import createElement from 'ts/createElement';
import StackColorScheme from 'ts/colorScheme';
import { setupScrollspy } from 'ts/scrollspy';
import { setupSmoothAnchors } from "ts/smoothAnchors";
let Stack = {
init: () => {
@@ -21,6 +23,8 @@ let Stack = {
const articleContent = document.querySelector('.article-content') as HTMLElement;
if (articleContent) {
new StackGallery(articleContent);
setupSmoothAnchors();
setupScrollspy();
}
/**
@@ -58,20 +62,21 @@ let Stack = {
/**
* Add copy button to code block
*/
const codeBlocks = document.querySelectorAll('.article-content .highlight');
const highlights = document.querySelectorAll('.article-content div.highlight');
const copyText = `Copy`,
copiedText = `Copied!`;
codeBlocks.forEach(codeBlock => {
highlights.forEach(highlight => {
const copyButton = document.createElement('button');
copyButton.innerHTML = copyText;
copyButton.classList.add('copyCodeButton');
codeBlock.appendChild(copyButton);
highlight.appendChild(copyButton);
const pre = codeBlock.getElementsByTagName('pre');
const code = pre[0].textContent;
const codeBlock = highlight.querySelector('code[data-lang]');
if (!codeBlock) return;
copyButton.addEventListener('click', () => {
navigator.clipboard.writeText(code)
navigator.clipboard.writeText(codeBlock.textContent)
.then(() => {
copyButton.textContent = copiedText;
+131
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 };
+118 -56
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">
+34
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 };
+8
View File
@@ -36,3 +36,11 @@ KaTeX:
integrity: sha384-vZTG03m+2yp6N6BNi5iM4rW4oIwk5DfcNdFfxkk9ZWpDriOkXX8voJBFrAO7MpVl
type: script
defer: true
Cactus:
- src: https://latest.cactus.chat/cactus.js
integrity:
type: script
- src: https://latest.cactus.chat/style.css
integrity:
type: style
+5 -2
View File
@@ -22,6 +22,9 @@ _testmain.go
*.exe
*.test
/public
/themes
public
themes
resources
assets/jsconfig.json
.DS_Store
+24 -13
View File
@@ -104,6 +104,12 @@ params:
path:
lang:
# See https://cactus.chat/docs/reference/web-client/#configuration for description of the various options
cactus:
defaultHomeserverUrl: "https://matrix.cactus.chat:8448"
serverName: "cactus.chat"
siteName: "" # You must insert a unique identifier here matching the one you registered (See https://cactus.chat/docs/getting-started/quick-start/#register-your-site)
giscus:
repo:
repoID:
@@ -116,16 +122,15 @@ params:
emitMetadata: 0
gitalk:
owner:
admin:
repo:
clientID:
clientSecret:
cusdis:
host:
id:
owner:
admin:
repo:
clientID:
clientSecret:
cusdis:
host:
id:
widgets:
enabled:
- search
@@ -178,19 +183,19 @@ menu:
### For demonstration purpose, the home link will be open in a new tab
newTab: true
icon: home
social:
- identifier: github
name: GitHub
url: https://github.com/CaiJimmy/hugo-theme-stack
params:
icon: brand-github
icon: brand-github
- identifier: twitter
name: Twitter
url: https://twitter.com
params:
icon: brand-twitter
icon: brand-twitter
related:
includeNewer: true
@@ -214,3 +219,9 @@ markup:
startLevel: 2
highlight:
noClasses: false
codeFences: true
guessSyntax: true
lineNoStart: 1
lineNos: true
lineNumbersInTable: true
tabWidth: 4
+37
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

@@ -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
@@ -148,3 +162,7 @@ X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
Press <kbd><kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>Delete</kbd></kbd> to end the session.
Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
## Hyperlinked image
[![Google](https://www.google.com/images/branding/googlelogo/1x/googlelogo_light_color_272x92dp.png)](https://google.com)
@@ -36,3 +36,7 @@ Hugo ships with several [Built-in Shortcodes](https://gohugo.io/content-manageme
## bilibilibi Shortcode
{{< bilibili av498363026 >}}
## Gist Shortcode
{{< gist spf13 7896402 >}}
+3
View File
@@ -0,0 +1,3 @@
module github.com/CaiJimmy/hugo-theme-stack
go 1.12
+4
View File
@@ -29,6 +29,10 @@ article:
lastUpdatedOn:
other: Dernière mise à jour le
readingTime:
one: "{{ .Count }} minute de lecture"
other: "{{ .Count }} minutes de lecture"
notFound:
title:
other: Page non trouvée
+30
View File
@@ -1,15 +1,42 @@
toggleMenu:
other: 메뉴 여닫기
darkMode:
other: 다크 모드
list:
page:
one: "{{ .Count }} 페이지"
other: "{{ .Count }} 페이지"
section:
other: 섹션
subsection:
one: 서브섹션
other: 서브섹션
article:
back:
other: 뒤로가기
tableOfContents:
other: 목차
relatedContents:
other: 관련 글
lastUpdatedOn:
other: "마지막 수정: "
readingTime:
one: "{{ .Count }} 분 정도"
other: "{{ .Count }} 분 정도"
notFound:
title:
other: 찾을 수 없음
subtitle:
other: 페이지를 찾을 수 없습니다.
@@ -19,6 +46,7 @@ widget:
other: 보관함
more:
other: 더보기
tagCloud:
title:
other: 태그
@@ -26,8 +54,10 @@ widget:
search:
title:
other: 검색
placeholder:
other: 검색어를 입력하세요...
resultTitle:
other: "#PAGES_COUNT 페이지 (#TIME_SECONDS 초)"
+11 -16
View File
@@ -25,22 +25,17 @@
{{- end -}}
{{- end -}}
<figure
<img src="{{ $Permalink }}"
{{ with $Width }}width="{{ . }}"{{ end }}
{{ with $Height }}height="{{ . }}"{{ end }}
{{ with $Srcset }}srcset="{{ . }}"{{ end }}
loading="lazy"
{{ with $alt }}
alt="{{ . }}"
{{ end }}
{{ if $galleryImage }}
class="gallery-image"
style="
flex-grow: {{ div (mul $image.Width 100) $image.Height }};
flex-basis: {{ div (mul $image.Width 240) $image.Height }}px"
{{ end }}>
<a href="{{ $Permalink }}" {{ if $galleryImage }}data-size="{{ $image.Width }}x{{ $image.Height }}"{{ end }}>
<img src="{{ $Permalink }}"
{{ with $Width }}width="{{ . }}"{{ end }}
{{ with $Height }}height="{{ . }}"{{ end }}
{{ with $Srcset }}srcset="{{ . }}"{{ end }}
loading="lazy"
{{ with $alt }}alt="{{ . }}"{{ end }}>
</a>
{{ with $alt }}
<figcaption>{{ . | markdownify }}</figcaption>
data-flex-grow="{{ div (mul $image.Width 100) $image.Height }}"
data-flex-basis="{{ div (mul $image.Width 240) $image.Height }}px"
{{ end }}
</figure>
>
+4
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) }}
@@ -29,6 +29,7 @@
<img src="{{ $Permalink }}"
width="{{ $Width }}"
height="{{ $Height }}"
alt="{{ .Title }}"
loading="lazy">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" alt="Featured image of post {{ .Title }}" />
+3 -2
View File
@@ -20,8 +20,9 @@
<img src="{{ $Permalink }}"
width="{{ $Width }}"
height="{{ $Height }}"
loading="lazy"
data-key="{{ .context.Slug }}"
loading="lazy"
alt="Featured image of post {{ .context.Title }}"
{{ with .context.Slug }}data-key="{{ . }}" {{ end }}
data-hash="{{ $imageRaw.Data.Integrity }}">
{{ else }}
<img src="{{ $image.permalink }}" loading="lazy" data-key="{{ .context.Slug }}" data-hash="{{ $image.permalink }}"/>
@@ -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>
@@ -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>
@@ -0,0 +1,29 @@
{{- with .Site.Params.comments.cactus -}}
{{- partial "helper/external" (dict "Context" $ "Namespace" "Cactus") -}}
<style>
.cactus-editor-textarea {
color: var(--body-text-color);
}
.cactus-comment-header {
color: var(--card-text-color-main);
}
.cactus-message-text > p {
color: var(--body-text-color);
}
</style>
<div id="comment-section"></div>
<script>
initComments({
node: document.getElementById("comment-section"),
defaultHomeserverUrl: "{{ .defaultHomeserverUrl | safeJS }}",
serverName: "{{ .serverName }}",
siteName: "{{ .siteName }}",
commentSectionId: "{{ $.File.UniqueID }}"
})
</script>
{{- end -}}
@@ -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 {
@@ -3,4 +3,10 @@
{{- $opts := dict "minify" hugo.IsProduction -}}
{{- $script := resources.Get "ts/main.ts" | js.Build $opts -}}
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
<script type="text/javascript" src="{{ $script.RelPermalink }}" defer></script>
{{- with resources.Get "ts/custom.ts" -}}
{{/* Place your custom script in HUGO_SITE_FOLDER/assets/ts/custom.ts */}}
{{- $customScript := . | js.Build $opts -}}
<script type="text/javascript" src="{{ $customScript.RelPermalink }}" defer></script>
{{- end -}}
+1 -1
View File
@@ -1,4 +1,4 @@
{{- $ThemeVersion := "3.5.0" -}}
{{- $ThemeVersion := "3.8.0" -}}
<footer class="site-footer">
<section class="copyright">
&copy;
+1 -1
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 }}">
+1 -1
View File
@@ -8,7 +8,7 @@
integrity="{{ . }}"
{{- end -}}
crossorigin="anonymous"
defer="{{ default false .defer }}"
{{ if .defer }}defer{{ end }}
>
</script>
{{- else if eq .type "style" -}}
+2 -1
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 }}