1 files deleted
1 files added
8 files modified
| | |
| | | - uses: actions/checkout@v2 |
| | | |
| | | - name: Build Link Index |
| | | uses: jackyzha0/hugo-obsidian@v2.1 |
| | | uses: jackyzha0/hugo-obsidian@v2.3 |
| | | with: |
| | | index: true |
| | | input: content |
| | | output: data |
| | | |
| | |
| | | .idea |
| | | content/.obsidian |
| | | data/linkIndex.yaml |
| | | data/contentIndex.yaml |
| | |
| | | .centered { |
| | | margin-top: 30vh; |
| | | } |
| | | |
| | | header { |
| | | display: flex; |
| | | flex-direction: row; |
| | | align-items: center; |
| | | |
| | | & > nav { |
| | | @media all and (max-width: 600px) { |
| | | display: none; |
| | | } |
| | | |
| | | & > a { |
| | | margin-left: 2em; |
| | | } |
| | | } |
| | | |
| | | & > .spacer { |
| | | flex: 1 1 auto; |
| | | } |
| | | |
| | | & > svg { |
| | | cursor: pointer; |
| | | width: 18px; |
| | | min-width: 18px; |
| | | margin: 0 1em; |
| | | |
| | | &:hover .search-path { |
| | | stroke: var(--tertiary); |
| | | } |
| | | |
| | | .search-path { |
| | | stroke: var(--gray); |
| | | stroke-width: 2px; |
| | | transition: stroke 0.5s ease; |
| | | } |
| | | } |
| | | } |
| | | |
| | | #search-container { |
| | | position: fixed; |
| | | z-index: 9999; |
| | | left: 0; |
| | | top: 0; |
| | | width: 100vw; |
| | | height: 100vh; |
| | | display: none; |
| | | backdrop-filter: blur(4px); |
| | | -webkit-backdrop-filter: blur(4px); |
| | | |
| | | & > div { |
| | | width: 50%; |
| | | margin-top: 15vh; |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | |
| | | @media all and (max-width: 1200px) { |
| | | width: 90%; |
| | | } |
| | | |
| | | & > * { |
| | | width: 100%; |
| | | border-radius: 4px; |
| | | background: var(--light); |
| | | box-shadow: 0 14px 50px rgba(27, 33, 48, 0.12), 0 10px 30px rgba(27, 33, 48, 0.16); |
| | | margin-bottom: 2em; |
| | | } |
| | | |
| | | & > input { |
| | | box-sizing: border-box; |
| | | padding: 0.5em 1em; |
| | | font-family: Inter, sans-serif; |
| | | color: var(--dark); |
| | | font-size: 1.1em; |
| | | border: 1px solid var(--outlinegray); |
| | | |
| | | &:focus { |
| | | outline: none; |
| | | } |
| | | } |
| | | |
| | | & > #results-container { |
| | | & > .result-card { |
| | | padding: 1em; |
| | | cursor: pointer; |
| | | transition: background 0.2s ease; |
| | | border: 1px solid var(--outlinegray); |
| | | border-bottom: none; |
| | | |
| | | &:hover { |
| | | background: rgba(180, 180, 180, 0.15); |
| | | } |
| | | |
| | | &:first-of-type { |
| | | border-top-left-radius: 5px; |
| | | border-top-right-radius: 5px; |
| | | } |
| | | |
| | | &:last-of-type { |
| | | border-bottom-left-radius: 5px; |
| | | border-bottom-right-radius: 5px; |
| | | border-bottom: 1px solid var(--outlinegray); |
| | | } |
| | | |
| | | & > h3, & > p { |
| | | margin: 0; |
| | | } |
| | | |
| | | & .search-highlight { |
| | | background-color: #afbfc966; |
| | | padding: 0.05em 0.2em; |
| | | border-radius: 3px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | .darkmode { |
| | | text-align: right; |
| | | float: right; |
| | | padding: 1em; |
| | | min-width: 30px; |
| | | position: relative; |
| | | |
| | | @media all and (max-width: 450px) { |
| | | padding: 1em; |
| | | } |
| | | |
| | | & > .toggle { |
| | | display: none; |
| | | box-sizing: border-box; |
| | | |
| | | &:checked + .toggle-button:after { |
| | | left: 50%; |
| | | } |
| | | |
| | | & + .toggle-button { |
| | | box-sizing: border-box; |
| | | outline: 0; |
| | | display: inline-block; |
| | | width: 3em; |
| | | height: 1.5em; |
| | | position: relative; |
| | | cursor: pointer; |
| | | border: 2px solid var(--gray); |
| | | user-select: none; |
| | | padding: 2px; |
| | | transition: all 0.2s ease; |
| | | border-radius: 2em; |
| | | |
| | | &:after, &:before { |
| | | position: relative; |
| | | display: block; |
| | | box-sizing: border-box; |
| | | content: ""; |
| | | width: 50%; |
| | | height: 100%; |
| | | } |
| | | |
| | | &:before { |
| | | display: none; |
| | | } |
| | | |
| | | &:after { |
| | | left: 0; |
| | | transition: all 0.2s ease; |
| | | background: var(--gray); |
| | | content: ""; |
| | | border-radius: 1em; |
| | | } |
| | | } |
| | | } |
| | | |
| | | & #dayIcon { |
| | | position: relative; |
| | | & svg { |
| | | opacity: 0; |
| | | position: absolute; |
| | | width: 20px; |
| | | height: 20px; |
| | | top: -1.5px; |
| | | top: calc(50% - 10px); |
| | | margin: 0 7px; |
| | | fill: var(--gray); |
| | | transition: opacity 0.1s ease; |
| | | } |
| | | } |
| | | |
| | | & #nightIcon { |
| | | position: relative; |
| | | width: 18px; |
| | | height: 18px; |
| | | top: -2px; |
| | | margin: 0 7px; |
| | | fill: var(--gray); |
| | | .toggle:checked ~ label { |
| | | & > #dayIcon { |
| | | opacity: 0; |
| | | } |
| | | & > #nightIcon { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | |
| | | .toggle:not(:checked) ~ label { |
| | | & > #dayIcon { |
| | | opacity: 1; |
| | | } |
| | | & > #nightIcon { |
| | | opacity: 0; |
| | | } |
| | | } |
| | |
| | | # 🌱 Quartz |
| | | ## v1.1 |
| | | ## v2.0 |
| | | |
| | | Simple second brain and [digital garden](https://jzhao.xyz/posts/digital-gardening). |
| | | |
| | |
| | | $ cd <location-of-your-local-quartz> |
| | | |
| | | # Scrape all links in your Quartz folder and generate info for Quartz |
| | | $ hugo-obsidian -input=content -output=data |
| | | $ hugo-obsidian -input=content -output=data -index=true |
| | | ``` |
| | | |
| | | Afterwards, start the Hugo server as shown above and your local backlinks and interactive graph should be populated! |
| | |
| | | {{ partial "head.html" . }} |
| | | |
| | | <body> |
| | | {{partial "search.html" .}} |
| | | <div class="singlePage"> |
| | | <!-- Begin actual content --> |
| | | {{partial "darkmode.html" .}} |
| | | <article> |
| | | <header> |
| | | {{if .Title}}<h1>{{ .Title }}</h1>{{end}} |
| | | <svg tabindex="0" id="search-icon" aria-labelledby="title desc" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"><title id="title">Search Icon</title><desc id="desc">Icon to open search</desc><g class="search-path" fill="none"><path stroke-linecap="square" d="M18.5 18.3l-5.4-5.4"/><circle cx="8" cy="8" r="7"/></g></svg> |
| | | <div class="spacer"></div> |
| | | {{partial "darkmode.html" .}} |
| | | </header> |
| | | <article> |
| | | {{if $.Site.Data.config.enableToc}} |
| | | <aside class="mainTOC"> |
| | | <h3>Table of Contents</h3> |
| | |
| | | <div class='darkmode'> |
| | | <input class='toggle' id='darkmode-toggle' type='checkbox' tabindex="-1"> |
| | | <label id="toggle-label-light" for='darkmode-toggle' tabindex="-1"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="dayIcon" x="0px" y="0px" viewBox="0 0 35 35" style="enable-background:new 0 0 35 35;" xml:space="preserve"> |
| | | <title>Light Mode</title> |
| | | <path d="M6,17.5C6,16.672,5.328,16,4.5,16h-3C0.672,16,0,16.672,0,17.5 S0.672,19,1.5,19h3C5.328,19,6,18.328,6,17.5z M7.5,26c-0.414,0-0.789,0.168-1.061,0.439l-2,2C4.168,28.711,4,29.086,4,29.5 C4,30.328,4.671,31,5.5,31c0.414,0,0.789-0.168,1.06-0.44l2-2C8.832,28.289,9,27.914,9,27.5C9,26.672,8.329,26,7.5,26z M17.5,6 C18.329,6,19,5.328,19,4.5v-3C19,0.672,18.329,0,17.5,0S16,0.672,16,1.5v3C16,5.328,16.671,6,17.5,6z M27.5,9 c0.414,0,0.789-0.168,1.06-0.439l2-2C30.832,6.289,31,5.914,31,5.5C31,4.672,30.329,4,29.5,4c-0.414,0-0.789,0.168-1.061,0.44 l-2,2C26.168,6.711,26,7.086,26,7.5C26,8.328,26.671,9,27.5,9z M6.439,8.561C6.711,8.832,7.086,9,7.5,9C8.328,9,9,8.328,9,7.5 c0-0.414-0.168-0.789-0.439-1.061l-2-2C6.289,4.168,5.914,4,5.5,4C4.672,4,4,4.672,4,5.5c0,0.414,0.168,0.789,0.439,1.06 L6.439,8.561z M33.5,16h-3c-0.828,0-1.5,0.672-1.5,1.5s0.672,1.5,1.5,1.5h3c0.828,0,1.5-0.672,1.5-1.5S34.328,16,33.5,16z M28.561,26.439C28.289,26.168,27.914,26,27.5,26c-0.828,0-1.5,0.672-1.5,1.5c0,0.414,0.168,0.789,0.439,1.06l2,2 C28.711,30.832,29.086,31,29.5,31c0.828,0,1.5-0.672,1.5-1.5c0-0.414-0.168-0.789-0.439-1.061L28.561,26.439z M17.5,29 c-0.829,0-1.5,0.672-1.5,1.5v3c0,0.828,0.671,1.5,1.5,1.5s1.5-0.672,1.5-1.5v-3C19,29.672,18.329,29,17.5,29z M17.5,7 C11.71,7,7,11.71,7,17.5S11.71,28,17.5,28S28,23.29,28,17.5S23.29,7,17.5,7z M17.5,25c-4.136,0-7.5-3.364-7.5-7.5 c0-4.136,3.364-7.5,7.5-7.5c4.136,0,7.5,3.364,7.5,7.5C25,21.636,21.636,25,17.5,25z" /> |
| | | </svg> |
| | | </label> |
| | | <input class='toggle' id='darkmode-toggle' type='checkbox' tabindex="-1"> |
| | | <label class='toggle-button' for='darkmode-toggle'></label> |
| | | <label id="toggle-label-dark" for='darkmode-toggle' tabindex="-1"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="nightIcon" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background='new 0 0 100 100'" xml:space="preserve"> |
| | | <title>Dark Mode</title> |
| New file |
| | |
| | | <div id="search-container"> |
| | | <div> |
| | | <input autocomplete="off" id="search-bar" name="search" type="text" aria-label="Search" placeholder="Search for something..."> |
| | | <div id="results-container"> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script src="https://cdn.jsdelivr.net/gh/nextapps-de/flexsearch@0.7.2/dist/flexsearch.bundle.js"></script> |
| | | <script> |
| | | // code from https://github.com/danestves/markdown-to-text |
| | | const removeMarkdown = ( |
| | | markdown, |
| | | options = { |
| | | listUnicodeChar: false, |
| | | stripListLeaders: true, |
| | | gfm: true, |
| | | useImgAltText: false, |
| | | preserveLinks: false, |
| | | } |
| | | ) => { |
| | | let output = markdown || ""; |
| | | output = output.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*$/gm, ""); |
| | | |
| | | try { |
| | | if (options.stripListLeaders) { |
| | | if (options.listUnicodeChar) |
| | | output = output.replace( |
| | | /^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, |
| | | options.listUnicodeChar + " $1" |
| | | ); |
| | | else output = output.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm, "$1"); |
| | | } |
| | | if (options.gfm) { |
| | | output = output |
| | | .replace(/\n={2,}/g, "\n") |
| | | .replace(/~{3}.*\n/g, "") |
| | | .replace(/~~/g, "") |
| | | .replace(/`{3}.*\n/g, ""); |
| | | } |
| | | if(options.preserveLinks) { |
| | | output = output.replace(/\[(.*?)\][\[\(](.*?)[\]\)]/g, "$1 ($2)") |
| | | } |
| | | output = output |
| | | .replace(/<[^>]*>/g, "") |
| | | .replace(/^[=\-]{2,}\s*$/g, "") |
| | | .replace(/\[\^.+?\](\: .*?$)?/g, "") |
| | | .replace(/\s{0,2}\[.*?\]: .*?$/g, "") |
| | | .replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, options.useImgAltText ? "$1" : "") |
| | | .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, "$1") |
| | | .replace(/^\s{0,3}>\s?/g, "") |
| | | .replace(/(^|\n)\s{0,3}>\s?/g, "\n\n") |
| | | .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, "") |
| | | .replace( |
| | | /^(\n)?\s{0,}#{1,6}\s+| {0,}(\n)?\s{0,}#{0,} {0,}(\n)?\s{0,}$/gm, |
| | | "$1$2$3" |
| | | ) |
| | | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2") |
| | | .replace(/([\*_]{1,3})(\S.*?\S{0,1})\1/g, "$2") |
| | | .replace(/(`{3,})(.*?)\1/gm, "$2") |
| | | .replace(/`(.+?)`/g, "$1") |
| | | .replace(/\n{2,}/g, "\n\n"); |
| | | } catch (e) { |
| | | console.error(e); |
| | | return markdown; |
| | | } |
| | | return output; |
| | | }; |
| | | </script> |
| | | <script> |
| | | const contentIndex = new FlexSearch.Worker({ |
| | | tokenize: "strict", |
| | | charset: "latin:advanced", |
| | | context: true, |
| | | depth: 3, |
| | | cache: 10, |
| | | suggest: true, |
| | | }) |
| | | |
| | | const scrapedContent = {{$.Site.Data.contentIndex}} |
| | | for (const [key, value] of Object.entries(scrapedContent)) { |
| | | contentIndex.add(key, value.content) |
| | | } |
| | | |
| | | const stopwords = ['i','me','my','myself','we','our','ours','ourselves','you','your','yours','yourself','yourselves','he','him','his','himself','she','her','hers','herself','it','its','itself','they','them','their','theirs','themselves','what','which','who','whom','this','that','these','those','am','is','are','was','were','be','been','being','have','has','had','having','do','does','did','doing','a','an','the','and','but','if','or','because','as','until','while','of','at','by','for','with','about','against','between','into','through','during','before','after','above','below','to','from','up','down','in','out','on','off','over','under','again','further','then','once','here','there','when','where','why','how','all','any','both','each','few','more','most','other','some','such','no','nor','not','only','own','same','so','than','too','very','s','t','can','will','just','don','should','now'] |
| | | const highlight = (content, term) => { |
| | | const highlightWindow = 15 |
| | | const tokenizedTerm = term.split(/\s+/).filter(t => t !== "") |
| | | const splitText = content.split(/\s+/).filter(t => t !== "") |
| | | const includesCheck = (token) => tokenizedTerm.some(term => token.toLowerCase().includes(term.toLowerCase())) |
| | | |
| | | const occurrencesIndices = splitText |
| | | .map(includesCheck) |
| | | |
| | | // calculate best index |
| | | let bestSum = 0 |
| | | let bestIndex = 0 |
| | | for (let i = 0; i < Math.max(occurrencesIndices.length - highlightWindow, 0); i++) { |
| | | const window = occurrencesIndices.slice(i, i + highlightWindow) |
| | | const windowSum = window.reduce((total, cur) => total + cur, 0) |
| | | if (windowSum > bestSum) { |
| | | bestSum = windowSum |
| | | bestIndex = i |
| | | } |
| | | } |
| | | |
| | | const startIndex = Math.max(bestIndex - highlightWindow, 0) |
| | | const endIndex = Math.min(startIndex + 2 * highlightWindow, splitText.length) |
| | | const mappedText = splitText |
| | | .slice(startIndex, endIndex) |
| | | .map(token => { |
| | | if (includesCheck(token)) { |
| | | return `<span class="search-highlight">${token}</span>` |
| | | } |
| | | return token |
| | | }) |
| | | .join(" ") |
| | | .replaceAll('</span> <span class="search-highlight">', " ") |
| | | return `${startIndex === 0 ? "" : "..."}${mappedText}${endIndex === splitText.length ? "" : "..."}` |
| | | } |
| | | |
| | | const resultToHTML = ({url, title, content, term}) => { |
| | | const md = content.split("---")[2] |
| | | const text = removeMarkdown(md) |
| | | const resultTitle = highlight(title, term) |
| | | const resultText = highlight(text, term) |
| | | return `<div class="result-card" id="${url}"> |
| | | <h3>${resultTitle}</h3> |
| | | <p>${resultText}</p> |
| | | </div>` |
| | | } |
| | | |
| | | const source = document.getElementById('search-bar') |
| | | const results = document.getElementById("results-container") |
| | | source.addEventListener('input', (e) => { |
| | | const term = e.target.value |
| | | contentIndex.search(term, { |
| | | limit: 5, |
| | | depth: 3, |
| | | suggest: true, |
| | | }).then(searchResults => { |
| | | const resultIds = [...new Set(searchResults)] |
| | | const finalResults = resultIds.map(id => ({ |
| | | url: id, |
| | | title: scrapedContent[id].title, |
| | | content: scrapedContent[id].content |
| | | })) |
| | | |
| | | // display |
| | | if (finalResults.length === 0) { |
| | | results.innerHTML = `<div class="result-card"> |
| | | <p>No results.</p> |
| | | </div>` |
| | | } else { |
| | | results.innerHTML = finalResults |
| | | .map(result => resultToHTML({ |
| | | ...result, |
| | | term, |
| | | })) |
| | | .join("\n") |
| | | const anchors = document.getElementsByClassName("result-card"); |
| | | [...anchors].forEach(anchor => { |
| | | anchor.onclick = () => { |
| | | window.location.href = `${anchor.id}#:~:text=${encodeURIComponent(term)}` |
| | | } |
| | | }) |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | |
| | | const searchContainer = document.getElementById("search-container") |
| | | function openSearch() { |
| | | if (searchContainer.style.display === "none" || searchContainer.style.display === "") { |
| | | source.value = "" |
| | | results.innerHTML = "" |
| | | searchContainer.style.display = "block" |
| | | source.focus() |
| | | } else { |
| | | searchContainer.style.display = "none" |
| | | } |
| | | } |
| | | |
| | | function closeSearch() { |
| | | searchContainer.style.display = "none" |
| | | } |
| | | |
| | | document.addEventListener('keydown', (event) => { |
| | | if (event.key === "/") { |
| | | event.preventDefault() |
| | | openSearch() |
| | | } |
| | | if (event.key === "Escape") { |
| | | event.preventDefault() |
| | | closeSearch() |
| | | } |
| | | }) |
| | | |
| | | window.addEventListener('DOMContentLoaded', () => { |
| | | const searchButton = document.getElementById("search-icon") |
| | | searchButton.addEventListener('click', (evt) => { |
| | | openSearch() |
| | | }) |
| | | searchButton.addEventListener('keydown', (evt) => { |
| | | openSearch() |
| | | }) |
| | | }) |
| | | |
| | | </script> |