diff --git a/.gitignore b/.gitignore index 9ebefdf..9ff142d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ public resources -assets/jsconfig.json \ No newline at end of file +assets/jsconfig.json +.hugo_build.lock \ No newline at end of file diff --git a/README.md b/README.md index ddacf82..0e6caa2 100644 --- a/README.md +++ b/README.md @@ -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) | \ No newline at end of file +| [MunifTanjim/minimo](https://github.com/MunifTanjim/minimo) | [MIT](https://github.com/MunifTanjim/minimo/blob/master/LICENSE) | diff --git a/assets/jsconfig.json b/assets/jsconfig.json index 9321136..040177a 100644 --- a/assets/jsconfig.json +++ b/assets/jsconfig.json @@ -6,5 +6,7 @@ "*" ] }, + "lib": ["es2020", "dom"], + "jsx": "preserve" } } \ No newline at end of file diff --git a/assets/scss/partials/base.scss b/assets/scss/partials/base.scss index ab3bf42..408629e 100644 --- a/assets/scss/partials/base.scss +++ b/assets/scss/partials/base.scss @@ -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; -} -/**/ diff --git a/assets/scss/partials/highlight/light.scss b/assets/scss/partials/highlight/light.scss index 9ddcff9..1798218 100644 --- a/assets/scss/partials/highlight/light.scss +++ b/assets/scss/partials/highlight/light.scss @@ -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 {} \ No newline at end of file +.chroma .w { +} diff --git a/assets/scss/partials/layout/article.scss b/assets/scss/partials/layout/article.scss index 7059853..78ed81c 100644 --- a/assets/scss/partials/layout/article.scss +++ b/assets/scss/partials/layout/article.scss @@ -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); diff --git a/assets/scss/variables.scss b/assets/scss/variables.scss index c75eb5c..8e371d6 100644 --- a/assets/scss/variables.scss +++ b/assets/scss/variables.scss @@ -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"; diff --git a/assets/ts/main.ts b/assets/ts/main.ts index d79c127..20de18c 100644 --- a/assets/ts/main.ts +++ b/assets/ts/main.ts @@ -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 => { diff --git a/assets/ts/scrollspy.ts b/assets/ts/scrollspy.ts new file mode 100644 index 0000000..8a14085 --- /dev/null +++ b/assets/ts/scrollspy.ts @@ -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): 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) { + 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 }; \ No newline at end of file diff --git a/assets/ts/search.tsx b/assets/ts/search.tsx index 8e4eb6f..68db7b3 100644 --- a/assets/ts/search.tsx +++ b/assets/ts/search.tsx @@ -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(`${replaceHTMLEnt(str.substring(item.start, end))}`); + 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 '' + match + ''; - } - 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 {

- +
{item.image &&
diff --git a/assets/ts/smoothAnchors.ts b/assets/ts/smoothAnchors.ts new file mode 100644 index 0000000..0718bf5 --- /dev/null +++ b/assets/ts/smoothAnchors.ts @@ -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 }; \ No newline at end of file diff --git a/exampleSite/content/page/about.md b/exampleSite/content/page/about/index.md similarity index 100% rename from exampleSite/content/page/about.md rename to exampleSite/content/page/about/index.md diff --git a/exampleSite/content/page/archives.md b/exampleSite/content/page/archives/index.md similarity index 100% rename from exampleSite/content/page/archives.md rename to exampleSite/content/page/archives/index.md diff --git a/exampleSite/content/page/links/index.md b/exampleSite/content/page/links/index.md new file mode 100644 index 0000000..9e29bf3 --- /dev/null +++ b/exampleSite/content/page/links/index.md @@ -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. \ No newline at end of file diff --git a/exampleSite/content/page/links/ts-logo-128.jpg b/exampleSite/content/page/links/ts-logo-128.jpg new file mode 100644 index 0000000..85e3323 Binary files /dev/null and b/exampleSite/content/page/links/ts-logo-128.jpg differ diff --git a/exampleSite/content/page/search.md b/exampleSite/content/page/search/index.md similarity index 100% rename from exampleSite/content/page/search.md rename to exampleSite/content/page/search/index.md diff --git a/exampleSite/content/post/markdown-syntax/index.md b/exampleSite/content/post/markdown-syntax/index.md index 8c53bc2..be381a0 100644 --- a/exampleSite/content/post/markdown-syntax/index.md +++ b/exampleSite/content/post/markdown-syntax/index.md @@ -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 {{< /highlight >}} +#### Diff code block + +```diff +[dependencies.bevy] +git = "https://github.com/bevyengine/bevy" +rev = "11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13" +- features = ["dynamic"] ++ features = ["jpeg", "dynamic"] +``` + ## List Types #### Ordered List diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1cf4674 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/CaiJimmy/hugo-theme-stack + +go 1.12 diff --git a/layouts/_default/single.html b/layouts/_default/single.html index 8b21c52..8ce42e9 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -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) }} diff --git a/layouts/partials/article/components/content.html b/layouts/partials/article/components/content.html index fc0f8f6..61e536c 100644 --- a/layouts/partials/article/components/content.html +++ b/layouts/partials/article/components/content.html @@ -1,3 +1,5 @@
- {{ .Content }} + + {{ $wrappedTable := printf "
${1}
" }} + {{ .Content | replaceRE "((?:.|\n)+?
)" $wrappedTable | safeHTML }}
diff --git a/layouts/partials/article/components/links.html b/layouts/partials/article/components/links.html new file mode 100644 index 0000000..118dbb3 --- /dev/null +++ b/layouts/partials/article/components/links.html @@ -0,0 +1,26 @@ +
+ {{ range $i, $link := .Params.links }} + + {{ end }} +
\ No newline at end of file diff --git a/layouts/partials/comments/provider/twikoo.html b/layouts/partials/comments/provider/twikoo.html index 4dbf976..30c7033 100644 --- a/layouts/partials/comments/provider/twikoo.html +++ b/layouts/partials/comments/provider/twikoo.html @@ -1,4 +1,4 @@ - +