Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26fb06951b | ||
|
|
4d552a9df5 | ||
|
|
0fbbc12949 | ||
|
|
4b0dbb4630 | ||
|
|
ce57ea915e | ||
|
|
9f36a956ca | ||
|
|
d75dbe2b6e | ||
|
|
88beecd101 | ||
|
|
67d5156507 | ||
|
|
6474b9dfd8 | ||
|
|
7dc5def217 | ||
|
|
2b40a32d47 | ||
|
|
9e08854681 | ||
|
|
0af9d23e49 | ||
|
|
3b98d3ba05 | ||
|
|
fe661c8c38 | ||
|
|
bfd1eb8400 | ||
|
|
eebaea81e8 | ||
|
|
ca260db07e | ||
|
|
4764a92df3 | ||
|
|
86cbc1b682 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
public
|
||||
resources
|
||||
assets/jsconfig.json
|
||||
assets/jsconfig.json
|
||||
.hugo_build.lock
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## Demo
|
||||
|
||||
[Example Site](https://theme-stack.jimmycai.com/)
|
||||
[Example Site](https://demo.stack.jimmycai.com/)
|
||||
|
||||
[](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) |
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"lib": ["es2020", "dom"],
|
||||
"jsx": "preserve"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
overflow-y: scroll;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
text-transform: unset;
|
||||
}
|
||||
|
||||
.article-copyright, .article-lastmod {
|
||||
.article-copyright,
|
||||
.article-lastmod {
|
||||
a {
|
||||
color: var(--body-text-color);
|
||||
}
|
||||
@@ -121,69 +122,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.article-page.has-toc {
|
||||
scroll-behavior: smooth;
|
||||
|
||||
.left-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: none;
|
||||
|
||||
@include respond(xl) {
|
||||
display: block;
|
||||
top: var(--main-top-padding);
|
||||
}
|
||||
}
|
||||
|
||||
#article-toolbar {
|
||||
display: block;
|
||||
|
||||
@include respond(md) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@include respond(xl) {
|
||||
margin-top: 0;
|
||||
position: sticky;
|
||||
top: var(--main-top-padding);
|
||||
flex-shrink: 1;
|
||||
|
||||
a {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--body-text-color);
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
|
||||
@include respond(xl) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-top: 0;
|
||||
|
||||
@include respond(xl) {
|
||||
padding-top: var(--main-top-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget--toc {
|
||||
background-color: var(--card-background);
|
||||
border-radius: var(--card-border-radius);
|
||||
@@ -193,6 +131,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 +149,7 @@
|
||||
list-style-type: none;
|
||||
counter-reset: item;
|
||||
|
||||
li:before {
|
||||
li a::before {
|
||||
counter-increment: item;
|
||||
content: counters(item, ".") ". ";
|
||||
font-weight: bold;
|
||||
@@ -220,7 +162,7 @@
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 15px 20px;
|
||||
margin: 15px 0 15px 20px;
|
||||
padding: 5px;
|
||||
|
||||
& > ol,
|
||||
@@ -234,6 +176,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 +333,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 +422,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
display: none;
|
||||
|
||||
&.sticky {
|
||||
|
||||
+13
-13
@@ -1,18 +1,6 @@
|
||||
$defaultTagBackgrounds: #8ea885, #df7988, #0177b8, #ffb900, #6b69d6;
|
||||
$defaultTagColors: #fff, #fff, #fff, #fff, #fff;
|
||||
|
||||
[data-scheme="light"] {
|
||||
--pre-text-color: #272822;
|
||||
--pre-background-color: #fafafa;
|
||||
@import "partials/highlight/light.scss";
|
||||
}
|
||||
|
||||
[data-scheme="dark"] {
|
||||
--pre-text-color: #f8f8f2;
|
||||
--pre-background-color: #272822;
|
||||
@import "partials/highlight/dark.scss";
|
||||
}
|
||||
|
||||
/*
|
||||
* Global style
|
||||
*/
|
||||
@@ -45,7 +33,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);
|
||||
}
|
||||
}
|
||||
@@ -159,3 +147,15 @@ $defaultTagColors: #fff, #fff, #fff, #fff, #fff;
|
||||
--shadow-l4: 0px 24px 32px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), 0px 4px 8px rgba(0, 0, 0, 0.04),
|
||||
0px 0px 1px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
[data-scheme="light"] {
|
||||
--pre-text-color: #272822;
|
||||
--pre-background-color: #fafafa;
|
||||
@import "partials/highlight/light.scss";
|
||||
}
|
||||
|
||||
[data-scheme="dark"] {
|
||||
--pre-text-color: #f8f8f2;
|
||||
--pre-background-color: #272822;
|
||||
@import "partials/highlight/dark.scss";
|
||||
}
|
||||
|
||||
@@ -57,6 +57,60 @@ 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 || !container.contains(paragraph)) continue;
|
||||
|
||||
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
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
+2
-10
@@ -108,16 +108,8 @@ params:
|
||||
id:
|
||||
|
||||
widgets:
|
||||
enabled:
|
||||
- search
|
||||
- archives
|
||||
- tag-cloud
|
||||
|
||||
archives:
|
||||
limit: 5
|
||||
|
||||
tagCloud:
|
||||
limit: 10
|
||||
homepage: []
|
||||
page: []
|
||||
|
||||
opengraph:
|
||||
twitter:
|
||||
|
||||
@@ -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
|
||||
|
||||
+34
-23
@@ -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,27 +122,26 @@ params:
|
||||
emitMetadata: 0
|
||||
|
||||
gitalk:
|
||||
owner:
|
||||
admin:
|
||||
repo:
|
||||
clientID:
|
||||
clientSecret:
|
||||
|
||||
owner:
|
||||
admin:
|
||||
repo:
|
||||
clientID:
|
||||
clientSecret:
|
||||
|
||||
cusdis:
|
||||
host:
|
||||
id:
|
||||
|
||||
host:
|
||||
id:
|
||||
widgets:
|
||||
enabled:
|
||||
- search
|
||||
- archives
|
||||
- tag-cloud
|
||||
|
||||
archives:
|
||||
limit: 5
|
||||
|
||||
tagCloud:
|
||||
limit: 10
|
||||
homepage:
|
||||
- type: search
|
||||
- type: archives
|
||||
params:
|
||||
limit: 5
|
||||
- type: tag-cloud
|
||||
params:
|
||||
limit: 10
|
||||
page:
|
||||
- type: toc
|
||||
|
||||
opengraph:
|
||||
twitter:
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -158,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
|
||||
|
||||
[](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 >}}
|
||||
@@ -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>
|
||||
>
|
||||
@@ -6,7 +6,8 @@
|
||||
</head>
|
||||
<body class="{{ block `body-class` . }}{{ end }}">
|
||||
{{- partial "head/colorScheme" . -}}
|
||||
<div class="container main-container flex {{ block `container-class` . }}on-phone--column {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }}{{ end }}">
|
||||
{{- $hasWidget := default (gt (len .Site.Params.widgets.homepage) 0) (.Scratch.Get "hasWidget") -}}
|
||||
<div class="container main-container flex on-phone--column {{ if $hasWidget }}extended{{ else }}compact{{ end }}">
|
||||
{{- block "left-sidebar" . -}}
|
||||
{{ partial "sidebar/left.html" . }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -77,5 +77,5 @@
|
||||
{{ end }}
|
||||
|
||||
{{ define "right-sidebar" }}
|
||||
{{ partialCached "sidebar/right.html" . }}
|
||||
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
|
||||
{{ end }}
|
||||
@@ -1,15 +1,26 @@
|
||||
{{ define "body-class" }}
|
||||
{{ $TOCEnabled := default (default false .Site.Params.article.toc) .Params.toc }}
|
||||
{{- .Scratch.Set "hasTOC" (and (ge (len .TableOfContents) 100) $TOCEnabled) -}}
|
||||
article-page {{ if (.Scratch.Get "hasTOC") }}has-toc{{ end }}
|
||||
{{ end }}
|
||||
article-page
|
||||
{{/*
|
||||
Enable the right sidebar if
|
||||
- Widget different from 'TOC' is enabled
|
||||
- TOC is enabled and not empty
|
||||
*/}}
|
||||
{{- $HasWidgetNotTOC := false -}}
|
||||
{{- $TOCWidgetEnabled := false -}}
|
||||
{{- range .Site.Params.widgets.page -}}
|
||||
{{- if ne .type "toc" -}}
|
||||
{{ $HasWidgetNotTOC = true -}}
|
||||
{{- else -}}
|
||||
{{ $TOCWidgetEnabled = true -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ define "container-class" }}
|
||||
{{ if (.Scratch.Get "hasTOC") }}
|
||||
extended
|
||||
{{ else }}
|
||||
on-phone--column {{ if .Site.Params.widgets.enabled }}extended{{ else }}compact{{ end }}
|
||||
{{ end }}
|
||||
{{- $TOCManuallyDisabled := eq .Params.toc false -}}
|
||||
{{- $TOCEnabled := and (not $TOCManuallyDisabled) $TOCWidgetEnabled -}}
|
||||
{{- $hasTOC := ge (len .TableOfContents) 100 -}}
|
||||
{{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}}
|
||||
|
||||
{{- .Scratch.Set "hasWidget" (or $HasWidgetNotTOC (and $TOCEnabled $hasTOC)) -}}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
@@ -30,32 +41,6 @@
|
||||
{{ partialCached "article/components/photoswipe" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "left-sidebar" }}
|
||||
{{ if (.Scratch.Get "hasTOC") }}
|
||||
<div id="article-toolbar">
|
||||
<a href="{{ .Site.BaseURL | relLangURL }}" class="back-home">
|
||||
{{ (resources.Get "icons/back.svg").Content | safeHTML }}
|
||||
<span>{{ T "article.back" }}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ partial "sidebar/left.html" . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "right-sidebar" }}
|
||||
{{ if (.Scratch.Get "hasTOC") }}
|
||||
<aside class="sidebar right-sidebar sticky">
|
||||
<section class="widget archives">
|
||||
<div class="widget-icon">
|
||||
{{ partial "helper/icon" "hash" }}
|
||||
</div>
|
||||
<h2 class="widget-title section-title">{{ T "article.tableOfContents" }}</h2>
|
||||
|
||||
<div class="widget--toc">
|
||||
{{ .TableOfContents }}
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
{{ end }}
|
||||
{{ if .Scratch.Get "hasWidget" }}{{ partial "sidebar/right.html" (dict "Context" . "Scope" "page") }}{{ end}}
|
||||
{{ end }}
|
||||
|
||||
+1
-1
@@ -15,5 +15,5 @@
|
||||
{{ end }}
|
||||
|
||||
{{ define "right-sidebar" }}
|
||||
{{ partial "sidebar/right.html" . }}
|
||||
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
|
||||
{{ end }}
|
||||
@@ -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,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.18/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,4 +1,4 @@
|
||||
{{- $ThemeVersion := "3.6.0" -}}
|
||||
{{- $ThemeVersion := "3.8.0" -}}
|
||||
<footer class="site-footer">
|
||||
<section class="copyright">
|
||||
©
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
integrity="{{ . }}"
|
||||
{{- end -}}
|
||||
crossorigin="anonymous"
|
||||
defer="{{ default false .defer }}"
|
||||
{{ if .defer }}defer{{ end }}
|
||||
>
|
||||
</script>
|
||||
{{- else if eq .type "style" -}}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
{{ if .Site.Params.widgets.enabled }}
|
||||
{{ $context := . }}
|
||||
{{- $scope := default "homepage" .Scope -}}
|
||||
{{- $context := .Context -}}
|
||||
{{- with (index .Context.Site.Params.widgets $scope) -}}
|
||||
<aside class="sidebar right-sidebar sticky">
|
||||
{{ range $widget := .Site.Params.widgets.enabled }}
|
||||
{{ partial (printf "widget/%s" $widget) $context }}
|
||||
{{ range $widget := . }}
|
||||
{{ if templates.Exists (printf "partials/widget/%s.html" .type) }}
|
||||
{{ partial (printf "widget/%s" .type) (dict "Context" $context "Params" .params) }}
|
||||
{{ else }}
|
||||
{{ warnf "Widget %s not found" .type }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</aside>
|
||||
{{ end }}
|
||||
@@ -1,4 +1,6 @@
|
||||
{{- $query := first 1 (where .Site.Pages "Layout" "==" "archives") -}}
|
||||
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}}
|
||||
{{- $context := .Context -}}
|
||||
{{- $limit := default 5 .Params.limit -}}
|
||||
{{- if $query -}}
|
||||
{{- $archivesPage := index $query 0 -}}
|
||||
<section class="widget archives">
|
||||
@@ -7,17 +9,17 @@
|
||||
</div>
|
||||
<h2 class="widget-title section-title">{{ T "widget.archives.title" }}</h2>
|
||||
|
||||
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
||||
{{ $notHidden := where .Site.RegularPages "Params.hidden" "!=" true }}
|
||||
{{ $pages := where $context.Site.RegularPages "Type" "in" $context.Site.Params.mainSections }}
|
||||
{{ $notHidden := where $context.Site.RegularPages "Params.hidden" "!=" true }}
|
||||
{{ $filtered := ($pages | intersect $notHidden) }}
|
||||
{{ $archives := $filtered.GroupByDate "2006" }}
|
||||
|
||||
<div class="widget-archive--list">
|
||||
{{ range $index, $item := first (add .Site.Params.widgets.archives.limit 1) ($archives) }}
|
||||
{{ range $index, $item := first (add $limit 1) ($archives) }}
|
||||
{{- $id := lower (replace $item.Key " " "-") -}}
|
||||
<div class="archives-year">
|
||||
<a href="{{ $archivesPage.RelPermalink }}#{{ $id }}">
|
||||
{{ if eq $index $.Site.Params.widgets.archives.limit }}
|
||||
{{ if eq $index $limit }}
|
||||
<span class="year">{{ T "widget.archives.more" }}</span>
|
||||
{{ else }}
|
||||
<span class="year">{{ .Key }}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- $query := first 1 (where .Site.Pages "Layout" "==" "search") -}}
|
||||
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "search") -}}
|
||||
{{- if $query -}}
|
||||
{{- $searchPage := index $query 0 -}}
|
||||
<form action="{{ $searchPage.RelPermalink }}" class="search-form widget" {{ with .OutputFormats.Get "json" -}}data-json="{{ .Permalink }}" {{- end }}>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{{- $context := .Context -}}
|
||||
{{- $limit := default 10 .Params.limit -}}
|
||||
<section class="widget tagCloud">
|
||||
<div class="widget-icon">
|
||||
{{ partial "helper/icon" "tag" }}
|
||||
@@ -5,7 +7,7 @@
|
||||
<h2 class="widget-title section-title">{{ T "widget.tagCloud.title" }}</h2>
|
||||
|
||||
<div class="tagCloud-tags">
|
||||
{{ range first .Site.Params.widgets.tagCloud.limit .Site.Taxonomies.tags.ByCount }}
|
||||
{{ range first $limit $context.Site.Taxonomies.tags.ByCount }}
|
||||
<a href="{{ .Page.RelPermalink }}" class="font_size_{{ .Count }}">
|
||||
{{ .Page.Title }}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{{ if (.Context.Scratch.Get "TOCEnabled") }}
|
||||
<section class="widget archives">
|
||||
<div class="widget-icon">
|
||||
{{ partial "helper/icon" "hash" }}
|
||||
</div>
|
||||
<h2 class="widget-title section-title">{{ T "article.tableOfContents" }}</h2>
|
||||
|
||||
<div class="widget--toc">
|
||||
{{ .Context.TableOfContents }}
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user