Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbc7e0cfd9 |
No files matched your search
@@ -1,6 +0,0 @@
|
||||
.git
|
||||
.delta
|
||||
.idea
|
||||
.vscode
|
||||
docs
|
||||
legacy_docs
|
||||
@@ -1,17 +0,0 @@
|
||||
# Server configuration. Copy to .env (gitignored), export or source it.
|
||||
|
||||
# Address the server listens on.
|
||||
LISTEN_ADDR=:8080
|
||||
# Session lifetime.
|
||||
SESSION_TTL=12h
|
||||
# Set to false only when testing locally over plain http.
|
||||
COOKIE_SECURE=false
|
||||
|
||||
# OIDC (Authentik). Unset: public pages still work, protected pages answer 503.
|
||||
OIDC_ISSUER_URL=https://auth.example.com/application/o/yggdrasil/
|
||||
OIDC_CLIENT_ID=yggdrasil
|
||||
OIDC_CLIENT_SECRET=change-me
|
||||
OIDC_REDIRECT_URL=http://localhost:8080/callback
|
||||
# Comma/space separated scopes. The groups claim must be covered by a scope
|
||||
# mapping on the provider for group checks to work.
|
||||
# OIDC_SCOPES=openid profile email groups
|
||||
@@ -1,48 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# shiroa bundles its own Typst compiler, so no separate typst install.
|
||||
- name: Install shiroa
|
||||
run: |
|
||||
curl -sSL -o /tmp/shiroa.tar.gz \
|
||||
https://github.com/Myriad-Dreamin/shiroa/releases/download/v0.4.0-rc2/shiroa-x86_64-unknown-linux-musl.tar.gz
|
||||
tar -xzf /tmp/shiroa.tar.gz -C /tmp
|
||||
install -m 755 /tmp/shiroa-x86_64-unknown-linux-musl/shiroa /usr/local/bin/shiroa
|
||||
shiroa --version
|
||||
|
||||
- name: Generate acl.json
|
||||
run: go run ./cmd/parser -in docs -out acl.json
|
||||
|
||||
- name: Build the book
|
||||
run: shiroa build docs --dest-dir ../dist
|
||||
|
||||
- name: Test and build the server
|
||||
run: |
|
||||
go vet ./...
|
||||
go test ./...
|
||||
test -z "$(gofmt -l cmd internal embed.go)" || { echo 'gofmt needed:'; gofmt -l cmd internal embed.go; exit 1; }
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o yggdrasil-server ./cmd/server
|
||||
|
||||
# upload-artifact v4 needs a reasonably current Gitea/act_runner;
|
||||
# fall back to v3 if your runner is too old to serve v4 artifacts.
|
||||
- name: Upload server artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: yggdrasil-server-linux-amd64
|
||||
path: yggdrasil-server
|
||||
@@ -1,21 +0,0 @@
|
||||
# build outputs (dist is regenerated by shiroa; .gitkeep keeps go:embed happy)
|
||||
/dist/*
|
||||
!/dist/.gitkeep
|
||||
/server
|
||||
yggdrasil-server
|
||||
|
||||
# secrets
|
||||
.env
|
||||
|
||||
# editors
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# delta workspace
|
||||
/.delta/
|
||||
|
||||
# pinned toolchain downloads (shiroa), managed by the Justfile
|
||||
tools/
|
||||
|
||||
# shiroa serve default output
|
||||
docs/dist/
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Not found</title><meta content="The requested page does not exist." name=description><meta content="The requested page does not exist." property=og:description><meta content="The requested page does not exist." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Not found | Yggdrasil" property=og:title><meta content="Not found | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/404.html rel=canonical><meta content=https://yggdrasil.kb28.ch/404.html property=og:url><link href=/assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke=Line truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
#set document(title: [Not found])
|
||||
#metadata((pdf: false)) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
The requested page does not exist.
|
||||
@@ -1,80 +0,0 @@
|
||||
# AGENT.md — operating rules for agents working on Yggdrasil
|
||||
|
||||
Personal knowledge base: Typst sources (`docs/`, a shiroa book) compiled to
|
||||
a static site, embedded into a Go binary that enforces per-page OIDC group
|
||||
permissions (Authentik). README.md is the human-facing documentation; this
|
||||
file is the working rulebook. Follow it; when it conflicts with the user,
|
||||
the user wins.
|
||||
|
||||
## Commands
|
||||
|
||||
- `just` — shows the command overview (bare `just` never builds anything).
|
||||
- `just build` — parse ACL, compile the book, build `./server`.
|
||||
- `just run` — build everything, then serve on :8080.
|
||||
- `just check` — go vet, go test, gofmt. Run it before every commit.
|
||||
- `just clean` — removes `dist/` and `server` (keeps `tools/` and
|
||||
restores the committed `dist/.gitkeep`).
|
||||
- The shiroa toolchain is pinned and self-bootstrapped into `tools/` by the
|
||||
`tools` recipe. Never assume a global shiroa/typst install exists; do not
|
||||
install shiroa globally; do not bump the pin casually (see Traps).
|
||||
|
||||
## Layout
|
||||
|
||||
- `docs/` — shiroa book. `book.typ` lists every built page; files not listed
|
||||
there are not part of the site. Categories are `=` part headers in the
|
||||
summary, pages are `# chapter("path.typ")[Title]` entries. Final URL =
|
||||
source path with `.typ` replaced by `.html`.
|
||||
- `cmd/parser` — build step: scans `docs/` for `#let access-groups = (..)`
|
||||
tags and emits `acl.json`. Untagged pages are public.
|
||||
- `cmd/server` + `embed.go` (repo root) — `//go:embed` of `dist/` and
|
||||
`acl.json` into a single binary.
|
||||
- `internal/acl` — ACL loading and page-key normalization (security critical).
|
||||
- `internal/oidc` — OIDC authorization-code client.
|
||||
- `internal/middleware` — sessions, login/callback/logout, access gate.
|
||||
- `legacy_docs/` — frozen previous site. Never modify or migrate it unless
|
||||
the user asks explicitly.
|
||||
|
||||
## Security invariants — do not break
|
||||
|
||||
- Fail closed: a malformed `acl.json` aborts startup; protected pages with
|
||||
no configured IdP answer 503, never the content.
|
||||
- Page keys: shiroa pages keep their rendered content in sibling
|
||||
`<page>.<theme>.multi.sir.in` files fetched by client-side JS.
|
||||
`acl.PageKey` reduces any URL to a page key so those files inherit the
|
||||
page's protection. If the shiroa output format changes, re-verify this
|
||||
mapping: content in any new sidecar file must be covered or blocked.
|
||||
- `/searchindex.json` contains the text of protected pages: blocked at the
|
||||
router, always.
|
||||
- Session cookies: HttpOnly, SameSite=Lax, Secure unless COOKIE_SECURE=false.
|
||||
OAuth2 states are random and single use; post-login redirect targets are
|
||||
sanitized (open-redirect guard).
|
||||
|
||||
## Traps
|
||||
|
||||
- Shiroa version pairing: the CLI and the `@preview/shiroa:*` typst package
|
||||
must match (CLI v0.4.0-rc2 pairs with package 0.4.0; the v0.3.0 release
|
||||
CLI rejects 0.4.0 books). The pin lives in two places: the Justfile
|
||||
`shiroa-version` and `.gitea/workflows/build.yml`. Bump all of them
|
||||
together with the package import and rebuild.
|
||||
- Typst call syntax: no space between `)` and the trailing content block —
|
||||
`#chapter("x.typ")[Title]`, not `#chapter("x.typ") [Title]`.
|
||||
- `dist/` and `acl.json` are embedded at Go build time; there is no runtime
|
||||
reload. After changing docs or ACL, rebuild the binary.
|
||||
- `go build` needs `dist/` and `acl.json` to exist: fresh clones carry
|
||||
`dist/.gitkeep` and a committed `acl.json` for exactly this reason.
|
||||
Never gitignore those two.
|
||||
|
||||
## Testing expectations
|
||||
|
||||
- Test what the user runs, not a proxy for it: from a clean state
|
||||
(`rm -rf tools && just clean`), `just build` and `just run` must succeed
|
||||
with no global shiroa on PATH. Test the bare `just` output too. After
|
||||
any clean/test cycle, `git status` must be clean: `dist/.gitkeep` is
|
||||
tracked, and committing a worktree where it is missing breaks fresh
|
||||
clones (`go:embed` needs `dist/`).
|
||||
- Server behavior: curl a public page (200), a protected page (503 without
|
||||
OIDC env; it must never serve protected content), and `/searchindex.json`
|
||||
(404). Extend the unit tests in `internal/acl` and `internal/middleware`
|
||||
whenever touching those paths.
|
||||
- A live OIDC round trip needs a real IdP and is not testable here. Say
|
||||
that explicitly when reporting what was verified.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Rootless image of the Yggdrasil server.
|
||||
# Build after `just parse build-docs` so that dist/ and acl.json are current:
|
||||
# just parse build-docs && docker build -t yggdrasil .
|
||||
|
||||
FROM golang:1.27 AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
COPY --from=build /out/server /server
|
||||
EXPOSE 8080
|
||||
USER nonroot:nonroot
|
||||
ENTRYPOINT ["/server"]
|
||||
@@ -1,52 +0,0 @@
|
||||
# Yggdrasil: Typst documentation behind an authenticated Go server.
|
||||
|
||||
# Pinned shiroa toolchain, self-managed under tools/ (no global install).
|
||||
# The CLI and the @preview/shiroa package version must match (see AGENT.md).
|
||||
shiroa-version := "v0.4.0-rc2"
|
||||
shiroa-target := if arch() == "x86_64" { "x86_64-unknown-linux-musl" } else if arch() == "aarch64" { "aarch64-unknown-linux-musl" } else { error("unsupported architecture " + arch()) }
|
||||
shiroa-dir := "tools/shiroa"
|
||||
shiroa-bin := shiroa-dir + "/shiroa-" + shiroa-target + "/shiroa"
|
||||
shiroa-url := "https://github.com/Myriad-Dreamin/shiroa/releases/download/" + shiroa-version + "/shiroa-" + shiroa-target + ".tar.gz"
|
||||
|
||||
# Show the available commands.
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Build everything: acl.json, the static site, and the server binary.
|
||||
build: parse build-docs build-server
|
||||
|
||||
# Scan docs/ for access-groups tags and regenerate acl.json.
|
||||
parse:
|
||||
go run ./cmd/parser -in docs -out acl.json
|
||||
|
||||
# Download the pinned shiroa into tools/ if missing.
|
||||
tools:
|
||||
@mkdir -p {{shiroa-dir}}
|
||||
@test -x {{shiroa-bin}} || { echo "Downloading shiroa {{shiroa-version}} ({{shiroa-target}})..."; curl -fsSL -o {{shiroa-dir}}/shiroa.tar.gz {{shiroa-url}}; tar -xzf {{shiroa-dir}}/shiroa.tar.gz -C {{shiroa-dir}}; rm {{shiroa-dir}}/shiroa.tar.gz; {{shiroa-bin}} --version; }
|
||||
|
||||
# Compile the Typst book into dist/.
|
||||
# (--dest-dir is resolved relative to the book directory, docs/, hence ../dist)
|
||||
build-docs: tools
|
||||
{{shiroa-bin}} build docs --dest-dir ../dist
|
||||
|
||||
# Build the server binary (embeds dist/ and acl.json).
|
||||
build-server:
|
||||
CGO_ENABLED=0 go build -trimpath -o server ./cmd/server
|
||||
|
||||
# Rebuild everything and run the server locally.
|
||||
run: build
|
||||
./server
|
||||
|
||||
# Live-preview the book without authentication (shiroa dev server).
|
||||
preview-docs: tools
|
||||
{{shiroa-bin}} serve docs
|
||||
|
||||
# Run static checks and tests.
|
||||
check:
|
||||
go vet ./...
|
||||
go test ./...
|
||||
@test -z "$(gofmt -l cmd internal embed.go)" || { echo 'gofmt needed:'; gofmt -l cmd internal embed.go; exit 1; }
|
||||
|
||||
# Remove build artifacts (keeps tools/ and the committed dist/.gitkeep).
|
||||
clean:
|
||||
rm -rf dist server && mkdir -p dist && touch dist/.gitkeep
|
||||
@@ -1,200 +0,0 @@
|
||||
# Yggdrasil
|
||||
|
||||
A personal knowledge base written in [Typst](https://typst.app), compiled to
|
||||
a static site by [shiroa](https://github.com/Myriad-Dreamin/shiroa), and
|
||||
served by a small Go binary that embeds the whole site and gates protected
|
||||
pages behind OIDC login (Authentik) with per-page group permissions.
|
||||
|
||||
No search, no database, no Node.js: one binary, one configuration.
|
||||
|
||||
## How it works
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["docs/*.typ (Typst sources)"] -->|"just build-docs (shiroa)"| B["dist/ (static site)"]
|
||||
A -->|"just parse (cmd/parser, access-groups tags)"| C["acl.json"]
|
||||
B -->|"go build (//go:embed)"| E["server binary"]
|
||||
C --> E
|
||||
E --> F{"request /page.html"}
|
||||
F -->|"public page"| G["serve page"]
|
||||
F -->|"protected page"| H{"session + groups ok?"}
|
||||
H -->|"no session"| I["redirect to /login, then Authentik"]
|
||||
H -->|"not authorized"| J["403"]
|
||||
H -->|"authorized"| G
|
||||
```
|
||||
|
||||
- Pages are written in Typst under `docs/` and listed in `docs/book.typ`.
|
||||
- A page can declare who may read it with a tag:
|
||||
`#let access-groups = ("family", "admin")`. Pages without a tag are public.
|
||||
- At build time, `cmd/parser` scans `docs/`, extracts the tags and generates
|
||||
`acl.json` mapping final page URLs to allowed groups.
|
||||
- The Go server embeds both `dist/` and `acl.json`. For every request it
|
||||
reduces the URL to a *page key* and looks it up in the ACL: public pages
|
||||
are served directly; protected pages require a session whose groups include
|
||||
at least one of the allowed groups.
|
||||
- Users authenticate with OIDC. The server reads the `groups` claim of the
|
||||
verified ID token to build the session.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
docs/ Typst sources (shiroa book)
|
||||
book.typ book metadata + chapter list (the sidebar)
|
||||
template.typ page layout (from the shiroa minimal template)
|
||||
index.typ home page (public)
|
||||
thermo/, security/, homelab/ example categories
|
||||
cmd/parser/ generates acl.json from access-groups tags
|
||||
cmd/server/ the web server
|
||||
internal/acl/ ACL loading, page-key normalization, group checks
|
||||
internal/oidc/ OIDC client (authorization code flow)
|
||||
internal/middleware/ sessions, login/logout/callback, access gate
|
||||
dist/ compiled site (generated, embedded at build time)
|
||||
tools/ pinned shiroa binary (downloaded by `just tools`)
|
||||
acl.json generated ACL (embedded at build time)
|
||||
legacy_docs/ the old documentation base (kept for manual migration)
|
||||
.gitea/workflows/ Gitea Actions CI
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
Prerequisites: Go 1.27.1+ (the version required by `go.mod`) and
|
||||
[just](https://github.com/casey/just). The
|
||||
pinned shiroa toolchain is downloaded automatically into `tools/` on the
|
||||
first build (needs `curl` + `tar` and network access); nothing is
|
||||
installed globally.
|
||||
|
||||
```sh
|
||||
just # show the available commands
|
||||
just run # parse ACL, compile the book, build and start the server
|
||||
```
|
||||
|
||||
Then open http://localhost:8080. The home page and public pages render;
|
||||
protected pages (`/thermo/fluids.html`, `/security/ssh.html`,
|
||||
`/homelab/backup.html` in the example book) return **503** until OIDC is
|
||||
configured.
|
||||
|
||||
For pure writing previews without authentication, use `just preview-docs`.
|
||||
|
||||
## Writing documentation
|
||||
|
||||
The book structure mirrors the URL structure: `docs/<path>.typ` is compiled
|
||||
to `/<path>.html`. To add a page:
|
||||
|
||||
1. Create the file, e.g. `docs/security/wireguard.typ`, starting with:
|
||||
|
||||
```typst
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= WireGuard
|
||||
|
||||
Your content...
|
||||
```
|
||||
|
||||
2. Reference it in `docs/book.typ`. A `=` heading creates a category
|
||||
(part) in the sidebar; each `- #chapter(...)` is a page:
|
||||
|
||||
```typst
|
||||
= Security
|
||||
- #chapter("security/ssh.typ", section: "4")[SSH hardening]
|
||||
- #chapter("security/wireguard.typ", section: "5")[WireGuard]
|
||||
```
|
||||
|
||||
3. Only pages listed in `book.typ` are built. Keep `section:` numbers
|
||||
consistent (sub-chapters like `"4.1"` nest under `"4"`).
|
||||
|
||||
To restrict a page, add the tag on the first line of its file:
|
||||
|
||||
```typst
|
||||
// Only readable by members of these groups.
|
||||
#let access-groups = ("family", "admin")
|
||||
```
|
||||
|
||||
- The groups are compared against the `groups` claim of the OIDC ID token
|
||||
(in Authentik: your user's group memberships).
|
||||
- A page without the tag is public.
|
||||
- Everything served *for* a protected page is protected too: the shiroa
|
||||
renderer fetches page content from `*.multi.sir.in` files, and those
|
||||
reduce to the same ACL entry as the page itself.
|
||||
- The search index (`searchindex.json`) would also contain protected text,
|
||||
so the server never serves it.
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables (see `.env.example`):
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---------------------|-----------------------------|---------|
|
||||
| `LISTEN_ADDR` | `:8080` | listen address |
|
||||
| `SESSION_TTL` | `12h` | session lifetime |
|
||||
| `COOKIE_SECURE` | `true` | `Secure` cookie flag; use `false` only on plain-http local tests |
|
||||
| `OIDC_ISSUER_URL` | (unset) | e.g. `https://auth.example.com/application/o/yggdrasil/` |
|
||||
| `OIDC_CLIENT_ID` | (unset) | OAuth2 client id |
|
||||
| `OIDC_CLIENT_SECRET`| (unset) | OAuth2 client secret |
|
||||
| `OIDC_REDIRECT_URL` | (unset) | e.g. `https://docs.example.com/callback` |
|
||||
| `OIDC_SCOPES` | `openid profile email groups` | requested scopes |
|
||||
|
||||
Without the `OIDC_*` variables the server runs in public-only mode:
|
||||
public pages are served, protected pages answer 503. The server refuses to
|
||||
start on a malformed `acl.json` or an unreachable OIDC issuer (fail closed).
|
||||
|
||||
### Setting up Authentik
|
||||
|
||||
1. **Applications -> Providers -> Create**: type *OAuth2/OpenID Connect*.
|
||||
2. Authorization flow: *implicit/explicit consent* (default is fine).
|
||||
3. Redirect URI: exactly `https://your-docs-host/callback`.
|
||||
4. Client type: *Confidential*; note the client id and secret.
|
||||
5. **Scope mappings**: make sure the issued ID token contains a `groups`
|
||||
claim. Recent Authentik versions ship a scope mapping that adds
|
||||
`groups`; otherwise create a *Property Mapping (Scope)* with e.g.
|
||||
`return {"groups": [g.name for g in user.ak_groups.all()]}` and add it
|
||||
to the provider.
|
||||
6. Note the issuer URL shown on the provider page
|
||||
(`.../application/o/<slug>/`) and set `OIDC_ISSUER_URL` to it.
|
||||
7. Create groups in **Directory -> Groups** (e.g. `family`, `admin`) whose
|
||||
names match the `access-groups` in your pages, and add users to them.
|
||||
|
||||
The redirect after login returns the user to the page they originally
|
||||
requested. Logout is available at `/logout`.
|
||||
|
||||
## Build and CI
|
||||
|
||||
Local commands (see `Justfile`): `just build`, `just run`, `just parse`,
|
||||
`just check`, `just clean`, `just preview-docs`. Bare `just` prints the
|
||||
command overview. The pinned shiroa lives in `tools/` (gitignored) and is
|
||||
downloaded on demand.
|
||||
|
||||
`.gitea/workflows/build.yml` (Gitea Actions, not GitLab CI) runs on push to
|
||||
`main` and on pull requests: installs shiroa, regenerates `acl.json`,
|
||||
compiles the book, runs `go vet`/`go test`, builds the static binary and
|
||||
uploads it as an artifact.
|
||||
|
||||
A rootless Docker/Podman image is available:
|
||||
|
||||
```sh
|
||||
just parse build-docs
|
||||
docker build -t yggdrasil .
|
||||
docker run --rm -p 8080:8080 --env-file .env yggdrasil
|
||||
```
|
||||
|
||||
Note that the binary embeds the site at build time: publishing new or
|
||||
edited pages always requires rebuilding and redeploying the server.
|
||||
|
||||
## Security notes and limitations
|
||||
|
||||
- Sessions live in memory: they vanish on restart and do not scale past
|
||||
one instance (users simply log in again).
|
||||
- Page *titles* and URLs of protected pages are visible in the sidebar of
|
||||
public pages (that is inherent to a static site). Page *content* is not:
|
||||
neither the HTML nor the renderer data files can be fetched without
|
||||
authorization.
|
||||
- Directory listing is enabled (file names only, e.g. `/thermo/`).
|
||||
- Logout is a GET request (`/logout`), like most documentation sites.
|
||||
- No search is implemented (`searchindex.json` is explicitly blocked).
|
||||
- Static assets referenced by pages (theme CSS/JS, fonts, renderer wasm)
|
||||
are public; they contain no page content.
|
||||
|
||||
## Legacy documentation
|
||||
|
||||
The previous documentation base lives in `legacy_docs/` and is kept only
|
||||
for reference; migrate pages into `docs/` manually, then delete the folder.
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"/homelab/backup.html": [
|
||||
"family"
|
||||
],
|
||||
"/security/ssh.html": [
|
||||
"admin"
|
||||
],
|
||||
"/thermo/fluids.html": [
|
||||
"family",
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Command parser scans a directory of Typst sources (a shiroa book) for
|
||||
// access-group tags and generates the acl.json consumed by the server.
|
||||
//
|
||||
// A page declares its access groups with a Typst let binding at the top of
|
||||
// the file:
|
||||
//
|
||||
// #let access-groups = ("family", "admin")
|
||||
//
|
||||
// Pages without a tag are public. The generated acl.json maps the final URL
|
||||
// of each page (e.g. "/thermo/fluids.html") to its authorized groups.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Matches: #let access-groups = ("group1", "group2")
|
||||
var accessGroupsRe = regexp.MustCompile(`(?m)^\s*#let\s+access-groups\s*=\s*\(([^)]*)\)`)
|
||||
|
||||
// Files that are never pages: book metadata, page layout, and partials
|
||||
// (files starting with "_") imported by other pages.
|
||||
func isSkipped(base string) bool {
|
||||
return base == "book.typ" || base == "template.typ" || strings.HasPrefix(base, "_")
|
||||
}
|
||||
|
||||
func main() {
|
||||
inDir := flag.String("in", "docs", "input directory containing the shiroa book (.typ sources)")
|
||||
outFile := flag.String("out", "acl.json", "output acl.json file")
|
||||
flag.Parse()
|
||||
|
||||
acl := make(map[string][]string)
|
||||
|
||||
err := filepath.WalkDir(*inDir, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := d.Name()
|
||||
if d.IsDir() {
|
||||
// Skip build output and hidden directories.
|
||||
if p != *inDir && (base == "dist" || strings.HasPrefix(base, ".")) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(base) != ".typ" || isSkipped(base) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %s: %w", p, err)
|
||||
}
|
||||
groups := extractGroups(content)
|
||||
if len(groups) == 0 {
|
||||
return nil // public page, no rule needed
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(*inDir, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// docs/<path>.typ compiles to dist/<path>.html, served at /<path>.html
|
||||
urlPath := "/" + strings.TrimSuffix(filepath.ToSlash(relPath), ".typ") + ".html"
|
||||
|
||||
acl[urlPath] = groups
|
||||
log.Printf("protected %s -> %s", urlPath, strings.Join(groups, ", "))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("scanning %s: %v", *inDir, err)
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(acl, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("encoding acl: %v", err)
|
||||
}
|
||||
out = append(out, '\n')
|
||||
if err := os.WriteFile(*outFile, out, 0o644); err != nil {
|
||||
log.Fatalf("writing %s: %v", *outFile, err)
|
||||
}
|
||||
|
||||
fmt.Printf("generated %s (%d protected pages)\n", *outFile, len(acl))
|
||||
}
|
||||
|
||||
func extractGroups(content []byte) []string {
|
||||
m := accessGroupsRe.FindSubmatch(content)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
var groups []string
|
||||
for _, part := range strings.Split(string(m[1]), ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
part = strings.Trim(part, "\"'")
|
||||
if part != "" {
|
||||
groups = append(groups, part)
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// Command server is the Yggdrasil web server: a single static binary that
|
||||
// embeds the compiled documentation (dist/) and its access control list
|
||||
// (acl.json), serves public pages directly, and gates protected pages
|
||||
// behind OIDC authentication (Authentik).
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Klagarge/yggdrasil"
|
||||
"github.com/Klagarge/yggdrasil/internal/acl"
|
||||
"github.com/Klagarge/yggdrasil/internal/middleware"
|
||||
"github.com/Klagarge/yggdrasil/internal/oidc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
listenAddr := envOr("LISTEN_ADDR", ":8080")
|
||||
sessionTTL := envDuration("SESSION_TTL", 12*time.Hour)
|
||||
cookieSecure := envOr("COOKIE_SECURE", "true") == "true"
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
list, err := acl.Load(yggdrasil.ACLData())
|
||||
if err != nil {
|
||||
log.Fatalf("refusing to start: %v", err) // fail closed
|
||||
}
|
||||
|
||||
sessions := middleware.NewSessionStore(sessionTTL)
|
||||
go sessions.StartJanitor(ctx, 10*time.Minute)
|
||||
|
||||
auth := setupOIDC(ctx)
|
||||
|
||||
dist, err := yggdrasil.DistFS()
|
||||
if err != nil {
|
||||
log.Fatalf("embedded dist: %v", err)
|
||||
}
|
||||
|
||||
gate := middleware.NewAccessControl(list, sessions, auth, cookieSecure)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /login", gate.Login)
|
||||
mux.HandleFunc("GET /callback", gate.Callback)
|
||||
mux.HandleFunc("GET /logout", gate.Logout)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
// The search index contains the text of protected pages: never serve it.
|
||||
mux.HandleFunc("/searchindex.json", http.NotFound)
|
||||
mux.Handle("/", gate.Wrap(http.FileServerFS(dist)))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: gate.SecurityHeaders(mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 2 * time.Minute,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.ListenAndServe() }()
|
||||
log.Printf("listening on %s (%d protected pages)", listenAddr, len(list))
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Printf("shutdown: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupOIDC builds the OIDC client from the environment. Without OIDC
|
||||
// configuration the server still serves public pages; protected pages
|
||||
// answer 503 instead of being exposed.
|
||||
func setupOIDC(ctx context.Context) *oidc.Authenticator {
|
||||
cfg, err := oidc.LoadConfigFromEnv()
|
||||
if err != nil {
|
||||
log.Printf("OIDC disabled (%v) - protected pages will answer 503", err)
|
||||
return nil
|
||||
}
|
||||
auth, err := oidc.NewAuthenticator(ctx, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("OIDC setup: %v", err)
|
||||
}
|
||||
log.Printf("OIDC enabled (issuer: %s)", cfg.IssuerURL)
|
||||
return auth
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envDuration(key string, def time.Duration) time.Duration {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
return d
|
||||
}
|
||||
log.Printf("invalid %s %q, using %s", key, v, def)
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Computer Science</title><meta content="Placeholder page for the Computer Science category. The real content comes later." name=description><meta content="Placeholder page for the Computer Science category. The real content comes later." property=og:description><meta content="Placeholder page for the Computer Science category. The real content comes later." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Computer Science | Yggdrasil" property=og:title><meta content="Computer Science | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/cs/ rel=canonical><meta content=https://yggdrasil.kb28.ch/cs/ property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svLine truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
#set document(title: [Computer Science])
|
||||
#metadata((title: "Overview", translation_key: "cs-index")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Placeholder page for the Computer Science category.
|
||||
The real content comes later.
|
||||
@@ -1,22 +0,0 @@
|
||||
#import "@preview/shiroa:0.4.0": *
|
||||
#show: book
|
||||
|
||||
#book-meta(
|
||||
title: "Yggdrasil",
|
||||
authors: ("Klagarge",),
|
||||
summary: [
|
||||
#prefix-chapter("index.typ")[Home]
|
||||
|
||||
= Thermodynamics
|
||||
- #chapter("thermo/heat-pumps.typ", section: "1")[Heat pumps]
|
||||
- #chapter("thermo/fluids.typ", section: "2")[Fluid mechanics]
|
||||
|
||||
= Security
|
||||
- #chapter("security/opsec.typ", section: "3")[Opsec basics]
|
||||
- #chapter("security/ssh.typ", section: "4")[SSH hardening]
|
||||
|
||||
= Homelab
|
||||
- #chapter("homelab/self-hosting.typ", section: "5")[Self-hosting]
|
||||
- #chapter("homelab/backup.typ", section: "6")[Backups]
|
||||
],
|
||||
)
|
||||
@@ -1,11 +0,0 @@
|
||||
// Access control: only members of these Authentik groups can read this page.
|
||||
// Delete this line to make the page public.
|
||||
#let access-groups = ("family",)
|
||||
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= Backups
|
||||
|
||||
Protected page placeholder about backups: the 3-2-1 rule, restic,
|
||||
off-site replication...
|
||||
@@ -1,7 +0,0 @@
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= Self-hosting
|
||||
|
||||
Public page placeholder about self-hosting: Docker/Podman, reverse
|
||||
proxies, DNS, VLANs...
|
||||
@@ -1,17 +0,0 @@
|
||||
#import "template.typ": project
|
||||
#show: project
|
||||
|
||||
= Yggdrasil
|
||||
|
||||
Welcome to the Yggdrasil knowledge base, built with Typst, Shiroa and a Go
|
||||
server.
|
||||
|
||||
*How it works:*
|
||||
|
||||
- Pages are written in Typst under `docs/` and compiled to static HTML.
|
||||
- A Go server embeds the HTML and an access-control list (`acl.json`).
|
||||
- Pages tagged with `#let access-groups = ("family", "admin")` are only
|
||||
visible to authenticated users in those groups; everything else is
|
||||
public.
|
||||
|
||||
Browse the categories in the sidebar to get started.
|
||||
@@ -1,7 +0,0 @@
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= Opsec basics
|
||||
|
||||
Public page placeholder about operational security: threat models,
|
||||
password managers, 2FA...
|
||||
@@ -1,11 +0,0 @@
|
||||
// Access control: only members of these Authentik groups can read this page.
|
||||
// Delete this line to make the page public.
|
||||
#let access-groups = ("admin",)
|
||||
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= SSH hardening
|
||||
|
||||
Protected page placeholder about SSH: key management, ciphers,
|
||||
session hardening, bastion hosts...
|
||||
@@ -1,44 +0,0 @@
|
||||
#import "@preview/shiroa:0.4.0": get-page-width, is-html-target, is-pdf-target, is-web-target, templates, x-target
|
||||
#import templates: *
|
||||
|
||||
// Metadata
|
||||
#let page-width = get-page-width()
|
||||
#let is-html-target = is-html-target() // target.starts-with("html")
|
||||
#let is-pdf-target = is-pdf-target() // target.starts-with("pdf")
|
||||
#let is-web-target = is-web-target() // target.starts-with("web")
|
||||
|
||||
#let project(body) = {
|
||||
// set web/pdf page properties
|
||||
set page(
|
||||
width: page-width,
|
||||
// for a website, we don't need pagination.
|
||||
height: auto,
|
||||
) if is-pdf-target or is-web-target
|
||||
|
||||
// remove margins for web target
|
||||
set page(margin: (
|
||||
// reserved beautiful top margin
|
||||
top: 20pt,
|
||||
// Typst is setting the page's bottom to the baseline of the last line of text. So bad :(.
|
||||
bottom: 0.5em,
|
||||
// remove rest margins.
|
||||
rest: 0pt,
|
||||
)) if is-web-target
|
||||
|
||||
let web-theme = if x-target.starts-with("html") and not x-target.starts-with("html-wrapper") {
|
||||
"starlight"
|
||||
} else {
|
||||
"mdbook"
|
||||
}
|
||||
show: if web-theme == "starlight" {
|
||||
import "@preview/shiroa-starlight:0.4.0": starlight
|
||||
starlight.with(include "book.typ")
|
||||
} else if web-theme == "mdbook" {
|
||||
import "@preview/shiroa-mdbook:0.4.0": mdbook
|
||||
mdbook.with(include "book.typ")
|
||||
} else {
|
||||
panic("Unknown web theme: " + web-theme)
|
||||
}
|
||||
|
||||
body
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Access control: only members of these Authentik groups can read this page.
|
||||
// Delete this line to make the page public.
|
||||
#let access-groups = ("family", "admin")
|
||||
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= Fluid mechanics
|
||||
|
||||
Protected page placeholder about fluid mechanics: statics, dynamics,
|
||||
Bernoulli's principle, pressure losses...
|
||||
@@ -1,7 +0,0 @@
|
||||
#import "../template.typ": project
|
||||
#show: project
|
||||
|
||||
= Heat pumps
|
||||
|
||||
Public page placeholder about heat pumps: COP, seasonal performance,
|
||||
refrigerant cycles...
|
||||
@@ -1,26 +0,0 @@
|
||||
// Package yggdrasil embeds the build artifacts: the static site compiled by
|
||||
// shiroa (dist/) and the access control list (acl.json). Embedding them at
|
||||
// the repository root lets //go:embed see both directories relative to this
|
||||
// file; `go build` requires acl.json and dist/ to exist (see the Justfile).
|
||||
package yggdrasil
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed all:dist
|
||||
var distFS embed.FS
|
||||
|
||||
//go:embed acl.json
|
||||
var aclData []byte
|
||||
|
||||
// DistFS returns the embedded static site.
|
||||
func DistFS() (fs.FS, error) {
|
||||
return fs.Sub(distFS, "dist")
|
||||
}
|
||||
|
||||
// ACLData returns the embedded acl.json.
|
||||
func ACLData() []byte {
|
||||
return aclData
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module github.com/Klagarge/yggdrasil
|
||||
|
||||
go 1.27.1
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc/v3 v3.21.0
|
||||
golang.org/x/oauth2 v0.37.0
|
||||
)
|
||||
|
||||
require github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
@@ -1,6 +0,0 @@
|
||||
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Cooking</title><meta content="Placeholder page — the real content comes later." name=description><meta content="Placeholder page — the real content comes later." property=og:description><meta content="Placeholder page — the real content comes later." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Cooking | Yggdrasil" property=og:title><meta content="Cooking | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/home/cooking.html rel=canonical><meta content=https://yggdrasil.kb28.ch/home/cooking.html property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' Line truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
#set document(title: [Cooking])
|
||||
#metadata((title: "Cooking", translation_key: "home-cooking")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Placeholder page — the real content comes later.
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Gardening</title><meta content="Some plants description and advices for keep them alive" name=description><meta content="Some plants description and advices for keep them alive" property=og:description><meta content="Some plants description and advices for keep them alive" name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Gardening | Yggdrasil" property=og:title><meta content="Gardening | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/home/gardening.html rel=canonical><meta content=https://yggdrasil.kb28.ch/home/gardening.html property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='httLine truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
#import "/lib/lib.typ": *
|
||||
#set document(title: [Gardening])
|
||||
#metadata((
|
||||
title: "Gardening",
|
||||
translation_key: "home-gardening",
|
||||
date: "2026-10-11",
|
||||
tags: ("plants", "yucca", "dracaena", "light", "water"),
|
||||
author: "Rémi Herdero",
|
||||
summary: "Some plants description and advices for keep them alive"
|
||||
)) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
= Yucca elephantipes <home:gardening:yucca>
|
||||
#calepin.elements.columns(
|
||||
columns: 2,
|
||||
wrap: true,
|
||||
[
|
||||
Yucca (@fig:plant:yucca_elephantipes) from central America is accustomed to arid conditions.
|
||||
|
||||
#tag(fill: yellow.lighten(30%))[Light ++]
|
||||
#tag(fill: blue.lighten(50%))[Water --]
|
||||
|
||||
This plant love light and should be placed on one of the most brightful parts of the room. It even tolerates direct sunlight very well.
|
||||
|
||||
The Yucca stores water in its thick trunk. You need to let the soil dry out almost completely between waterings. Generally speaking, watering once *every 2 to 3 weeks* is more than enough. It's better to forget about it for a while than to overwater it, otherwise its roots will decay. Usually we can put around #frackable(1, 2) liter every 2 Weeks. But it's important to check the dirt is dry before watering.
|
||||
|
||||
],[
|
||||
#figure(
|
||||
image("assets/yucca_elephantipes.jpg", width: 60%),
|
||||
caption: "Yucca elephantipes"
|
||||
) <fig:plant:yucca_elephantipes>
|
||||
]
|
||||
)
|
||||
|
||||
= Dracaena marginata <home:gardening:dracaena>
|
||||
#calepin.elements.columns(
|
||||
columns: 2,
|
||||
wrap: true,
|
||||
[
|
||||
The Dracaena (@fig:plant:dracaena_marginata) come from Madagascar.
|
||||
|
||||
#tag(fill: yellow.lighten(30%))[Light +]
|
||||
#tag(fill: blue.lighten(50%))[Water -]
|
||||
|
||||
This plant prefer lot of indirect light. Avoid to place it in direct sunlight because it can burn its leaves.
|
||||
|
||||
The Dracaena don't need a lot of water. Before watering, make sure the first 3 or 4 centimeters of soil are dry. In average, water the plant once with #frackable(1, 2) litre *every 10 to 14 days*. The extremity of the leaves tend to turn brown when the air is too dry or the water contain too much calcium or chlorine.
|
||||
|
||||
], [
|
||||
#figure(
|
||||
image("assets/dracaena_marginata.jpg", width: 60%),
|
||||
caption: [Dracaena marginata]
|
||||
) <fig:plant:dracaena_marginata>
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Home</title><meta content="Placeholder page for the home category: cooking, washing clothes, gardening. The real content comes later." name=description><meta content="Placeholder page for the home category: cooking, washing clothes, gardening. The real content comes later." property=og:description><meta content="Placeholder page for the home category: cooking, washing clothes, gardening. The real content comes later." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Home | Yggdrasil" property=og:title><meta content="Home | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/home/ rel=canonical><meta content=https://yggdrasil.kb28.ch/home/ property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline pointsLine truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
#set document(title: [Home])
|
||||
#metadata((title: "Overview", translation_key: "home-index")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Placeholder page for the home category: cooking, washing clothes, gardening.
|
||||
The real content comes later.
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Backup</title><meta content="Placeholder page — the real content comes later." name=description><meta content="Placeholder page — the real content comes later." property=og:description><meta content="Placeholder page — the real content comes later." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="Backup | Yggdrasil" property=og:title><meta content="Backup | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/homelab/backup.html rel=canonical><meta content=https://yggdrasil.kb28.ch/homelab/backup.html property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'Line truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,6 @@
|
||||
#set document(title: [Backup])
|
||||
#metadata((title: "Backup", translation_key: "homelab-backup")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Placeholder page — the real content comes later.
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>HomeLab</title><meta content="Placeholder page for the homelab category. The real content comes later." name=description><meta content="Placeholder page for the homelab category. The real content comes later." property=og:description><meta content="Placeholder page for the homelab category. The real content comes later." name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content="HomeLab | Yggdrasil" property=og:title><meta content="HomeLab | Yggdrasil" name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/homelab/ rel=canonical><meta content=https://yggdrasil.kb28.ch/homelab/ property=og:url><link href=../assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/svLine truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
#set document(title: [HomeLab])
|
||||
#metadata((title: "Overview", translation_key: "homelab")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Placeholder page for the homelab category.
|
||||
The real content comes later.
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html><html lang=en><head><meta charset=utf-8><meta content="width=device-width,initial-scale=1" name=viewport><title>Yggdrasil</title><meta content="Welcome to Yggdrasil, my personal knowledge base. Pick a master category from the menu" name=description><meta content="Welcome to Yggdrasil, my personal knowledge base. Pick a master category from the menu" property=og:description><meta content="Welcome to Yggdrasil, my personal knowledge base. Pick a master category from the menu" name=twitter:description><meta content=Yggdrasil property=og:site_name><meta content=Yggdrasil property=og:title><meta content=Yggdrasil name=twitter:title><meta content=article property=og:type><meta content=summary name=twitter:card><link href=https://yggdrasil.kb28.ch/ rel=canonical><meta content=https://yggdrasil.kb28.ch/ property=og:url><link href=assets/yggdrasil.ico rel=icon><meta content="light dark" name=color-scheme><script data-calepin-theme-param=theme data-calepin-theme-storage-key=calepin-website-theme>(()=>{let currentScript=document.currentScript,storageKey=currentScript?.dataset?.calepinThemeStorageKey||`calepin-website-theme`,param=currentScript?.dataset?.calepinThemeParam||`theme`,normalize=value=>value===`light`||value===`dark`?value:``,root=document.documentElement,media=window.matchMedia?window.matchMedia(`(prefers-color-scheme: dark)`):null,systemTheme=()=>media&&media.matches?`dark`:`light`,mode=``;if(param)try{mode=normalize(new URL(window.location.href).searchParams.get(param)||``)}catch{mode=``}if(!mode)try{mode=normalize(localStorage.getItem(storageKey)||``)}catch{mode=``}let theme=mode||systemTheme();root.dataset.calepinThemeMode=mode,root.dataset.theme=theme,root.style.colorScheme=theme})();</script><style>:host,:root{--pico-font-family-emoji:"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--pico-font-family-sans-serif:system-ui,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,Helvetica,Arial,"Helvetica Neue",sans-serif,var(--pico-font-family-emoji);--pico-font-family-monospace:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace,var(--pico-font-family-emoji);--pico-font-family:var(--pico-font-family-sans-serif);--pico-line-height:1.5;--pico-font-weight:400;--pico-font-size:100%;--pico-text-underline-offset:.1rem;--pico-border-radius:.25rem;--pico-border-width:.0625rem;--pico-outline-width:.125rem;--pico-transition:.2s ease-in-out;--pico-spacing:1rem;--pico-typography-spacing-vertical:1rem;--pico-block-spacing-vertical:var(--pico-spacing);--pico-block-spacing-horizontal:var(--pico-spacing);--pico-grid-column-gap:var(--pico-spacing);--pico-grid-row-gap:var(--pico-spacing);--pico-form-element-spacing-vertical:.75rem;--pico-form-element-spacing-horizontal:1rem;--pico-group-box-shadow:0 0 0 #0000;--pico-group-box-shadow-focus-with-button:0 0 0 var(--pico-outline-width) var(--pico-primary-focus);--pico-group-box-shadow-focus-with-input:0 0 0 .0625rem var(--pico-form-element-border-color);--pico-modal-overlay-backdrop-filter:blur(.375rem);--pico-nav-element-spacing-vertical:1rem;--pico-nav-element-spacing-horizontal:.5rem;--pico-nav-link-spacing-vertical:.5rem;--pico-nav-link-spacing-horizontal:.5rem;--pico-nav-breadcrumb-divider:">";--pico-icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--pico-icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--pico-icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--pico-icon-search:url("data:image/sLine truncated
|
||||
`:Array.from(node.childNodes).map(codeText).join(``):``:``}document.querySelectorAll(selector).forEach(block=>{if(block.dataset.calepinCopyBound===`true`||block.querySelector(`:scope > .${buttonClass}`))return;block.dataset.calepinCopyBound=`true`;let button=document.createElement(`button`);button.type=`button`,button.className=buttonClass,button.setAttribute(`aria-label`,`Copy code`),button.setAttribute(`title`,`Copy code`),button.innerHTML=icon;let restoreTimeout=null;button.addEventListener(`click`,async()=>{restoreTimeout!==null&&(window.clearTimeout(restoreTimeout),restoreTimeout=null);let text=codeText(block.querySelector(`pre code, code, pre`));try{await copyText(text),button.classList.add(copiedClass),button.innerHTML=copiedIcon,restoreTimeout=window.setTimeout(()=>{button.classList.remove(copiedClass),button.innerHTML=icon,restoreTimeout=null},900)}catch{button.classList.remove(copiedClass),button.innerHTML=icon}}),block.prepend(button)})})();</script><script>(()=>{let pagefindModal=()=>document.querySelector(`pagefind-modal`);function modalSearchInput(modal){return modal?modal.querySelector(`pagefind-input input, input[type='search'], input`):null}function setModalSearchQuery(modal,query){if(!query)return!1;let input=modalSearchInput(modal);return input?(input.value=query,input.dispatchEvent(new Event(`input`,{bubbles:!0})),!0):!1}function focusModalSearch(modal){let input=modalSearchInput(modal);return input?(input.focus(),!0):!1}async function openPagefindSearch(query){let modal=pagefindModal();if(!modal)return!1;if(typeof modal.open!=`function`&&window.customElements?.whenDefined)try{await window.customElements.whenDefined(`pagefind-modal`)}catch{}if(typeof modal.open!=`function`)return!1;modal.open();let sync=()=>{let hasQuery=setModalSearchQuery(modal,query),hasFocus=focusModalSearch(modal);return hasQuery||hasFocus};return sync()||requestAnimationFrame(()=>{sync()||window.setTimeout(sync,50)}),!0}function isTextEntryElement(element){return element?element.isContentEditable||element.tagName===`INPUT`||element.tagName===`SELECT`||element.tagName===`TEXTAREA`:!1}let SEARCH_SUGGESTION_LIMIT=20,SEARCH_SUBRESULT_LIMIT=3,SEARCH_SUGGESTION_MIN_LENGTH=2,SEARCH_SUGGESTION_DELAY=160,pagefindModulePromise=null;function pagefindBundlePath(){let script=document.querySelector(`script[src$="pagefind-component-ui.js"], script[src$="pagefind-ui.js"]`);return script?.src?new URL(`./`,script.src).toString():new URL(`pagefind/`,window.location.href).toString()}function loadPagefindModule(){return pagefindModulePromise||=import(`${pagefindBundlePath()}pagefind.js`),pagefindModulePromise}function plainTextFromHtml(html){let element=document.createElement(`div`);return element.innerHTML=html||``,element.textContent?.trim()||``}function setSearchResultsOpen(input,results,isOpen){results.hidden=!isOpen,input.setAttribute(`aria-expanded`,String(isOpen))}function clearSearchResults(input,results){results.innerHTML=``,setSearchResultsOpen(input,results,!1)}function renderSearchMessage(input,results,message){results.innerHTML=``;let item=document.createElement(`p`);item.className=`calepin-website-search-empty`,item.textContent=message,results.appendChild(item),setSearchResultsOpen(input,results,!0)}function renderSearchResults(input,results,items){if(results.innerHTML=``,!items.length){renderSearchMessage(input,results,`No results`);return}let basePathPrefix=localCanonicalBasePathPrefix(),addSearchLink=(item,className,titleClassName,excerptClassName)=>{let href=hrefWithoutLocalBasePath(item.meta?.url||item.url||``,basePathPrefix);if(!href)return null;let link=document.createElement(`a`);link.className=className,link.href=href,link.setAttribute(`role`,`option`);let title=document.createElement(`span`);title.className=titleClassName,title.textContent=item.meta?.title||item.title||href,link.appendChild(title);let excerptText=plainTextFromHtml(item.excerpt||item.content||``);if(excerptText){let excerpt=document.createElement(`span`);excerpt.className=excerptClassName,excerpt.textContent=excerptText,link.appendChild(excerpt)}return link};for(let item of items){let link=addSearchLink(item,`calepin-website-search-result`,`calepin-website-search-result-title`,`calepin-website-search-result-excerpt`);if(!link)continue;results.appendChild(link);let subResults=Array.isArray(item.sub_results)?item.sub_results.slice(0,3):[];if(!subResults.length)continue;let nested=document.createElement(`div`);nested.className=`calepin-website-search-subresults`;for(let subResult of subResults){let subLink=addSearchLink(subResult,`calepin-website-search-subresult`,`calepin-website-search-subresult-title`,`calepin-website-search-subresult-excerpt`);subLink&&nested.appendChild(subLink)}nested.children.length&&results.appendChild(nested)}setSearchResultsOpen(input,results,!!results.children.length)}function initMobileSearchTriggers(){document.querySelectorAll(`[data-calepin-mobile-search-trigger]`).forEach(button=>{button.dataset.calepinSearchBound!==Line truncated
|
||||
*
|
||||
* One module-local script wires: view switcher (HTML/Source/PDF), the mobile
|
||||
* sidebar drawer, sidebar section folding, inline SVGs, internal link state
|
||||
* preservation, the native <dialog> video lightbox, and the Pagefind navbar
|
||||
* search bridge.
|
||||
*
|
||||
* The Rust theme loader links this file near the end of the page body, so
|
||||
* the DOM already exists when it runs and no DOMContentLoaded guard is
|
||||
* needed. */
|
||||
(()=>{if(!document.querySelector(`.calepin-website-topbar, .calepin-website-main, pagefind-modal, img[data-inline-svg], [data-video-dialog], [data-lightbox-dialog]`))return;let VIEW={param:`view`,rendered:`rendered`,source:`source`,pdf:`pdf`},MOBILE_QUERY=`(max-width: 56rem)`,SOURCE_DATA_ID=`calepin-website-source-data`,isMobile=()=>window.matchMedia(`(max-width: 56rem)`).matches,isPlainLeftClick=event=>event&&event.button===0&&!event.metaKey&&!event.ctrlKey&&!event.shiftKey&&!event.altKey,isInternalPageHref=href=>!!href&&!href.startsWith(`#`)&&!href.startsWith(`http://`)&&!href.startsWith(`https://`)&&!href.startsWith(`//`)&&!href.startsWith(`mailto:`)&&!href.startsWith(`tel:`);function setUrlParam(url,name,value){value?url.searchParams.set(name,value):url.searchParams.delete(name)}function readUrlParam(name,normalize,fallback=``){try{return normalize((new URL(window.location.href).searchParams.get(name)||``).toLowerCase())}catch{return fallback}}function updateCurrentUrlParam(name,value){try{let url=new URL(window.location.href);setUrlParam(url,name,value),window.history.replaceState({},``,url.toString())}catch{}}let getMain=()=>document.querySelector(`.calepin-website-main`),normalizeView=value=>value===VIEW.source||value===VIEW.pdf?value:VIEW.rendered;function currentView(selects){let select=Array.isArray(selects)?selects[0]:selects;return normalizeView(select&&select.value||VIEW.rendered)}function pageAssetCandidates(extension){let{pathname}=window.location;return pathname.endsWith(`/`)?[`${pathname}index.${extension}`]:pathname.endsWith(`.html`)?[`${pathname.slice(0,-5)}.${extension}`]:!pathname||pathname.endsWith(`.`)?[`index.${extension}`]:pathname.includes(`.`)?[`${pathname}.${extension}`]:[`${pathname}/index.${extension}`]}function replaceMainContent(node){let main=getMain();return main?(main.innerHTML=``,main.appendChild(node),!0):!1}function showMainFallback(message){let main=getMain();if(!main)return;let fallback=document.createElement(`p`);fallback.textContent=message,main.prepend(fallback)}function renderSourceText(text){let container=document.createElement(`div`);container.className=`calepin-website-source-view`;let pre=document.createElement(`pre`),code=document.createElement(`code`);code.className=`language-typ`,code.textContent=text,pre.appendChild(code),container.appendChild(pre),replaceMainContent(container)}async function renderSourceMode(){let element=document.getElementById(`calepin-website-source-data`);if(element)try{let inline=JSON.parse(element.textContent||``);if(typeof inline==`string`&&inline.length>0){renderSourceText(inline);return}}catch{}for(let sourcePath of pageAssetCandidates(`typ`))try{let response=await fetch(sourcePath);if(!response.ok)continue;renderSourceText(await response.text());return}catch{}showMainFallback(`Could not load source view.`)}async function renderPdfMode(){let isLocal=window.location.protocol===`file:`;for(let pdfPath of pageAssetCandidates(`pdf`)){if(!isLocal)try{let response=await fetch(pdfPath,{method:`HEAD`});if(!response.ok&&response.status!==405)continue}catch{}let viewer=document.createElement(`iframe`);viewer.className=`calepin-website-pdf-viewer`;try{viewer.src=new URL(pdfPath,window.location.href).toString()}catch{viewer.src=pdfPath}viewer.setAttribute(`title`,`Page PDF view`),viewer.setAttribute(`loading`,`lazy`),replaceMainContent(viewer);return}showMainFallback(`Could not load PDF view.`)}function applyView(mode,selects){let value=normalizeView(mode);(Array.isArray(selects)?selects:[selects]).filter(Boolean).forEach(select=>{select.value=value}),updateCurrentUrlParam(VIEW.param,value===VIEW.rendered?``:value),value===VIEW.source?renderSourceMode():value===VIEW.pdf&&renderPdfMode()}function initView(selects){let viewSelects=(Array.isArray(selects)?selects:[selects]).filter(Boolean);applyView(readUrlParam(VIEW.param,normalizeView,VIEW.rendered),viewSelects),viewSelects.forEach(select=>{select.dataset.calepinBound||(select.dataset.calepinBound=`true`,select.addEventListener(`change`,event=>{let value=normalizeView(event.target.value),url=new URL(window.location.href);setUrlParam(url,VIEW.param,value===VIEW.rendered?``:value),window.location.href=url.toString()}))})}
|
||||
// ----------------------------------------------------------- nav drawer
|
||||
function createNav(){let shell=document.querySelector(`.calepin-website-shell`),backdrop=document.querySelector(`.calepin-website-nav-backdrop`),toggle=document.querySelector(`.calepin-website-nav-toggle`);function setOpen(isOpen){!shell||!toggle||(shell.classList.toggle(`nav-open`,isOpen),document.body.classList.toggle(`calepin-website-nav-open`,isOpen),toggle.setAttribute(`aria-expanded`,String(isOpen)),backdrop&&backdrop.setAttribute(`aria-hidden`,String(!isOpen)))}let close=()=>setOpen(!1);return toggle&&toggle.addEventListener(`click`,event=>{event.preventDefault(),isMobile()&&shell&&setOpen(!shell.classList.contains(`nav-open`))}),backdrop?.addEventListener(`click`,close),document.addEventListener(`keydown`,event=>{event.key===`Escape`&&close()}),window.addEventListener(`resize`,()=>{isMobile()||close()}),{close}}
|
||||
// ------------------------------------------------------ sidebar sections
|
||||
// Only one section is open at a time, but "section" means siblings: opening a
|
||||
// nested subsection must not fold the parent it lives in, and must only fold
|
||||
// its own siblings.
|
||||
function initSidebarSections(){let sections=Array.from(document.querySelectorAll(`.calepin-website-sidebar-section`));sections.length<2||sections.forEach(section=>{section.addEventListener(`toggle`,()=>{section.open&§ions.forEach(other=>{other!==section&&other.parentElement===section.parentElement&&other.removeAttribute(`open`)})})})}
|
||||
// ------------------------------------------------------------- inline svg
|
||||
function svgTextFromSource(src){if(!src)return Promise.resolve(``);let prefix=`data:image/svg+xml`;if(src.startsWith(prefix)){let payload=src.slice(src.indexOf(`,`)+1);try{return Promise.resolve(src.startsWith(`${prefix};base64,`)?atob(payload):decodeURIComponent(payload))}catch{return Promise.resolve(payload)}}return fetch(src).then(response=>response.text()).catch(()=>``)}function inlineSvgImage(image){return svgTextFromSource(image.getAttribute(`src`)).then(text=>{if(!text)return;let svg=new DOMParser().parseFromString(text,`image/svg+xml`).querySelector(`svg`);if(!svg)return;svg.removeAttribute(`xmlns:xlink`),svg.removeAttribute(`width`),svg.removeAttribute(`height`),image.className&&svg.setAttribute(`class`,image.className),image.id&&svg.setAttribute(`id`,image.id);let style=image.getAttribute(`style`);style&&svg.setAttribute(`style`,style);let alt=image.getAttribute(`alt`);alt&&(svg.setAttribute(`aria-label`,alt),svg.setAttribute(`role`,`img`));let ariaHidden=image.getAttribute(`aria-hidden`);ariaHidden!==null&&svg.setAttribute(`aria-hidden`,ariaHidden),image.replaceWith(svg)})}function inlineSvgs(){document.querySelectorAll(`img[data-inline-svg]`).forEach(image=>inlineSvgImage(image))}
|
||||
// --------------------------------------------------------- link state
|
||||
function urlWithState(href,viewSelect){if(!isInternalPageHref(href))return href;try{let url=new URL(href,window.location.href),view=currentView(viewSelect);return setUrlParam(url,VIEW.param,view===VIEW.rendered?``:view),url.toString()}catch{return href}}function preserveStateInLinks(viewSelect){document.querySelectorAll(`a[href]`).forEach(link=>{let href=link.getAttribute(`href`);if(!isInternalPageHref(href))return;let next=urlWithState(href,viewSelect);next&&next!==href&&link.setAttribute(`href`,next)})}function initLinkInterception(nav,viewSelect){document.addEventListener(`click`,event=>{let tocLink=event.target.closest(`.calepin-website-toc a`);if(tocLink){let href=tocLink.getAttribute(`href`);if(href&&href.startsWith(`#`)){let target=null;try{target=document.querySelector(href)}catch{target=null}target&&target.scrollIntoView({behavior:`smooth`,block:`start`})}isMobile()&&nav.close();return}let link=event.target.closest(`a`);if(!link||!isPlainLeftClick(event))return;event.target.closest(`.calepin-website-sidebar a`)&&isMobile()&&nav.close();let href=link.getAttribute(`href`),next=urlWithState(href,viewSelect);next&&next!==href&&(event.preventDefault(),window.location.href=next)})}
|
||||
// ----------------------------------------------------- dialog lightbox
|
||||
function initDialogs(){document.querySelectorAll(`[data-video-dialog], [data-lightbox-dialog]`).forEach(trigger=>{let dialogId=trigger.dataset.videoDialog||trigger.dataset.lightboxDialog,dialog=document.getElementById(dialogId);!dialog||typeof dialog.showModal!=`function`||trigger.addEventListener(`click`,event=>{event.preventDefault(),dialog.showModal()})}),document.querySelectorAll(`dialog.calepin-video-dialog, dialog.calepin-screenshot-dialog`).forEach(dialog=>{let stop=()=>{let video=dialog.querySelector(`video`);video&&video.pause()};dialog.addEventListener(`click`,event=>{
|
||||
// Clicking the dialog backdrop (the dialog element itself) closes it.
|
||||
event.target===dialog&&dialog.close()}),dialog.querySelectorAll(`[data-close-dialog]`).forEach(button=>{button.addEventListener(`click`,()=>dialog.close())}),dialog.addEventListener(`close`,stop)})}
|
||||
// --------------------------------------------------------------- bootstrap
|
||||
let viewSelects=Array.from(document.querySelectorAll(`[data-calepin-view-mode], #calepin-website-view-mode`));initView(viewSelects);let nav=createNav();initSidebarSections(),inlineSvgs(),initDialogs(),preserveStateInLinks(viewSelects),initLinkInterception(nav,viewSelects),window.addEventListener(`pageshow`,event=>{event.persisted&&(initView(viewSelects),preserveStateInLinks(viewSelects))})})();</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
#set document(title: [Yggdrasil])
|
||||
#metadata((title: "Home", translation_key: "home", layout: "layouts/site-landing.html")) <website-metadata>
|
||||
|
||||
#title()
|
||||
|
||||
Welcome to Yggdrasil, my personal knowledge base.
|
||||
Pick a master category from the menu
|
||||
@@ -1,122 +0,0 @@
|
||||
// Package acl loads the access control list generated by cmd/parser and
|
||||
// answers authorization questions for the HTTP middleware.
|
||||
//
|
||||
// acl.json maps the URL of each protected page ("/thermo/fluids.html") to
|
||||
// the groups allowed to read it. URLs absent from the list are public.
|
||||
//
|
||||
// Keys are normalized to a "page key": the URL path reduced to the page it
|
||||
// belongs to, e.g. "/thermo/fluids.html" and "/thermo/fluids.html" both
|
||||
// become "/thermo/fluids". The shiroa renderer fetches the page content
|
||||
// from companion files such as "/thermo/fluids.light.multi.sir.in", which
|
||||
// reduce to the same key, so a protected page's content cannot be fetched
|
||||
// directly while the page itself is protected.
|
||||
package acl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ACL maps page keys ("/thermo/fluids") to the groups allowed to read them.
|
||||
type ACL map[string][]string
|
||||
|
||||
// Load parses acl.json and normalizes its keys. A malformed file is an error:
|
||||
// the caller must refuse to start rather than serve protected pages as
|
||||
// public (fail closed).
|
||||
func Load(data []byte) (ACL, error) {
|
||||
var raw map[string][]string
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return nil, fmt.Errorf("parsing acl.json: %w", err)
|
||||
}
|
||||
acl := make(ACL, len(raw))
|
||||
for urlPath, groups := range raw {
|
||||
if len(groups) == 0 {
|
||||
continue
|
||||
}
|
||||
key, err := PageKey(urlPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acl entry %q: %w", urlPath, err)
|
||||
}
|
||||
// Merge entries that reduce to the same page (e.g. a page and
|
||||
// its directory index): require the union of their groups.
|
||||
acl[key] = appendUnique(acl[key], groups...)
|
||||
}
|
||||
return acl, nil
|
||||
}
|
||||
|
||||
// PageKey reduces a URL path to the page it belongs to.
|
||||
//
|
||||
// "/thermo/fluids.html" -> "/thermo/fluids"
|
||||
// "/thermo/fluids.light.multi.sir.in" -> "/thermo/fluids"
|
||||
// "/" -> "/index"
|
||||
// "/thermo/" -> "/thermo/index"
|
||||
func PageKey(urlPath string) (string, error) {
|
||||
if !strings.HasPrefix(urlPath, "/") {
|
||||
return "", fmt.Errorf("not an absolute path")
|
||||
}
|
||||
// A request for a directory serves that directory's index page.
|
||||
if urlPath != "/" && strings.HasSuffix(urlPath, "/") {
|
||||
urlPath += "index.html"
|
||||
}
|
||||
p := path.Clean(urlPath)
|
||||
if p == "/" {
|
||||
return "/index", nil
|
||||
}
|
||||
dir, base := path.Split(p)
|
||||
if i := strings.Index(base, "."); i >= 0 {
|
||||
base = base[:i]
|
||||
}
|
||||
if base == "" {
|
||||
base = "index"
|
||||
}
|
||||
return path.Join(dir, base), nil
|
||||
}
|
||||
|
||||
// GroupsFor returns the union of the groups required by any of the given
|
||||
// page keys, and whether at least one of them is protected.
|
||||
func (a ACL) GroupsFor(keys ...string) (required []string, protected bool) {
|
||||
seen := make(map[string]struct{})
|
||||
for _, k := range keys {
|
||||
for _, g := range a[k] {
|
||||
if _, dup := seen[g]; !dup {
|
||||
seen[g] = struct{}{}
|
||||
required = append(required, g)
|
||||
}
|
||||
protected = true
|
||||
}
|
||||
}
|
||||
return required, protected
|
||||
}
|
||||
|
||||
// Allows reports whether the user's groups include at least one of the
|
||||
// required groups.
|
||||
func Allows(userGroups, required []string) bool {
|
||||
for _, u := range userGroups {
|
||||
for _, r := range required {
|
||||
if u == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendUnique(dst []string, groups ...string) []string {
|
||||
for _, g := range groups {
|
||||
if !contains(dst, g) {
|
||||
dst = append(dst, g)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func contains(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package acl
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPageKey(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"/", "/index"},
|
||||
{"/index.html", "/index"},
|
||||
{"/thermo/fluids.html", "/thermo/fluids"},
|
||||
// The renderer fetches content from theme variants; they must map
|
||||
// to the same page or the ACL would be bypassable.
|
||||
{"/thermo/fluids.light.multi.sir.in", "/thermo/fluids"},
|
||||
{"/thermo/fluids.ayu.multi.sir.in", "/thermo/fluids"},
|
||||
// Directory requests resolve to the directory index page.
|
||||
{"/thermo/", "/thermo/index"},
|
||||
{"/thermo", "/thermo"},
|
||||
// Path traversal is neutralized before matching.
|
||||
{"/thermo/../security/ssh.html", "/security/ssh"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got, err := PageKey(c.in); err != nil {
|
||||
t.Errorf("PageKey(%q) error: %v", c.in, err)
|
||||
} else if got != c.want {
|
||||
t.Errorf("PageKey(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
if _, err := PageKey("relative.html"); err == nil {
|
||||
t.Error("PageKey must reject relative paths")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAndCheck(t *testing.T) {
|
||||
// Keys in acl.json are final page URLs; Load normalizes them.
|
||||
data := []byte(`{
|
||||
"/thermo/fluids.html": ["family", "admin"],
|
||||
"/index.html": ["admin"],
|
||||
"/public.html": ["family"]
|
||||
}`)
|
||||
a, err := Load(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if got, _ := a.GroupsFor("/thermo/fluids"); len(got) != 2 {
|
||||
t.Errorf("expected 2 groups for /thermo/fluids, got %v", got)
|
||||
}
|
||||
if _, protected := a.GroupsFor("/thermo/fluids"); !protected {
|
||||
t.Error("/thermo/fluids should be protected")
|
||||
}
|
||||
if _, protected := a.GroupsFor("/thermo/heat-pumps"); protected {
|
||||
t.Error("/thermo/heat-pumps should be public")
|
||||
}
|
||||
if !Allows([]string{"family"}, []string{"family", "admin"}) {
|
||||
t.Error("family should be allowed")
|
||||
}
|
||||
if Allows([]string{"guest"}, []string{"family", "admin"}) {
|
||||
t.Error("guest should be denied")
|
||||
}
|
||||
if Allows(nil, []string{"admin"}) {
|
||||
t.Error("no groups should never be allowed on a protected page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMalformed(t *testing.T) {
|
||||
if _, err := Load([]byte("not json")); err == nil {
|
||||
t.Fatal("malformed acl.json must be an error (fail closed)")
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Package middleware implements the access-control layer of the server:
|
||||
// OIDC login/logout, cookie sessions, and the gate that decides for every
|
||||
// request whether its URL may be served to the caller.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Klagarge/yggdrasil/internal/acl"
|
||||
"github.com/Klagarge/yggdrasil/internal/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionCookie = "ygg_session"
|
||||
stateTTL = 10 * time.Minute
|
||||
)
|
||||
|
||||
// AccessControl bundles everything the gate needs.
|
||||
type AccessControl struct {
|
||||
ACL acl.ACL
|
||||
Sessions *SessionStore
|
||||
Auth *oidc.Authenticator // nil: OIDC disabled
|
||||
CookieSecure bool
|
||||
|
||||
stateMu sync.Mutex
|
||||
states map[string]loginState // pending OIDC flows, keyed by state
|
||||
}
|
||||
|
||||
// NewAccessControl wires the gate. auth may be nil (OIDC disabled): public
|
||||
// pages keep working and protected pages answer 503.
|
||||
func NewAccessControl(a acl.ACL, sessions *SessionStore, auth *oidc.Authenticator, cookieSecure bool) *AccessControl {
|
||||
return &AccessControl{
|
||||
ACL: a,
|
||||
Sessions: sessions,
|
||||
Auth: auth,
|
||||
CookieSecure: cookieSecure,
|
||||
states: make(map[string]loginState),
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap returns a handler that serves next only when the request is allowed.
|
||||
//
|
||||
// The request path is reduced to a page key (see acl.PageKey) and looked up
|
||||
// in the ACL. Public pages pass through; protected pages require a session
|
||||
// whose groups include at least one of the groups required by the ACL.
|
||||
func (ac *AccessControl) Wrap(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key, err := acl.PageKey(r.URL.Path)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid request path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
required, protected := ac.ACL.GroupsFor(key)
|
||||
if !protected {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ac.Auth == nil {
|
||||
http.Error(w, "this page is protected, but the server has no OIDC configuration",
|
||||
http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
sess, ok := ac.session(r)
|
||||
if !ok {
|
||||
http.Redirect(w, r, "/login?target="+url.QueryEscape(r.URL.RequestURI()), http.StatusFound)
|
||||
return
|
||||
}
|
||||
if !acl.Allows(sess.Groups, required) {
|
||||
http.Error(w, "403 Forbidden: your account does not have access to this page.",
|
||||
http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// SecurityHeaders sets conservative defaults on every response.
|
||||
func (ac *AccessControl) SecurityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := w.Header()
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Klagarge/yggdrasil/internal/oidc"
|
||||
)
|
||||
|
||||
// loginState remembers where a pending OIDC flow must return.
|
||||
type loginState struct {
|
||||
target string
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
// session reads the session cookie and returns the session if valid.
|
||||
func (ac *AccessControl) session(r *http.Request) (*Session, bool) {
|
||||
c, err := r.Cookie(sessionCookie)
|
||||
if err != nil || c.Value == "" {
|
||||
return nil, false
|
||||
}
|
||||
return ac.Sessions.Get(c.Value)
|
||||
}
|
||||
|
||||
func (ac *AccessControl) setSessionCookie(w http.ResponseWriter, id string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: id,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: ac.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: int(ac.Sessions.TTL() / time.Second),
|
||||
})
|
||||
}
|
||||
|
||||
func (ac *AccessControl) clearSessionCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookie,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: ac.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
// Login redirects to the OIDC provider, remembering where to return.
|
||||
func (ac *AccessControl) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if ac.Auth == nil {
|
||||
http.Error(w, "OIDC is not configured on this server", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
target := sanitizeTarget(r.URL.Query().Get("target"))
|
||||
state := randomID()
|
||||
ac.saveState(state, target)
|
||||
http.Redirect(w, r, ac.Auth.AuthCodeURL(state), http.StatusFound)
|
||||
}
|
||||
|
||||
// Callback finishes the OIDC flow: it verifies the provider's response and
|
||||
// starts a session.
|
||||
func (ac *AccessControl) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
if ac.Auth == nil {
|
||||
http.Error(w, "OIDC is not configured on this server", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
target, ok := ac.takeState(r.URL.Query().Get("state"))
|
||||
if !ok {
|
||||
http.Error(w, "unknown or expired login state; restart the login", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
http.Error(w, "missing authorization code", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
token, err := ac.Auth.Exchange(r.Context(), code)
|
||||
if err != nil {
|
||||
http.Error(w, "token exchange failed", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
idToken, err := ac.Auth.VerifyIDToken(r.Context(), token)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid identity token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
profile, err := oidc.ProfileFromToken(idToken)
|
||||
if err != nil {
|
||||
http.Error(w, "unreadable identity token claims", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
ac.setSessionCookie(w, ac.Sessions.Create(profile))
|
||||
http.Redirect(w, r, target, http.StatusFound)
|
||||
}
|
||||
|
||||
// Logout ends the session.
|
||||
func (ac *AccessControl) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
if c, err := r.Cookie(sessionCookie); err == nil && c.Value != "" {
|
||||
ac.Sessions.Delete(c.Value)
|
||||
}
|
||||
ac.clearSessionCookie(w)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (ac *AccessControl) saveState(state, target string) {
|
||||
ac.stateMu.Lock()
|
||||
now := time.Now()
|
||||
for k, v := range ac.states { // opportunistic purge
|
||||
if now.After(v.expires) {
|
||||
delete(ac.states, k)
|
||||
}
|
||||
}
|
||||
ac.states[state] = loginState{target: target, expires: now.Add(stateTTL)}
|
||||
ac.stateMu.Unlock()
|
||||
}
|
||||
|
||||
func (ac *AccessControl) takeState(state string) (string, bool) {
|
||||
if state == "" {
|
||||
return "", false
|
||||
}
|
||||
ac.stateMu.Lock()
|
||||
defer ac.stateMu.Unlock()
|
||||
s, ok := ac.states[state]
|
||||
if ok {
|
||||
delete(ac.states, state) // states are single use
|
||||
}
|
||||
if !ok || time.Now().After(s.expires) {
|
||||
return "", false
|
||||
}
|
||||
return s.target, true
|
||||
}
|
||||
|
||||
// sanitizeTarget only accepts local redirect targets (open-redirect guard).
|
||||
func sanitizeTarget(t string) string {
|
||||
if t == "" || !strings.HasPrefix(t, "/") || strings.HasPrefix(t, "//") || strings.HasPrefix(t, "/\\") {
|
||||
return "/"
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Klagarge/yggdrasil/internal/oidc"
|
||||
)
|
||||
|
||||
func TestSanitizeTarget(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "/",
|
||||
"/thermo/fluids.html": "/thermo/fluids.html",
|
||||
"/a b/c.html?x=1": "/a b/c.html?x=1",
|
||||
"//evil.example.com": "/",
|
||||
"/\\evil.example.com": "/",
|
||||
"https://evil.example.com": "/",
|
||||
"relative.html": "/",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := sanitizeTarget(in); got != want {
|
||||
t.Errorf("sanitizeTarget(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateRoundTrip(t *testing.T) {
|
||||
ac := NewAccessControl(nil, NewSessionStore(0), nil, false)
|
||||
|
||||
ac.saveState("s1", "/thermo/fluids.html")
|
||||
target, ok := ac.takeState("s1")
|
||||
if !ok || target != "/thermo/fluids.html" {
|
||||
t.Fatalf("state round trip failed: %q %v", target, ok)
|
||||
}
|
||||
// States are single use.
|
||||
if _, ok := ac.takeState("s1"); ok {
|
||||
t.Error("state must be single use")
|
||||
}
|
||||
// Unknown states are rejected (CSRF guard).
|
||||
if _, ok := ac.takeState("nope"); ok {
|
||||
t.Error("unknown state must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionExpiry(t *testing.T) {
|
||||
store := NewSessionStore(0) // zero TTL: expires immediately
|
||||
id := store.Create(oidc.Profile{Subject: "sub", Email: "user@example.com", Groups: []string{"family"}})
|
||||
if _, ok := store.Get(id); ok {
|
||||
t.Error("expired session must not validate")
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Klagarge/yggdrasil/internal/oidc"
|
||||
)
|
||||
|
||||
// Session is an authenticated user.
|
||||
type Session struct {
|
||||
Subject string
|
||||
Email string
|
||||
Name string
|
||||
Groups []string
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// SessionStore keeps sessions in memory. Sessions do not survive server
|
||||
// restarts; users simply log in again.
|
||||
type SessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*Session
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewSessionStore creates a store with the given session lifetime.
|
||||
func NewSessionStore(ttl time.Duration) *SessionStore {
|
||||
return &SessionStore{
|
||||
sessions: make(map[string]*Session),
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// TTL reports the session lifetime.
|
||||
func (s *SessionStore) TTL() time.Duration { return s.ttl }
|
||||
|
||||
// Create registers a new session and returns its ID.
|
||||
func (s *SessionStore) Create(p oidc.Profile) string {
|
||||
id := randomID()
|
||||
s.mu.Lock()
|
||||
s.sessions[id] = &Session{
|
||||
Subject: p.Subject,
|
||||
Email: p.Email,
|
||||
Name: p.Name,
|
||||
Groups: p.Groups,
|
||||
Expires: time.Now().Add(s.ttl),
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// Get returns the session for id if it exists and has not expired.
|
||||
func (s *SessionStore) Get(id string) (*Session, bool) {
|
||||
s.mu.RLock()
|
||||
sess, ok := s.sessions[id]
|
||||
s.mu.RUnlock()
|
||||
if !ok || time.Now().After(sess.Expires) {
|
||||
return nil, false
|
||||
}
|
||||
return sess, true
|
||||
}
|
||||
|
||||
// Delete removes a session.
|
||||
func (s *SessionStore) Delete(id string) {
|
||||
s.mu.Lock()
|
||||
delete(s.sessions, id)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// StartJanitor purges expired sessions periodically until ctx is done.
|
||||
// Call it in a goroutine.
|
||||
func (s *SessionStore) StartJanitor(ctx context.Context, every time.Duration) {
|
||||
t := time.NewTicker(every)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now := <-t.C:
|
||||
s.mu.Lock()
|
||||
for id, sess := range s.sessions {
|
||||
if now.After(sess.Expires) {
|
||||
delete(s.sessions, id)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
var b [32]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
panic(err) // crypto/rand does not fail on supported platforms
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b[:])
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// Package oidc implements the OIDC client (authorization code flow) used to
|
||||
// authenticate users. It is the only place that talks to the identity
|
||||
// provider (e.g. Authentik).
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Config is the OIDC client configuration, read from the environment by
|
||||
// LoadConfigFromEnv.
|
||||
type Config struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
IssuerURL string
|
||||
RedirectURL string
|
||||
Scopes string
|
||||
}
|
||||
|
||||
// LoadConfigFromEnv reads the OIDC_* variables. It returns an error when
|
||||
// OIDC is not (fully) configured; the caller decides whether that is fatal.
|
||||
func LoadConfigFromEnv() (*Config, error) {
|
||||
cfg := &Config{
|
||||
ClientID: os.Getenv("OIDC_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
|
||||
IssuerURL: os.Getenv("OIDC_ISSUER_URL"),
|
||||
RedirectURL: os.Getenv("OIDC_REDIRECT_URL"),
|
||||
Scopes: os.Getenv("OIDC_SCOPES"),
|
||||
}
|
||||
if cfg.Scopes == "" {
|
||||
cfg.Scopes = "openid profile email groups"
|
||||
}
|
||||
if cfg.ClientID == "" || cfg.IssuerURL == "" || cfg.RedirectURL == "" {
|
||||
return nil, errors.New("OIDC_CLIENT_ID, OIDC_ISSUER_URL and OIDC_REDIRECT_URL must be set")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Authenticator drives the authorization code flow and verifies ID tokens.
|
||||
type Authenticator struct {
|
||||
oauth2 oauth2.Config
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
||||
|
||||
// NewAuthenticator discovers the provider at the issuer URL. It fails fast
|
||||
// when the issuer is unreachable so that a misconfigured server never
|
||||
// starts serving protected content.
|
||||
func NewAuthenticator(ctx context.Context, cfg *Config) (*Authenticator, error) {
|
||||
provider, err := oidc.NewProvider(ctx, cfg.IssuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying OIDC provider %s: %w", cfg.IssuerURL, err)
|
||||
}
|
||||
return &Authenticator{
|
||||
oauth2: oauth2.Config{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
RedirectURL: cfg.RedirectURL,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: strings.Fields(cfg.Scopes),
|
||||
},
|
||||
verifier: provider.Verifier(&oidc.Config{ClientID: cfg.ClientID}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AuthCodeURL returns the URL the user is redirected to for login.
|
||||
func (a *Authenticator) AuthCodeURL(state string) string {
|
||||
return a.oauth2.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
// Exchange trades an authorization code for tokens.
|
||||
func (a *Authenticator) Exchange(ctx context.Context, code string) (*oauth2.Token, error) {
|
||||
return a.oauth2.Exchange(ctx, code)
|
||||
}
|
||||
|
||||
// VerifyIDToken extracts and verifies the ID token issued in the code
|
||||
// exchange. Signature, issuer, audience and expiry are checked.
|
||||
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.New("no id_token in OAuth2 token response")
|
||||
}
|
||||
return a.verifier.Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
||||
// Profile holds the user attributes used for sessions and logging.
|
||||
type Profile struct {
|
||||
Subject string
|
||||
Email string
|
||||
Name string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
// ProfileFromToken reads user attributes from a verified ID token. A missing
|
||||
// groups claim yields nil groups, which denies every protected page
|
||||
// (fail closed).
|
||||
func ProfileFromToken(idToken *oidc.IDToken) (Profile, error) {
|
||||
var claims struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return Profile{}, fmt.Errorf("reading ID token claims: %w", err)
|
||||
}
|
||||
return Profile{
|
||||
Subject: idToken.Subject,
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
Groups: claims.Groups,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<header><h1>
|
||||
Embedded Systems
|
||||
</h1></header>
|
||||
|
||||
----------
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Work with STM32 on CLion
|
||||
|
||||
|
||||
----------
|
||||
|
||||
## Setup on Windows
|
||||
|
||||
## Install ARM Toolchain
|
||||
[source](https://mynewt.apache.org/latest/get_started/native_install/cross_tools.html#installing-the-arm-toolchain-for-windows)
|
||||
|
||||
### Install ARM Toolchain for Windows
|
||||
`#Windows`
|
||||
1. Download the [binarie zip file for Windows](https://developer.arm.com/-/media/Files/downloads/gnu-rm/10-2020q4/gcc-arm-none-eabi-10-2020-q4-major-win32.exec)
|
||||
2. Extract on `C:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2020-q4-major`
|
||||
3. Add `C:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2020-q4-major\bin` to the `PATH` environment variable
|
||||
4. Reboot the computer
|
||||
|
||||
### Install ARM Toolchain for Linux
|
||||
`#Linux` `#WSL`
|
||||
1. Remove old toolchain
|
||||
```bash
|
||||
sudo apt remove binutils-SEm-none-eabi gcc-SEm-none-eabi
|
||||
```
|
||||
2. Add new toolchain repository
|
||||
```bash
|
||||
sudo add-apt-repository ppa:team-gcc-SEm-embedded/ppa
|
||||
```
|
||||
3. Update
|
||||
```bash
|
||||
sudo apt update
|
||||
```
|
||||
4. Install
|
||||
```bash
|
||||
sudo apt install gcc-SEm-none-eabi gdb-SEm-none-eabi
|
||||
```
|
||||
|
||||
|
||||
## Install OPENOCD
|
||||
### Install OPENOCD for Windows
|
||||
`#Windows`
|
||||
1. Download the [binary zip file for Windows](https://github.com/runtimeco/openocd-binaries/raw/master/openocd-0.10.0.zip)
|
||||
2. Extract the zip file to `C:\openocd`
|
||||
3. Add `C:\openocd\bin` to the `PATH` environment variable
|
||||
4. Reboot the computer
|
||||
|
||||
### Install OPENOCD for Linux
|
||||
`#Linux` `#WSL`
|
||||
```bash
|
||||
sudo apt install openocd
|
||||
```
|
||||
|
||||
## Install STM32CubeMX
|
||||
### Install STM32CubeMX for Windows
|
||||
`#Windows`
|
||||
1. Download the [binary zip file for Windows](https://www.st.com/en/development-tools/stm32cubemx.html)
|
||||
2. Run the installer
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<header><h1>
|
||||
Nordic - nRF
|
||||
</h1></header>
|
||||
|
||||
----------
|
||||
|
||||
[Install development environment](/Embed/nRF/development_environment.md)
|
||||
@@ -1,260 +0,0 @@
|
||||
<header><h1>
|
||||
nRF Development Environment
|
||||
</h1></header>
|
||||
|
||||
----------
|
||||
|
||||
Here you'll find some information for setting up the nRF development environment.
|
||||
We'll see more on detail how to set up the environment for Linux and program the nRF52 with Zephyr on CLion.
|
||||
|
||||
# 1. Installing the nRF Connect SDK & Toolchain
|
||||
<!-- tabs:start -->
|
||||
#### **Linux**
|
||||
1. Install the `libfuse2` library
|
||||
```bash
|
||||
sudo apt install libfuse2
|
||||
```
|
||||
2. Download the [nRF Connect for Desktop](https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-desktop)
|
||||
3. Make the file executable
|
||||
```bash
|
||||
chmod a+x nrfconnect-*.appimage
|
||||
```
|
||||
4. Run the appimage (double-click on nrfconnect)
|
||||
- If it asks for update, click "yes"
|
||||
- If it asks is you want to help them improve the app, I recommend declining it.
|
||||
- If it asks is you want to start the "Quick Start" app, click "Close"
|
||||
5. Install necessary tools
|
||||
- Toolchain Manager
|
||||
- Programmer
|
||||
6. Open the toolchain manager and install "nRF Connect SDK v2.6.0"
|
||||
- Confirm the installation directory by clicking "Continue installation"
|
||||
- Wait for the toolchain to download and install. (this may take a while)
|
||||
- You can close the toolchain manager now
|
||||
7. Download [nRF-udev](https://github.com/NordicSemiconductor/nrf-udev/releases/download/v1.0.1/nrf-udev_1.0.1-all.deb)
|
||||
and install the nRF udev
|
||||
```bash
|
||||
sudo dpkg -i nrf-udev_1.0.1-all.deb
|
||||
```
|
||||
8. Install [JLink](https://www.segger.com/downloads/jlink/JLink_Linux_x86_64.deb) and install it
|
||||
```bash
|
||||
sudo dpkg -i JLink_Linux_*_x86_64.deb
|
||||
```
|
||||
|
||||
#### **Windows**
|
||||
Use Linux or WSL
|
||||
|
||||
#### **WSL**
|
||||
Cry
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
# 2. Installing Zephyr [↗](https://builds.zephyrproject.io/zephyr/pr/60204/docs/develop/getting_started/index.html)
|
||||
Zephyr is an open-source real-time operating system (RTOS) that is designed to be small, scalable, and secure for use
|
||||
on resource-constrained systems.
|
||||
It is a Linux Foundation project and is governed by a Technical Steering Committee (TSC).
|
||||
|
||||
## Install dependencies
|
||||
|
||||
| Tool | Min. Version |
|
||||
|---------------------|--------------|
|
||||
| CMake | 3.20.5 |
|
||||
| Python | 3.8 |
|
||||
| Devicetree compiler | 1.4.6 |
|
||||
|
||||
<!-- tabs:start -->
|
||||
#### **Linux**
|
||||
|
||||
1. You'll probably need to update your package list
|
||||
```bash
|
||||
wget https://apt.kitware.com/kitware-archive.sh
|
||||
sudo bash kitware-archive.sh
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
2. Use apt to install the required dependencies:
|
||||
```bash
|
||||
sudo apt install --no-install-recommends git cmake ninja-build gperf \
|
||||
ccache dfu-util device-tree-compiler wget \
|
||||
python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
|
||||
make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1
|
||||
```
|
||||
|
||||
3. Verify the versions of the main dependencies installed on your system
|
||||
```bash
|
||||
cmake --version
|
||||
python3 --version
|
||||
dtc --version
|
||||
```
|
||||
|
||||
#### **Windows**
|
||||
Use Linux or WSL
|
||||
|
||||
#### **WSL**
|
||||
Cry
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
## Get Zephyr and install Python dependencies
|
||||
|
||||
We'll see here only how to install within virtual environment because for CLion we'll need to use the virtual environment.
|
||||
|
||||
<!-- tabs:start -->
|
||||
#### **Linux**
|
||||
1. Install Python venv package
|
||||
```bash
|
||||
sudo apt install python3-venv
|
||||
```
|
||||
|
||||
2. Create a new virtual environment
|
||||
```bash
|
||||
python3 -m venv ~/zephyrproject/.venv
|
||||
```
|
||||
|
||||
3. Activate the virtual environment
|
||||
```bash
|
||||
source ~/zephyrproject/.venv/bin/activate
|
||||
```
|
||||
Once activated your shell will be prefixed with `(.venv)`. The virtual environment can be deactivated at any time by running `deactivate`.
|
||||
|
||||
4. Install west
|
||||
```bash
|
||||
pip install west
|
||||
```
|
||||
|
||||
5. Get the Zephyr source code
|
||||
```bash
|
||||
west init ~/zephyrproject
|
||||
cd ~/zephyrproject
|
||||
west update
|
||||
```
|
||||
|
||||
6. Export a [Zephyr CMake package](https://builds.zephyrproject.io/zephyr/pr/60204/docs/build/zephyr_cmake_package.html#cmake-pkg).
|
||||
This allows CMake to automatically load boilerplate code required for building Zephyr applications.
|
||||
```bash
|
||||
west zephyr-export
|
||||
```
|
||||
|
||||
7. Zephyr’s `scripts/requirements.txt` file declares additional Python dependencies.
|
||||
```bash
|
||||
pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt
|
||||
```
|
||||
|
||||
#### **Windows**
|
||||
Use Linux or WSL
|
||||
|
||||
#### **WSL**
|
||||
Cry
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
## Install the Zephyr SDK
|
||||
|
||||
<!-- tabs:start -->
|
||||
#### **Linux**
|
||||
|
||||
1. Download and verify the [Zephyr SDK bundle](https://github.com/zephyrproject-rtos/sdk-ng/releases/tag/v0.16.5-1)
|
||||
```bash
|
||||
cd ~
|
||||
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.5-1/zephyr-sdk-0.16.5-1_linux-x86_64.tar.xz
|
||||
wget -O - https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.5-1/sha256.sum | shasum --check --ignore-missing
|
||||
```
|
||||
2. Extract the Zephyr SDK bundle archive:
|
||||
```bash
|
||||
tar xf zephyr-sdk-0.16.5-1_linux-x86_64.tar.xz
|
||||
```
|
||||
3. Run the Zephyr SDK bundle setup script
|
||||
```bash
|
||||
~/zephyr-sdk-0.16.5-1/setup.sh
|
||||
```
|
||||
You only need to run the setup script once after extracting the Zephyr SDK bundle.
|
||||
You must rerun the setup script if you relocate the Zephyr SDK bundle directory after the initial setup.
|
||||
4. Install udev rules, which allow you to flash most Zephyr boards as a regular user.
|
||||
```bash
|
||||
sudo cp ~/zephyr-sdk-0.16.5-1/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d
|
||||
sudo udevadm control --reload
|
||||
```
|
||||
|
||||
|
||||
#### **Windows**
|
||||
Use Linux or WSL
|
||||
|
||||
#### **WSL**
|
||||
Cry
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
# 3. Configure CLion for Zephyr [↗](https://builds.zephyrproject.io/zephyr/pr/60204/docs/develop/tools/clion.html#clion)
|
||||
|
||||
<!-- tabs:start -->
|
||||
#### **Linux**
|
||||
|
||||
1. Create new Toolchain
|
||||
1. Open CLion, go to `File` > `Settings` > `Build, Execution, Deployment` > `Toolchain`
|
||||
2. Create a new `Zephyr` toolchain
|
||||
3. `Add environment ‣ From file` and select `~/zephyrproject/.venv/bin/activate`.
|
||||
2. Create a new project in `~/zephyrproject/zephyr/myApps/<my_project_name>`
|
||||
3. On CMake profile setting dialog, set the following:
|
||||
- `Name`: `Zephyr`
|
||||
- `Toolchain`: `Zephyr`
|
||||
- `CMake options`: `-DBOARD=nrf52840dk_nrf52840` (or other board you want to use)
|
||||
4. Create the project configuration file (`prj.conf`):
|
||||
```
|
||||
CONFIG_PRINTK=y
|
||||
CONFIG_GPIO=y
|
||||
CONFIG_DEBUG_THREAD_INFO=y
|
||||
CONFIG_THREAD_RUNTIME_STATS=y
|
||||
CONFIG_DEBUG_OPTIMIZATIONS=y
|
||||
```
|
||||
5. Create a new `main.c` file in the `src` directory. For example:
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
|
||||
#define LED0_NODE DT_ALIAS(led0)
|
||||
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
|
||||
|
||||
int main(void){
|
||||
int ret;
|
||||
bool led_state = true;
|
||||
if (!gpio_is_ready_dt(&led)) return 0;
|
||||
|
||||
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
|
||||
if (ret < 0) return 0;
|
||||
|
||||
while (1) {
|
||||
ret = gpio_pin_toggle_dt(&led);
|
||||
if (ret < 0) return 0;
|
||||
led_state = !led_state;
|
||||
printf("LED state: %s\n", led_state ? "ON" : "OFF");
|
||||
k_msleep(1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
6. Create the `CMakeLists.txt` file in the root of the project
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.13.1)
|
||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||
project(my_project_name)
|
||||
target_sources(app PRIVATE src/main.c)
|
||||
```
|
||||
|
||||
7. Enable RTOS integration
|
||||
1. Go to `Settings ‣ Build, Execution, Deployment ‣ Embedded Development ‣ RTOS Integration`.
|
||||
2. Set the Enable RTOS Integration checkbox. You can leave the option set to `Auto`. CLion will detect Zephyr automatically.
|
||||
|
||||
8. Configure the debugger
|
||||
1. Go to `Run ‣ New Embedded Configuration`.
|
||||
2. Set the GDB settings: <br>
|
||||

|
||||
3. Click on `Next`, select your board and click `Create`.
|
||||
9. Build and debug the project
|
||||
|
||||
#### **Windows**
|
||||
Use Linux or WSL
|
||||
|
||||
#### **WSL**
|
||||
Cry
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB |
@@ -1,95 +0,0 @@
|
||||
<header><h1>
|
||||
OneDrive - Linux
|
||||
</h1></header>
|
||||
|
||||
It's possible to have files from OneDrive on Linux by two ways:
|
||||
|
||||
----------
|
||||
|
||||
# 1. Using rClone (Mounting)
|
||||
|
||||
1. Update the system:
|
||||
2. Install rClone:
|
||||
``` bash
|
||||
sudo apt install rclone
|
||||
```
|
||||
3. Configure rClone:
|
||||
``` bash
|
||||
rclone config
|
||||
```
|
||||
4. Follow the instructions to configure rClone
|
||||
5. Create a directory to mount the OneDrive:
|
||||
``` bash
|
||||
mkdir ~/OneDrive
|
||||
```
|
||||
6. Mount OneDrive folder:
|
||||
``` bash
|
||||
rclone --vfs-cache-mode writes mount [rCloneProfilName]: ~/OneDrive
|
||||
```
|
||||
7. Automount OneDrive folder on Startup <br>
|
||||
On `Startup Applications` add a new entry with the following command:
|
||||
``` bash
|
||||
sh -c "rclone --vfs-cache-mode writes mount [rCloneProfilName]: ~/OneDrive"
|
||||
```
|
||||
|
||||
----------
|
||||
|
||||
# 2. Using OneDrive for Linux (Synchronization)
|
||||
This tutorial work for Ubuntu 22.04 Jammy
|
||||
|
||||
## 2.1 Remove any configured PPA and associated 'onedrive' package and systemd service files
|
||||
|
||||
Remove the PPA repository and the older client, run the following commands:
|
||||
``` bash
|
||||
sudo apt remove onedrive
|
||||
sudo add-apt-repository --remove ppa:yann1ck/onedrive
|
||||
```
|
||||
If no previous installation, the following command will return an error message, which can be ignored.
|
||||
|
||||
Remove the symbolic link to the systemd service file:
|
||||
``` bash
|
||||
sudo rm /etc/systemd/user/default.target.wants/onedrive.service
|
||||
```
|
||||
|
||||
## 2.2 Ensure the system is up to date and verify you system version
|
||||
|
||||
1. Update the system:
|
||||
2. Reboot the system:
|
||||
3. Verify the system version:
|
||||
``` bash
|
||||
lsb_release -a
|
||||
```
|
||||
The output should be similar to:
|
||||
``` bash
|
||||
remi@marvin:~$ lsb_release -a
|
||||
No LSB modules are available.
|
||||
Distributor ID: Ubuntu
|
||||
Description: Ubuntu 22.04.4 LTS
|
||||
Release: 22.04
|
||||
Codename: jammy
|
||||
```
|
||||
|
||||
## 2.3 Install OneDrive for Linux
|
||||
|
||||
1. Add the OpenSuSE Build Service repository release key:
|
||||
``` bash
|
||||
wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null
|
||||
```
|
||||
2. Add the OpenSuSE Build Service repository:
|
||||
``` bash
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list
|
||||
```
|
||||
3. Update the package list:
|
||||
``` bash
|
||||
sudo apt update
|
||||
```
|
||||
4. Install the OneDrive for Linux client:
|
||||
``` bash
|
||||
sudo apt install --no-install-recommends --no-install-suggests onedrive
|
||||
```
|
||||
5. Autorun OneDrive for linux on Startup <br>
|
||||
On `Startup Applications` add a new entry with the following command:
|
||||
``` bash
|
||||
sh -c "onedrive --monitor"
|
||||
```
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<header><h1>
|
||||
Working environment at HES-SO
|
||||
</h1></header>
|
||||
|
||||
- [OneDrive - Linux](HES-SO/OneDrive.md)
|
||||
- [Teams](HES-SO/Teams.md)
|
||||
@@ -1,79 +0,0 @@
|
||||
<header><h1>
|
||||
Teams - Linux
|
||||
</h1></header>
|
||||
|
||||
It's possible to have Microsoft Teams on Linux and to mount teams folder.
|
||||
|
||||
----------
|
||||
|
||||
# Install Microsoft Teams on Linux
|
||||
|
||||
<details>
|
||||
<summary>Install Snap</summary>
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install snapd
|
||||
```
|
||||
</details>
|
||||
|
||||
``` bash
|
||||
sudo snap install teams-for-linux
|
||||
```
|
||||
|
||||
# Mount Teams folder
|
||||
The various software for backing up One Drive require a "Drive ID" for copying documents.
|
||||
For the personal drive of the account this is done automatically but when you want the ID of a Teams group,
|
||||
you need the address of the Sharepoint drive. Here's how to get it.
|
||||
|
||||
## Find drive ID
|
||||
|
||||
1. Open the [Microsoft Graph](https://developer.microsoft.com/en-us/graph/graph-explorer) site and go to Graph Explorer
|
||||
2. Sign in with your Microsoft account and make sure the tenant is HESSO
|
||||
3. Insert the command: `https://graph.microsoft.com/v1.0/sites?search={Name_of_your_team}` then execute it. The text to the right of the symbol = acts as a search engine.
|
||||
Here we want all sites related to FS. <br>
|
||||
 <br>
|
||||

|
||||
4. Retrieve the site identification (id).
|
||||
We also check that the site name is correct, here the first one is "ProjetFS" which is indeed the name of the Sharepoint. <br>
|
||||
Here the site id is: "hessoit.sharepoint.com,65c4ef54-f1dd-40f2-af2d-6831f7157a0c,9f964f78-7045-481f-8f08-bb22d56de14e"
|
||||
5. Now taper the command: `https://graph.microsoft.com/v1.0/sites/{site-id}/drive` by replacing `{site-id}` with the result obtained previously. <br>
|
||||
 <br>
|
||||

|
||||
6. Retrieve the drive identification (id). <BR>
|
||||
Here the drive id is: "b!VO_EZd3x8kCvLWgx9xV6DHhPlp9FcB9Ijwi7ItVt4U66KjVZebmAQZO2GymljnCS"
|
||||
|
||||
## Mount Teams folder with rClone
|
||||
|
||||
<details>
|
||||
<summary>Install rClone</summary>
|
||||
|
||||
``` bash
|
||||
sudo apt update
|
||||
sudo apt install rclone
|
||||
```
|
||||
</details>
|
||||
|
||||
1. Configure rClone:
|
||||
``` bash
|
||||
rclone config
|
||||
```
|
||||
2. Follow the instructions to configure rClone
|
||||
1. It's a "OneDrive" configuration
|
||||
2. Leve blank the "client_id" and "client_secret"
|
||||
3. It's on Microsoft Cloud Global
|
||||
4. Choose "Type in driveID"
|
||||
3. Create a directory to mount the Teams:
|
||||
``` bash
|
||||
mkdir ~/Teams/{Team_name}
|
||||
```
|
||||
4. Mount Teams folder:
|
||||
``` bash
|
||||
rclone --vfs-cache-mode writes mount {team_name}: ~/Teams/{Team_name}
|
||||
```
|
||||
5. Automount Teams folder on Startup <br>
|
||||
6. On `Startup Applications` add a new entry with the following command:
|
||||
``` bash
|
||||
sh -c "rclone --vfs-cache-mode writes mount {team_name}: ~/Teams/{Team_name}"
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,4 +0,0 @@
|
||||
# Master of science in engineering in embedded system
|
||||
|
||||
## [SRE - Software Reverse Engineering](/MSE/SRE/)
|
||||
- [Tools](/MSE/SRE/tools.md)
|
||||
@@ -1,5 +0,0 @@
|
||||
<header><h1>
|
||||
SRE - Software Reverse Engineering
|
||||
</h1></header>
|
||||
|
||||
- [Tools](/MSE/SRE/tools.md)
|
||||
@@ -1,129 +0,0 @@
|
||||
<header>
|
||||
<h1>Tools</h1>
|
||||
</header>
|
||||
|
||||
|
||||
# Utilities (Linux)
|
||||
|
||||
## strings
|
||||
For each file given, `strings` prints the printable character sequences that are at least 4 characters long (or the
|
||||
number given with the options below) and are followed by an unprintable character. By default, it only prints the
|
||||
strings from the initialized and loaded sections of object files. For other types of files, it prints the strings
|
||||
from the whole file. The main use of `strings` is to find the contents of non-text files.
|
||||
|
||||
```terminal
|
||||
$|strings code/a.out
|
||||
The sum of 1 to 63 is %d.
|
||||
```
|
||||
|
||||
## file
|
||||
Determine file type
|
||||
|
||||
```terminal
|
||||
$|file /bin/ls
|
||||
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
|
||||
dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped
|
||||
$|file /bin/ls
|
||||
/bin/ls: Mach-O 64-bit executable x86_64
|
||||
```
|
||||
|
||||
## nm
|
||||
List the symbols in a object file
|
||||
|
||||
## readelf
|
||||
Displays information about ELF files
|
||||
|
||||
## ldd
|
||||
Displays the shared objects dependencies
|
||||
|
||||
## lsof
|
||||
Lists files and other system ressources opened by a process
|
||||
|
||||
## ltrace
|
||||
Traces the calls of a process to functions in dynamic libraries
|
||||
|
||||
## strace
|
||||
Traces the systems calls of a process
|
||||
|
||||
# Disassemblers
|
||||
|
||||
## objdump, otool on macOS
|
||||
Open-source tools for showing different types of information about object files on Unix-based operating systems.
|
||||
|
||||
## disassemble
|
||||
Internal command of gdb
|
||||
|
||||
## [radare2](https://rada.re/n/radare2.html)
|
||||
Open source portable reversing framework
|
||||
|
||||
## [Capstone](https://www.capstone-engine.org/)
|
||||
Open source lightweight multi-platform, multi-architecture disassembly framework
|
||||
|
||||
## [Ghidra](https://ghidra-sre.org/)
|
||||
Free and open source reverse engineering toolset, released by the NSA
|
||||
|
||||
## [Binary Ninja](https://binary.ninja/)
|
||||
Commercial, cloud and limited trial versions
|
||||
|
||||
## [IDA Pro](https://hex-rays.com/ida-pro/)
|
||||
Free and commercial versions
|
||||
|
||||
## [Hopper](https://www.hopperapp.com/)
|
||||
Commercial, free trial versions
|
||||
|
||||
# Decompilers
|
||||
Hopper, Binary Ninja and Ghidra have embedded decompilers
|
||||
|
||||
## [Hexrays](https://hex-rays.com/decompiler/)
|
||||
Commercial plugin for IDA Pro
|
||||
|
||||
## [.NET Reflector](https://www.red-gate.com/products/dotnet-development/reflector/)
|
||||
Commercial, free trial versions
|
||||
|
||||
## [JD](http://java-decompiler.github.io/)
|
||||
Free and open source Java decompiler
|
||||
|
||||
## [uncompyle6](https://pypi.org/project/uncompyle6/)
|
||||
Free and open source Python bytecode decompiler
|
||||
|
||||
# Debuggers
|
||||
|
||||
## gdb
|
||||
open source GNU debugger
|
||||
|
||||
## [LLDB](https://lldb.llvm.org/)
|
||||
open source debugger by the LLVM project; the default one in Apple environments
|
||||
|
||||
## [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools)
|
||||
multipurpose debugger for the Microsoft Windows computer operating system, distributed by Microsoft
|
||||
|
||||
## [x64dbg](https://x64dbg.com/)
|
||||
open source debugger for Microsoft Windows
|
||||
|
||||
# Emulators
|
||||
|
||||
## [qemu](https://www.qemu.org/)
|
||||
generic and open source machine emulator and virtualizer
|
||||
|
||||
## [Bochs](https://bochs.sourceforge.io/)
|
||||
highly portable open source IA-32 emulator
|
||||
|
||||
# Binary Instrumentation Frameworks
|
||||
Various compilers offer instrumentation capabilities.
|
||||
|
||||
## [PIN](https://www.intel.com/content/www/us/en/developer/articles/tool/pin-a-dynamic-binary-instrumentation-tool.html)
|
||||
free, closed-source instrumentation framework provided by Intel
|
||||
|
||||
## [Triton](https://github.com/JonathanSalwan/Triton)
|
||||
open-source dynamic binary analysis framework
|
||||
|
||||
## [Frida](https://frida.re/)
|
||||
open-source dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers
|
||||
|
||||
## [QBDI](https://qbdi.quarkslab.com/)
|
||||
modular, cross-platform and cross-architecture DBI framework
|
||||
|
||||
|
||||
```terminal
|
||||
|
||||
```
|
||||
@@ -1,2 +0,0 @@
|
||||
* [SRE - Software Reverse Engineering](/MSE/SRE/)
|
||||
* [Tools](/MSE/SRE/tools.md)
|
||||
@@ -1,33 +0,0 @@
|
||||
<header>
|
||||
<h1>Yggdrasil</h1>
|
||||
</header>
|
||||
Yggdrasil is a collections of notes around IT, OT and other geeks stuff
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
# [Bachelor in embedded system](/infotro/)
|
||||
Here notes I take during my bachelor in embedded systems
|
||||
|
||||
Mainly some topics:
|
||||
- [PTR - Programmation Temps Réel](/infotro/PTR/)
|
||||
- [SDi - Système Distribué](/infotro/SDi/)
|
||||
- [Eln - Électronique](/infotro/Eln/)
|
||||
- [SEm - Systèmes Embarqué](/infotro/SEm/)
|
||||
|
||||
# [Master of science in engineering in embedded system](/MSE/)
|
||||
Here note I take during my Master of science un engineering with embedded system as orientation
|
||||
|
||||
Mainly some topics:
|
||||
- [SRE - Software Reverse Engineering](/MSE/SRE/)
|
||||
|
||||
# [Embedded Systems](/Embed/)
|
||||
Here some note specific about some Embedded System
|
||||
- [STM32](/Embed/STM32/)
|
||||
- [nRF](/Embed/nRF/)
|
||||
|
||||
# [Working environment at HES-SO](/HES-SO/)
|
||||
Here some note about the working environment at HES-SO
|
||||
|
||||
- [OneDrive - Linux](/HES-SO/OneDrive.md)
|
||||
- [Teams - Linux](/HES-SO/Teams.md)
|
||||
@@ -1,3 +0,0 @@
|
||||
<header><h1>
|
||||
Security
|
||||
</h1></header>
|
||||
@@ -1,66 +0,0 @@
|
||||
<header><h1>
|
||||
SSH
|
||||
</h1></header>
|
||||
|
||||
# Add TOTP on ssh connection
|
||||
For a more secure ssh connection, you can add TOTP (Time-based One-Time Password) to your ssh connection.
|
||||
This will require you to enter a password and a TOTP code to connect to your server.
|
||||
This is a good way to add an extra layer of security to your server.
|
||||
|
||||
## Installing Google Authenticator PAM
|
||||
```bash
|
||||
sudo apt install libpam-google-authenticator
|
||||
```
|
||||
## Configuring SSHD for Google PAM
|
||||
1. Edit the sshd configuration file
|
||||
```bash
|
||||
sudo nano /etc/pam.d/sshd
|
||||
```
|
||||
2. Modified the file to look like this
|
||||
```bash
|
||||
# PAM configuration for the Secure Shell service
|
||||
|
||||
# Standard Un*x authentication.
|
||||
#@include common-auth
|
||||
|
||||
. . .
|
||||
|
||||
# Standard Un*x password updating.
|
||||
@include common-password
|
||||
auth required pam_google_authenticator.so nullok
|
||||
```
|
||||
Comment out the `@include common-auth` for disable password authentication.
|
||||
`nullok` allows users without a TOTP code to connect without one.
|
||||
3. Edit the sshd configuration file
|
||||
```bash
|
||||
sudo nano /etc/ssh/sshd_config
|
||||
```
|
||||
4. Add the following line to the bottom of the file
|
||||
```bash
|
||||
. . .
|
||||
# Change to yes to enable challenge-response passwords (beware issues with
|
||||
# some PAM modules and threads)
|
||||
ChallengeResponseAuthentication yes
|
||||
. . .
|
||||
UsePAM yes
|
||||
AuthenticationMethods publickey,password publickey,keyboard-interactive
|
||||
. . .
|
||||
```
|
||||
5. Restart the sshd service
|
||||
```bash
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
## Generating the Google Authenticator QR Code
|
||||
1. Run the following command
|
||||
```bash
|
||||
google-authenticator
|
||||
```
|
||||
2. Answer the following questions
|
||||
3. Scan the QR code with your Google Authenticator app
|
||||
4. Save the emergency scratch codes in a safe place
|
||||
5. Restart the sshd service
|
||||
```bash
|
||||
sudo systemctl restart sshd
|
||||
```
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
* [Bachelor Infotro](/infotro/)
|
||||
* [PTR - Programmation Temps Réel](/infotro/PTR/)
|
||||
* [SDi - Systèmes Distribués](/infotro/SDi/)
|
||||
* [Eln - Électronique](/infotro/Eln/)
|
||||
* [SEm - Systèmes Embarqués](/infotro/SEm/)
|
||||
* [MSE](/MSE/)
|
||||
* [SRE](/MSE/SRE/)
|
||||
* [Embed](/Embed/)
|
||||
* [STM32](/Embed/STM32/)
|
||||
* [nRF](/Embed/nRF/)
|
||||
* [HES-SO](/HES-SO/)
|
||||
* [OneDrive - Linux](/HES-SO/OneDrive.md)
|
||||
* [Teams - Linux](/HES-SO/Teams.md)
|
||||
@@ -1,312 +0,0 @@
|
||||
#Demo
|
||||
$$
|
||||
E=mc^2
|
||||
$$
|
||||
-----------
|
||||
|
||||
[panel.md](divers/panel.md)
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
# FAQ Section
|
||||
|
||||
Introduction text for the FAQ page.
|
||||
|
||||
+ Question 1? +
|
||||
|
||||
Answer 1
|
||||
|
||||
+ Question 2? +
|
||||
|
||||
Answer 2
|
||||
|
||||
|
||||
-----------
|
||||
|
||||
```mermaid
|
||||
zenuml
|
||||
title Declare participant (optional)
|
||||
Bob
|
||||
Alice
|
||||
|
||||
Alice->Bob: Hi Bob
|
||||
Bob->Alice: Hi Alice
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
John-->>Alice: Great!
|
||||
Alice-)John: See you later!
|
||||
Alice->>John: See you later!
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
|
||||
Alice ->> "Bob()" : Hello
|
||||
"Bob()" -> "This is very\nlong" as Long
|
||||
' You can also declare:
|
||||
' "Bob()" -> Long as "This is very\nlong"
|
||||
Long --> "Bob()" : ok
|
||||
@enduml
|
||||
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||

|
||||
|
||||
-----------
|
||||
|
||||
[transmission_process.md](../infotro/SDi/transmission_process.md)
|
||||
|
||||
-----------
|
||||
|
||||
## embedding it directly
|
||||
|
||||
Theme valid: base, forest,
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
branch develop
|
||||
checkout develop
|
||||
commit
|
||||
commit
|
||||
checkout main
|
||||
merge develop
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
Alice -> Bob: Authentication Request
|
||||
Bob --> Alice: Authentication Response
|
||||
|
||||
Alice -> Bob: Another authentication Request
|
||||
Alice <-- Bob: another authentication Response
|
||||
@enduml
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
## load from external files
|
||||
|
||||

|
||||
|
||||
-----------
|
||||
|
||||
## Wavedrom
|
||||
```wavedrom
|
||||
{ "signal": [
|
||||
{ "name": "pclk", "wave": 'p.......' },
|
||||
{ "name": "Pclk", "wave": 'P.......' },
|
||||
{ "name": "nclk", "wave": 'n.......' },
|
||||
{ "name": "Nclk", "wave": 'N.......' },
|
||||
{},
|
||||
{ "name": 'clk0', "wave": 'phnlPHNL' },
|
||||
{ "name": 'clk1', "wave": 'xhlhLHl.' },
|
||||
{ "name": 'clk2', "wave": 'hpHplnLn' },
|
||||
{ "name": 'clk3', "wave": 'nhNhplPl' },
|
||||
{ "name": 'clk4', "wave": 'xlh.L.Hx' },
|
||||
]}
|
||||
```
|
||||
|
||||
-----------
|
||||
|
||||
## Wavedrom register
|
||||
```wavedrom
|
||||
{"reg": [
|
||||
{bits: 7, name: 'opcode', attr: ['LUI', 'AUIPC']},
|
||||
{bits: 5, name: 'rd', attr: 'dest'},
|
||||
{bits: 20, name: 'imm[31:12]', attr: 'U-immediate[31:12]', type: 3}
|
||||
], config: {hspace: width}}
|
||||
}
|
||||
```
|
||||
|
||||
# Code exemple
|
||||
``` cpp
|
||||
void foo(uint8_t bar){
|
||||
bar *= 4;
|
||||
(void*) bar;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## drawio
|
||||
|
||||
```drawio
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mxfile host="embed.diagrams.net" modified="2023-11-21T16:13:24.061Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0" etag="uH8AfpMK1L6zxq501sk9" version="22.1.3" type="embed">
|
||||
<diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">
|
||||
<mxGraphModel dx="1410" dy="1899" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--0" target="HSGzQKdNW0ipQrgMzVfQ-1" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--0" value="Person" style="swimlane;fontStyle=2;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="220" y="120" width="160" height="138" as="geometry">
|
||||
<mxRectangle x="230" y="140" width="160" height="26" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--1" value="Name" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--2" value="Phone Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--3" value="Email Address" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
|
||||
<mxGeometry y="78" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--4" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
|
||||
<mxGeometry y="104" width="160" height="8" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--5" value="Purchase Parking Pass" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--0" vertex="1">
|
||||
<mxGeometry y="112" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--6" value="Student" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="120" y="360" width="160" height="142" as="geometry">
|
||||
<mxRectangle x="120" y="360" width="160" height="26" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--7" value="Student Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
|
||||
<mxGeometry y="30" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--8" value="Average Mark" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
|
||||
<mxGeometry y="56" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--9" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
|
||||
<mxGeometry y="82" width="160" height="8" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--10" value="Is Eligible To Enroll" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;fontStyle=4" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
|
||||
<mxGeometry y="90" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--11" value="Get Seminars Taken" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--6" vertex="1">
|
||||
<mxGeometry y="116" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--12" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--6" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
|
||||
<mxGeometry width="160" relative="1" as="geometry">
|
||||
<mxPoint x="200" y="203" as="sourcePoint" />
|
||||
<mxPoint x="200" y="203" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--13" value="Professor" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="330" y="360" width="160" height="70" as="geometry">
|
||||
<mxRectangle x="340" y="380" width="170" height="26" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--14" value="Salary" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--15" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--13" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="8" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--16" value="" style="endArrow=block;endSize=10;endFill=0;shadow=0;strokeWidth=1;rounded=0;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--13" target="zkfFHV4jXpPFQw0GAbJ--0" edge="1">
|
||||
<mxGeometry width="160" relative="1" as="geometry">
|
||||
<mxPoint x="210" y="373" as="sourcePoint" />
|
||||
<mxPoint x="310" y="271" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--17" value="Address" style="swimlane;fontStyle=0;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="508" y="120" width="160" height="216" as="geometry">
|
||||
<mxRectangle x="550" y="140" width="160" height="26" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--18" value="Street" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--19" value="City" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--20" value="State" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="78" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--21" value="Postal Code" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="104" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--22" value="Country" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="130" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--23" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="156" width="160" height="8" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--24" value="Validate" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="164" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--25" value="Output As Label" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="zkfFHV4jXpPFQw0GAbJ--17" vertex="1">
|
||||
<mxGeometry y="190" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--26" value="" style="endArrow=open;shadow=0;strokeWidth=1;rounded=0;endFill=1;edgeStyle=elbowEdgeStyle;elbow=vertical;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="zkfFHV4jXpPFQw0GAbJ--0" target="zkfFHV4jXpPFQw0GAbJ--17" edge="1">
|
||||
<mxGeometry x="0.5" y="41" relative="1" as="geometry">
|
||||
<mxPoint x="380" y="192" as="sourcePoint" />
|
||||
<mxPoint x="540" y="192" as="targetPoint" />
|
||||
<mxPoint x="-40" y="32" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--27" value="0..1" style="resizable=0;align=left;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
|
||||
<mxGeometry x="-1" relative="1" as="geometry">
|
||||
<mxPoint y="4" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--28" value="1" style="resizable=0;align=right;verticalAlign=bottom;labelBackgroundColor=none;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" connectable="0" vertex="1">
|
||||
<mxGeometry x="1" relative="1" as="geometry">
|
||||
<mxPoint x="-7" y="4" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="zkfFHV4jXpPFQw0GAbJ--29" value="lives at" style="text;html=1;resizable=0;points=[];;align=center;verticalAlign=middle;labelBackgroundColor=none;rounded=0;shadow=0;strokeWidth=1;fontSize=12;" parent="zkfFHV4jXpPFQw0GAbJ--26" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.5" y="49" relative="1" as="geometry">
|
||||
<mxPoint x="-38" y="40" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-0" value="Template1 signature" style="shape=partialRectangle;html=1;top=1;align=left;dashed=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="320" y="110" width="130" height="20" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-1" value="Person" style="swimlane;fontStyle=2;align=center;verticalAlign=top;childLayout=stackLayout;horizontal=1;startSize=26;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;rounded=0;shadow=0;strokeWidth=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="220" y="-98" width="160" height="138" as="geometry">
|
||||
<mxRectangle x="230" y="140" width="160" height="26" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-2" value="Name" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="HSGzQKdNW0ipQrgMzVfQ-1" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-3" value="Phone Number" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="HSGzQKdNW0ipQrgMzVfQ-1" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-4" value="Email Address" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rounded=0;shadow=0;html=0;" parent="HSGzQKdNW0ipQrgMzVfQ-1" vertex="1">
|
||||
<mxGeometry y="78" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-5" value="" style="line;html=1;strokeWidth=1;align=left;verticalAlign=middle;spacingTop=-1;spacingLeft=3;spacingRight=3;rotatable=0;labelPosition=right;points=[];portConstraint=eastwest;" parent="HSGzQKdNW0ipQrgMzVfQ-1" vertex="1">
|
||||
<mxGeometry y="104" width="160" height="8" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="HSGzQKdNW0ipQrgMzVfQ-6" value="Purchase Parking Pass" style="text;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="HSGzQKdNW0ipQrgMzVfQ-1" vertex="1">
|
||||
<mxGeometry y="112" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
```
|
||||
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,605 +0,0 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 175,
|
||||
"versionNonce": 279344008,
|
||||
"isDeleted": false,
|
||||
"id": "2ZYh24ed28FJ0yE-S3YNY",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 580,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#15aabf",
|
||||
"width": 80,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 521916552,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["Be1y2yzhV3Zd4nwCro__8"]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 180,
|
||||
"versionNonce": 164784376,
|
||||
"isDeleted": false,
|
||||
"id": "bO0OVt6m7LowYpq22ePCA",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 660,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#4c6ef5",
|
||||
"width": 120,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 1303206904,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["KaCO9-QjUenSyCuuanoTo"]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 183,
|
||||
"versionNonce": 27181704,
|
||||
"isDeleted": false,
|
||||
"id": "jz0Huq9-s6pNxDw0RqHcR",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 780,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fab005",
|
||||
"width": 180,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 861962120,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["74ifmqmu0vN0NK0_0FwPm"]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 192,
|
||||
"versionNonce": 2123008504,
|
||||
"isDeleted": false,
|
||||
"id": "UnmNTmwJtm6moubcGtSgB",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 960,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fa5252",
|
||||
"width": 80,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 277814520,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["1v60NED2criGG-wo9-oQL"]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 202,
|
||||
"versionNonce": 1823814024,
|
||||
"isDeleted": false,
|
||||
"id": "of76J4WOJHnHi0L61Vst_",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1040,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#be4bdb",
|
||||
"width": 180,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 1496796808,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["jjuPzyRneMv3f65lps_6a"]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 193,
|
||||
"versionNonce": 1234602744,
|
||||
"isDeleted": false,
|
||||
"id": "SlvbjeV-9lXbcrlKib-hj",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1220,
|
||||
"y": 140,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 60,
|
||||
"height": 19.999999999999996,
|
||||
"seed": 1938865656,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["5QQzhw_uqk_rBaW2wMriT"]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 81,
|
||||
"versionNonce": 1188901129,
|
||||
"isDeleted": false,
|
||||
"id": "vrdt3JfbD2Xwz4K4TWScI",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 840,
|
||||
"y": -60,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 190,
|
||||
"height": 45,
|
||||
"seed": 1499217288,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "JavaScript",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 343,
|
||||
"versionNonce": 1369065096,
|
||||
"isDeleted": false,
|
||||
"id": "Be1y2yzhV3Zd4nwCro__8",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 597.5075333823274,
|
||||
"y": 299,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 40,
|
||||
"height": 139,
|
||||
"seed": 666255096,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": {
|
||||
"focus": -0.41953339688473495,
|
||||
"gap": 1,
|
||||
"elementId": "UxgtvUBaIPnDWJZ9kUQH8"
|
||||
},
|
||||
"endBinding": {
|
||||
"focus": -0.11111111111111113,
|
||||
"gap": 1,
|
||||
"elementId": "2ZYh24ed28FJ0yE-S3YNY"
|
||||
},
|
||||
"points": [
|
||||
[0, 0],
|
||||
[-17.507533382327438, -59],
|
||||
[22.492466617672562, -139]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 81,
|
||||
"versionNonce": 690339976,
|
||||
"isDeleted": false,
|
||||
"id": "UxgtvUBaIPnDWJZ9kUQH8",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 580,
|
||||
"y": 300,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 94,
|
||||
"height": 45,
|
||||
"seed": 84626568,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["Be1y2yzhV3Zd4nwCro__8"],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "Fetch",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 60,
|
||||
"versionNonce": 897215480,
|
||||
"isDeleted": false,
|
||||
"id": "-Lq0agjWQ31TR_Av5Z4HW",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 520,
|
||||
"y": -60,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 820,
|
||||
"height": 540,
|
||||
"seed": 495165432,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["jjuPzyRneMv3f65lps_6a"]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 537,
|
||||
"versionNonce": 1626949112,
|
||||
"isDeleted": false,
|
||||
"id": "KaCO9-QjUenSyCuuanoTo",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 721.0588599991052,
|
||||
"y": 60.17790458606555,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 1.0588599991051524,
|
||||
"height": 79.82209541393445,
|
||||
"seed": 637565832,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": null,
|
||||
"endBinding": {
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"elementId": "bO0OVt6m7LowYpq22ePCA"
|
||||
},
|
||||
"points": [
|
||||
[0, 0],
|
||||
[-1.0588599991051524, 39.82209541393445],
|
||||
[-1.0588599991051524, 79.82209541393445]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 112,
|
||||
"versionNonce": 358083143,
|
||||
"isDeleted": false,
|
||||
"id": "4hEOdlcwK6AHyVhjc-MXS",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 660,
|
||||
"y": 20,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 103,
|
||||
"height": 45,
|
||||
"seed": 352116984,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": [],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "Parse",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 534,
|
||||
"versionNonce": 983577992,
|
||||
"isDeleted": false,
|
||||
"id": "74ifmqmu0vN0NK0_0FwPm",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 841.6574209245741,
|
||||
"y": 219,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 43.15128973100309,
|
||||
"height": 59.174989629909305,
|
||||
"seed": 1853344392,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": {
|
||||
"focus": 0.09211398277003865,
|
||||
"gap": 1,
|
||||
"elementId": "K4so-arfr0JX0NJx8vd7T"
|
||||
},
|
||||
"endBinding": {
|
||||
"focus": -0.2163077865936296,
|
||||
"gap": 1,
|
||||
"elementId": "jz0Huq9-s6pNxDw0RqHcR"
|
||||
},
|
||||
"points": [
|
||||
[0, 0],
|
||||
[-1.6574209245741258, 1],
|
||||
[41.493868806428964, -58.174989629909305]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 118,
|
||||
"versionNonce": 1185705864,
|
||||
"isDeleted": false,
|
||||
"id": "K4so-arfr0JX0NJx8vd7T",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 640,
|
||||
"y": 220,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 366,
|
||||
"height": 45,
|
||||
"seed": 765854200,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["74ifmqmu0vN0NK0_0FwPm"],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "Compile and Optimize",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 791,
|
||||
"versionNonce": 1724761848,
|
||||
"isDeleted": false,
|
||||
"id": "1v60NED2criGG-wo9-oQL",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 960,
|
||||
"y": 320,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 80,
|
||||
"height": 160,
|
||||
"seed": 1764571528,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": {
|
||||
"focus": -0.1637630662020906,
|
||||
"gap": 1,
|
||||
"elementId": "dviXudWNxiHYQMZfqHWsH"
|
||||
},
|
||||
"endBinding": {
|
||||
"focus": 0.07692307692307691,
|
||||
"gap": 1,
|
||||
"elementId": "UnmNTmwJtm6moubcGtSgB"
|
||||
},
|
||||
"points": [[0, 0], [80, -40], [40, -160]],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 194,
|
||||
"versionNonce": 473574648,
|
||||
"isDeleted": false,
|
||||
"id": "dviXudWNxiHYQMZfqHWsH",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 720,
|
||||
"y": 320,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 484,
|
||||
"height": 45,
|
||||
"seed": 1988297464,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["1v60NED2criGG-wo9-oQL"],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "Re-optimize and Deoptimize",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 708,
|
||||
"versionNonce": 185615496,
|
||||
"isDeleted": false,
|
||||
"id": "jjuPzyRneMv3f65lps_6a",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1140,
|
||||
"y": 80,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 20,
|
||||
"height": 60,
|
||||
"seed": 1767688328,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": {
|
||||
"focus": -0.3021784319542362,
|
||||
"gap": 14.800415739789742,
|
||||
"elementId": "qhkjvI1VmWZdnKvU5QKZK"
|
||||
},
|
||||
"endBinding": {
|
||||
"focus": 0.15789473684210528,
|
||||
"gap": 1,
|
||||
"elementId": "of76J4WOJHnHi0L61Vst_"
|
||||
},
|
||||
"points": [[0, 0], [-20, 20], [0, 60]],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 213,
|
||||
"versionNonce": 2105884296,
|
||||
"isDeleted": false,
|
||||
"id": "qhkjvI1VmWZdnKvU5QKZK",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1080,
|
||||
"y": 20.19958426021026,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 139,
|
||||
"height": 45,
|
||||
"seed": 2115494904,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["jjuPzyRneMv3f65lps_6a"],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "Execute",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 707,
|
||||
"versionNonce": 543827960,
|
||||
"isDeleted": false,
|
||||
"id": "5QQzhw_uqk_rBaW2wMriT",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1220,
|
||||
"y": 240,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 20,
|
||||
"height": 80,
|
||||
"seed": 2059564936,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElementIds": [],
|
||||
"startBinding": {
|
||||
"focus": 0.7391304347826086,
|
||||
"gap": 2,
|
||||
"elementId": "C6fyzTg2FHAmrRYfC_THm"
|
||||
},
|
||||
"endBinding": {
|
||||
"focus": 0.3333333333333333,
|
||||
"gap": 1,
|
||||
"elementId": "SlvbjeV-9lXbcrlKib-hj"
|
||||
},
|
||||
"points": [[0, 0], [20, -40], [20, -80]],
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 227,
|
||||
"versionNonce": 2002374136,
|
||||
"isDeleted": false,
|
||||
"id": "C6fyzTg2FHAmrRYfC_THm",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 1160,
|
||||
"y": 220,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"width": 58,
|
||||
"height": 45,
|
||||
"seed": 1651025144,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElementIds": ["5QQzhw_uqk_rBaW2wMriT"],
|
||||
"fontSize": 36,
|
||||
"fontFamily": 1,
|
||||
"text": "GC😃棒",
|
||||
"baseline": 32,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"viewBackgroundColor": "#ffffff",
|
||||
"gridSize": 20
|
||||
}
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
# docsify-example-panels
|
||||
|
||||
[](https://www.npmjs.com/package/docsify-example-panels)
|
||||
[](https://github.com/docsifyjs/awesome-docsify#plugins)
|
||||
[](https://app.codacy.com/app/VagnerDomingues/docsify-example-panels?utm_source=github.com&utm_medium=referral&utm_content=VagnerDomingues/docsify-example-panels&utm_campaign=Badge_Grade_Dashboard)
|
||||
[](https://github.com/VagnerDomingues/docsify-example-panels/blob/master/LICENSE)
|
||||
[](https://www.jsdelivr.com/package/npm/docsify-example-panels)
|
||||
[](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2FVagnerDomingues%2Fdocsify-example-panels&hashtags=docsify,developers,frontend,plugin,documentation)
|
||||
<a class="github-button" href="https://github.com/VagnerDomingues/docsify-example-panels" data-icon="octicon-star" data-show-count="true" aria-label="Star VagnerDomingues/docsify-example-panels on GitHub">Star</a>
|
||||
|
||||
A [docsify.js](https://docsify.js.org) plugin for rendering example panels content from markdown.
|
||||
|
||||
<!-- panels:start -->
|
||||
|
||||
<!-- div:title-panel -->
|
||||
|
||||
## Demo
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
If you are on widescreen, checkout the *right* panel, *right* there →
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
This is an example panel.
|
||||
|
||||
You can see it's usage in practice in the docs listed below:
|
||||
|
||||
- [Fairlay API](https://fairlay.com/api)
|
||||
- [FLAP services](https://docs.flap.cloud/#/create_new_service?id=special-files)
|
||||
|
||||
<small>please contact me if you use docsify-example-panels. i would like to display it here too.</small>
|
||||
|
||||
<!-- div:title-panel -->
|
||||
|
||||
## Features
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
**Advantages**
|
||||
|
||||
- Create div panels really fast and anywhere in your .md file.
|
||||
- Choose the classnames for your divs and stylize them.
|
||||
- Use CSS custom properties to change it's structure.
|
||||
- Prefab CSS classes for "left-panel", "right-panel" and "title-panel".
|
||||
|
||||
**Compatibility**
|
||||
|
||||
- Fully compatible with any markdown or html features:
|
||||
|
||||
<details>
|
||||
<summary>code snippets </summary>
|
||||
|
||||
```html
|
||||
<body>
|
||||
<img src="http://www.pudim.com.br/pudim.jpg">
|
||||
</body>
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>quotes</summary>
|
||||
|
||||
> just a quote
|
||||
|
||||
?> a cooler quote... <small> (at least i think it is)</small>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>images <small>(memorable)</small></summary>
|
||||
|
||||
<br/>
|
||||
<img src="https://avatars0.githubusercontent.com/u/5666881?s=400&u=d94729bdf16611396a720b338c115ec0be656ba6&v=4" width="64" height="64">
|
||||
</details>
|
||||
|
||||
- Fully compatible with major docsify plugins such as:
|
||||
|
||||
> [docsify-themeable](https://jhildenbiddle.github.io/docsify-themeable/)
|
||||
>
|
||||
> [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/)
|
||||
>
|
||||
> [docsify-copy-code](https://github.com/jperasmus/docsify-copy-code)
|
||||
>
|
||||
> [docsify-pagination](https://github.com/imyelo/docsify-pagination)
|
||||
|
||||
**Limitations**
|
||||
|
||||
*Nesting* (i.e panels within panels) all pannels are rendered as siblings, Nesting only avaiable throught post-render javascript code.
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
For multi-language documentation you can use it along with [docsify-tabs](https://jhildenbiddle.github.io/docsify-tabs/):
|
||||
|
||||
<!-- tabs:start -->
|
||||
|
||||
### **HTML**
|
||||
|
||||
```html
|
||||
<!-- HTML -->
|
||||
<h1>Heading</h1>
|
||||
<p>This is a paragraph.</p>
|
||||
```
|
||||
|
||||
### **JS**
|
||||
|
||||
```js
|
||||
// JavaScript
|
||||
function add(a, b) {
|
||||
return Number(a + b);
|
||||
}
|
||||
```
|
||||
|
||||
### **CSS**
|
||||
|
||||
```css
|
||||
/* CSS */
|
||||
body {
|
||||
background: white;
|
||||
}
|
||||
```
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
<!-- div:title-panel -->
|
||||
|
||||
## Installation
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
Just add the docsify-example-panels plugin to your `HTML` after docsify and you'll be ready to go.
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```html
|
||||
<!-- docsify (latest v4.x.x)-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||
|
||||
<!-- docsify-example-panels (latest) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-example-panels"></script>
|
||||
```
|
||||
|
||||
<!-- div:title-panel -->
|
||||
|
||||
## Usage
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
1. Define a panel in markdown file using `panels:start` and `panels:end` HTML comments.
|
||||
|
||||
HTML comments are used to mark the start and end of a panel. The use of HTML comments prevents any markup from being displayed when markdown is rendered as HTML outside of your docsify site (e.g. Github, GitLab, etc).
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```markdown
|
||||
<!-- panels:start -->
|
||||
|
||||
(...) - Awesome div wrapper
|
||||
|
||||
<!-- panels:end -->
|
||||
```
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
2. Define divs within a panel in markdown file using HTML comments.
|
||||
|
||||
The character sequence after double dot : will be rendered as the classname of the div content. all proceeding content will be associated with that div up to start of the next div `div:class` or end of panel `panels:end` comments.
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```markdown
|
||||
<!-- panels:start -->
|
||||
<!-- div:title-panel -->
|
||||
|
||||
(...) - Awesome title
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
(...) - Awesome explanation
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
|
||||
(...) - Awesome example
|
||||
|
||||
<!-- panels:end -->
|
||||
```
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
3. You can add a custom class aswell.
|
||||
|
||||
perphaps you want something like a "floating-cat" class
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```markdown
|
||||
<!-- panels:start -->
|
||||
<!-- div:your-custom-class -->
|
||||
|
||||
...
|
||||
|
||||
<!-- panels:end -->
|
||||
```
|
||||
|
||||
<!-- div:title-panel -->
|
||||
|
||||
## Customization
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
### Properties
|
||||
|
||||
Allows you to customize panels styles without rewriting any CSS rule.
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```css
|
||||
:root{
|
||||
/* Document */
|
||||
--docsify-example-panels-document-width: 90%;
|
||||
|
||||
/* Wrapper */
|
||||
--docsify-example-panels-wrapper-width: 100%;
|
||||
|
||||
/* Standards */
|
||||
--docsify-example-panels-padding-inner : 8px 16px;
|
||||
--docsify-example-panels-padding-surroundings : 0px;
|
||||
|
||||
/* Panel Left */
|
||||
--docsify-example-panels-left-panel-background : transparent;
|
||||
--docsify-example-panels-left-panel-width : 60%;
|
||||
|
||||
/* Panel Right */
|
||||
--docsify-example-panels-right-panel-background : transparent;
|
||||
--docsify-example-panels-right-panel-width : 40%;
|
||||
|
||||
/* Titles */
|
||||
--docsify-example-panels-title-background: transparent;
|
||||
}
|
||||
```
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
### Custom Properties
|
||||
|
||||
You can change properties by adding a `<style>` tag or `<link>` reference to your `index.html` file after all other stylesheets, just remember to set the properties within a `:root` selector.
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```html
|
||||
<style>
|
||||
:root {
|
||||
--docsify-example-panels-document-width: 100%; /* Remove margins */
|
||||
--docsify-example-panels-left-panel-width : 70%; /* bit more for left */
|
||||
--docsify-example-panels-right-panel-width : 30%; /* bit less for right */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
<br/>
|
||||
<br/>
|
||||
<!-- div:title-panel -->
|
||||
|
||||
### Custom Styles
|
||||
|
||||
Custom styles can be created using the classname you choosed in markdown
|
||||
|
||||
The main purpose of this plugin is to ease and rush the production of any markdown written documentation using the right panel as code examples.
|
||||
|
||||
But let's suppose you indeed wants to add a floating cat in your documentation, like i did.
|
||||
|
||||
<small> In case you insisted to read this in github url the cat image won't be so floating.</small>
|
||||
|
||||
<!-- div:floating-cat -->
|
||||
|
||||
<img src="assets/imgs/floating-cat.png">
|
||||
|
||||
<!-- div:title-panel -->
|
||||
<hr>
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
1. Add this to your markdown:
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```markdown
|
||||
<!-- panels:start -->
|
||||
<!-- div:floating-cat -->
|
||||
|
||||
<img src="path/to/floating-cat.png">
|
||||
|
||||
<!-- panels:end -->
|
||||
```
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
2. will be rendered like this:
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```html
|
||||
<div class="docsify-example-panels">
|
||||
<div class="docsify-example-panels floating-cat">
|
||||
<img src="path/to/floating-cat.png">
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
<!-- div:left-panel -->
|
||||
|
||||
3. Then you can play with css animations:
|
||||
|
||||
<!-- div:right-panel -->
|
||||
|
||||
```css
|
||||
.floating-cat{
|
||||
position:absolute;
|
||||
right: 0px;
|
||||
width: 200px;
|
||||
height: 360px;
|
||||
z-index: 1000;
|
||||
|
||||
-webkit-filter: drop-shadow(0px 5px 15px rgba(0,0,0,0.6));
|
||||
filter: drop-shadow(0px 5px 15px rgba(0,0,0,0.6));
|
||||
|
||||
transform: translatey(0px);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes float {
|
||||
0% {
|
||||
-webkit-filter: drop-shadow(0px 5px 5px rgba(0,0,0,0.8));
|
||||
filter: drop-shadow(0px 5px 5px rgba(0,0,0,0.6));
|
||||
transform: translatey(0px);
|
||||
}
|
||||
50% {
|
||||
box-shadow: drop-shadow(0px 25px 10px rgba(0,0,0,0.4));
|
||||
filter: drop-shadow(0px 25px 10px rgba(0,0,0,0.2));
|
||||
transform: translatey(-20px);
|
||||
}
|
||||
100% {
|
||||
box-shadow: drop-shadow(0px 5px 5px rgba(0,0,0,0.8));
|
||||
filter: drop-shadow(0px 5px 5px rgba(0,0,0,0.6));
|
||||
transform: translatey(0px);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- panels:end -->
|
||||
|
||||
## Contact & Support
|
||||
|
||||
- Create a [GitHub issue](https://github.com/VagnerDomingues/docsify-example-panels/issues) for bug reports, feature requests, or questions
|
||||
- Add a ⭐️ [star on GitHub](https://github.com/VagnerDomingues/docsify-example-panels) or ❤️ [tweet](https://twitter.com/intent/tweet?url=https%3A%2F%2Fgithub.com%2FVagnerDomingues%2Fdocsify-example-panels&hashtags=docsify,developers,frontend,plugin,documentation) to support the project!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](https://github.com/VagnerDomingues/docsify-example-panels/blob/master/LICENSE).
|
||||
|
||||
Plugin created by [Vagner Domingues Madeira](https://github.com/VagnerDomingues/)
|
||||
|
||||
<!-- GitHub Buttons -->
|
||||
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
@@ -1,209 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Yggdrasil is a collections of notes around IT, OT and other geeks stuff">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="icon" href="style/yggdrasil.ico">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/style.min.css"
|
||||
title="docsify-darklight-theme"
|
||||
type="text/css"
|
||||
/>
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Yggdrasil</div>
|
||||
<footer id="mb-footer"></footer>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'Yggdrasil',
|
||||
repo: 'https://github.com/Klagarge/Yggdrasil.git',
|
||||
maxLevel: 5,
|
||||
coverpage: 'style/_coverpage.md',
|
||||
loadNavbar: true, // Nav bar on top
|
||||
subMaxLevel: 5, // Add table of contents (TOC) in custom sidebar.
|
||||
auto2top: true, // Scrolls to the top of the screen when the route is changed.
|
||||
mergeNavbar: true, // Navbar will be merged with the sidebar on smaller screens.
|
||||
latex: {
|
||||
inlineMath : [['$', '$'], ['\\(', '\\)']], // default
|
||||
displayMath : [['$$', '$$']], // default
|
||||
customOptions: {} // default
|
||||
},
|
||||
select: {
|
||||
detectOperatingSystem: {
|
||||
enabled: false,
|
||||
menuId: "operating-system"
|
||||
}
|
||||
},
|
||||
flexibleAlerts: {
|
||||
style: 'callout'
|
||||
},
|
||||
markdown: {
|
||||
renderer: {
|
||||
|
||||
code: function (code, lang) {
|
||||
if (lang === 'drawio') {
|
||||
if (window.drawioConverter) {
|
||||
console.log('drawio')
|
||||
return window.drawioConverter(code)
|
||||
} else {
|
||||
return `<div class='drawio-code'>${code}</div>`
|
||||
}
|
||||
} else {
|
||||
return this.origin.code.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
darklightTheme: {
|
||||
siteFont : "Ubuntu",
|
||||
defaultTheme : 'light',
|
||||
codeFontFamily : 'Roboto Mono, Monaco, courier, monospace',
|
||||
bodyFontSize : '17px',
|
||||
dark: {
|
||||
coverBackground : 'linear-gradient(to left bottom, hsl(118, 18%, 34%) 0%,hsl(207, 63%, 10%) 100%)',
|
||||
},
|
||||
light: {
|
||||
coverBackground : 'linear-gradient(to left bottom, hsl(118, 100%, 85%) 0%,hsl(181, 100%, 100%) 100%)',
|
||||
}
|
||||
},
|
||||
kroki: {
|
||||
langs: [
|
||||
"actdiag",
|
||||
"blockdiag",
|
||||
"bpmn",
|
||||
"bytefield",
|
||||
"c4plantuml",
|
||||
"d2",
|
||||
"dbml",
|
||||
"ditaa",
|
||||
"erd",
|
||||
"excalidraw",
|
||||
"graphviz",
|
||||
"nomnoml",
|
||||
"nwdiag",
|
||||
"packetdiag",
|
||||
"pikchr",
|
||||
//"plantuml",
|
||||
"rackdiag",
|
||||
"seqdiag",
|
||||
"structurizr",
|
||||
"svgbob",
|
||||
"symbolator",
|
||||
"tikz",
|
||||
"vega",
|
||||
"vegalite",
|
||||
"wavedrom",
|
||||
"wireviz",
|
||||
"umlet",
|
||||
],
|
||||
},
|
||||
plantuml: {
|
||||
skin: "classic",
|
||||
renderAsObject: false,
|
||||
},
|
||||
count:{
|
||||
position: 'bottom',
|
||||
float: 'left',
|
||||
isExpected: false
|
||||
},
|
||||
timeUpdater: {
|
||||
text: "<div align='center' width='200px' style='color:gray;font-size:10px' >last update time: {docsify-updated}</div>",
|
||||
formatUpdated: "{YYYY}-{MM}-{DD} {HH}:{mm}",
|
||||
whereToPlace: "top",
|
||||
},
|
||||
tabs: {
|
||||
persist: true, // default
|
||||
sync: true, // default
|
||||
theme: 'classic', // default
|
||||
tabComments: true, // default
|
||||
tabHeadings: true // default
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!----- Docsify v4 ----->
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
|
||||
|
||||
<!----- Render PlantUML ----->
|
||||
<script src="//unpkg.com/docsify-puml/dist/docsify-puml.min.js"></script>
|
||||
|
||||
<!----- Render graph (UML/Mermaid/...) ----->
|
||||
<script src="//unpkg.com/docsify-kroki"></script>
|
||||
|
||||
<!----- Mermaid (use this one for Light and Dark theme ----->
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
||||
mermaid.initialize({ startOnLoad: true });
|
||||
window.mermaid = mermaid;
|
||||
</script>
|
||||
<script src="//unpkg.com/docsify-mermaid@2.0.1/dist/docsify-mermaid.js"></script>
|
||||
|
||||
<!----- Copy code button ----->
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
|
||||
|
||||
<!----- LateX ----->
|
||||
<script src="//cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-latex@0"></script>
|
||||
|
||||
<!----- Flexible alert ----->
|
||||
<script src="https://unpkg.com/docsify-plugin-flexible-alerts"></script>
|
||||
|
||||
<!----- Code highlights ----->
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-armasm.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-matlab.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-c.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-cpp.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-java.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-xml.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-sql.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-pascal.js"></script>
|
||||
|
||||
<!----- Dark Theme ----->
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-darklight-theme@latest/dist/index.min.js"> </script>
|
||||
|
||||
<!----- docsify-select (latest v1.x.x) ----->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-select@1"></script>
|
||||
|
||||
<!----- Draw.io ----->
|
||||
<script src="scripts/viewer.min.js"></script>
|
||||
<script src='scripts/drawio.js'></script>
|
||||
|
||||
<!----- Glossary ----->
|
||||
<script src="//unpkg.com/docsify-glossary/dist/docsify-glossary.min.js"></script>
|
||||
|
||||
<!----- Count word ----->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-count@latest/dist/countable.min.js"></script>
|
||||
|
||||
<!----- Progress ----->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-progress@latest/dist/progress.min.js"></script>
|
||||
|
||||
<!----- Accordion ----->
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify-accordion/src/style.css">
|
||||
<script src="//unpkg.com/docsify-accordion/src/index.js"></script>
|
||||
|
||||
<!----- Terminal ----->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-terminal-block@1.0.3"></script>
|
||||
|
||||
<!----- Last time updated ----->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-updated/src/time-updater.js"></script>
|
||||
|
||||
<!-- docsify-tabs (latest v1.x.x) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
|
||||
<style>
|
||||
:root {
|
||||
--docsifytabs-border-color: #ededed;
|
||||
--docsifytabs-tab-highlight-color: purple;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,158 +0,0 @@
|
||||
<header><h1>
|
||||
AD/DA
|
||||
</h1></header>
|
||||
|
||||
Il existe 4 méthodes principales pour une conversion Analogique vers Digital:
|
||||
- Parallèle
|
||||
- Par pesée
|
||||
- Par comptage
|
||||
- Sigma-delta
|
||||
|
||||
| | + (Avantage) | - (Inconvénient) |
|
||||
|-------------------------|------------------------|-----------------------------|
|
||||
| **Parallèle<br>Unaire** | Simplicité<br>Monotone | Taille du circuit ~$2^{n}$ |
|
||||
| **Pesée** | Taille du circuit ~$n$ | |
|
||||
| **Par comptage** | 1 seul switch | Nécessite d'aller très vite |
|
||||
|
||||
$$SNR [dB]=6,02\cdot n+1,78$$
|
||||
$n$ : nombre de bits du convertisseur
|
||||
|
||||
$$OSR = \frac{f_{s}}{2f_{0}}$$
|
||||
$OSR$ : OverSampling Ratio <br>
|
||||
$f_{s}$ : Sampling Frequency <br>
|
||||
$f_{0}$ : Entry Fréquency <br>
|
||||
|
||||
$$n=\frac{SNR}{20\log_{10}(2\cdot OSR)}$$
|
||||
$n$ : Ordre du filtre <br>
|
||||
$SNR$ : Signal sur bruit <br>
|
||||
$OSR$ : Oversampling ratio <br>
|
||||
|
||||
|
||||
|
||||
|
||||
# AD - Parallèle
|
||||
|
||||

|
||||
|
||||
----------
|
||||
|
||||
# AD - Par comptage
|
||||
Un seul switch mais avec une fréquence de commutation très rapide
|
||||
$$
|
||||
V_{a} = \frac{t_{on}}{T}\cdot V_{ref}
|
||||
$$
|
||||

|
||||

|
||||
|
||||
----------
|
||||
|
||||
# AD - Par pesée
|
||||
Un switch par bit.
|
||||
|
||||

|
||||
|
||||
Pour un $V_{red} = 1[V]$ on a:
|
||||
|
||||
| z2 | z1 | z0 | Va |
|
||||
|----|----|----|-------|
|
||||
| 0 | 0 | 0 | 0,000 |
|
||||
| 0 | 0 | 1 | 0,125 |
|
||||
| 0 | 1 | 0 | 0,250 |
|
||||
| 0 | 1 | 1 | 0,375 |
|
||||
| 1 | 0 | 0 | 0,500 |
|
||||
| 1 | 0 | 1 | 0,625 |
|
||||
| 1 | 1 | 0 | 0,750 |
|
||||
| 1 | 1 | 1 | 0,875 |
|
||||
|
||||
----------
|
||||
|
||||
# DA - Échantillonneur avec maintien
|
||||
|
||||
|
||||
# Caractéristique de convertisseurs
|
||||
|
||||
## Digits et affichage
|
||||
|
||||
$$
|
||||
nBits = Ceiling\left[log_2\cdot \frac{plage}{pas}\right]
|
||||
$$
|
||||
|
||||
----------
|
||||
|
||||
# Sigma / Delta
|
||||
|
||||
[sigma-delta.drawio](medias/sigma-delta.drawio ':include :type=code')
|
||||
|
||||
|
||||
## Principe
|
||||
Un convertisseur analogique-numérique (AD) ou numérique-analogique (DA) delta-sigma se compose toujours d'un modulateur
|
||||
delta-sigma produisant un "flux binaire" ou "signal binaire séquentiel" et d'un filtre passe-bas.
|
||||
|
||||
Le modulateur peut être mis en œuvre comme un circuit numérique pour un signal numérique ou comme un circuit analogique
|
||||
pour un signal analogique. De même, le filtre passe-bas peut être analogique pour générer un signal de sortie
|
||||
analogique ou numérique, réalisé soit par un circuit numérique soit par un algorithme dans un processeur de signal.
|
||||
Cette structure permet d'explorer le flux binaire et le filtre passe-bas avant d'approfondir l'étude du
|
||||
modulateur delta-sigma.
|
||||
|
||||
### Le flux binaire
|
||||
Le flux binaire peut être interprété comme un signal numérique ou analogique.
|
||||
Il s'agit d'un signal séquentiel à un bit avec une fréquence d'horloge significativement plus élevée que le débit des
|
||||
données numériques de l'ADC ou du DAC. Sa caractéristique essentielle est que sa moyenne correspond à la moyenne du
|
||||
signal d'entrée. Un niveau logique "haut" représente la valeur maximale de sortie, tandis qu'un niveau logique "bas"
|
||||
représente la valeur minimale de sortie.
|
||||
|
||||
- **Sortie analogique :** Le flux binaire est converti par un DA à un bit en un signal analogique.
|
||||
Les valeurs d'entrée "bas" et "haut" se traduisent respectivement par deux tensions analogiques précises, par exemple,
|
||||
1V et +1V.
|
||||
|
||||
- **Sortie numérique :** Dans le cas d'une sortie numérique, un niveau "haut" (ou "bas") dans le flux binaire représente
|
||||
la valeur maximale (ou minimale) de la plage de valeurs numériques. Par exemple, dans un système à 8 bits, un niveau
|
||||
"haut" pourrait correspondre à FF en hexadécimal, tandis qu'un niveau "bas" pourrait correspondre à 00 en hexadécimal.
|
||||
|
||||
Un flux binaire similaire existe dans un signal modulé en largeur d‘impulsions (PWM), mais il présente quelques
|
||||
désavantages par rapport au flux binaire d'un convertisseur delta-sigma, également appelé signal modulé en rapport
|
||||
d’impulsions (PPM). La transmission en série d'une valeur numérique, telle que la sortie en série d'un ADC conventionnel
|
||||
par pesée, est quant à elle appelée modulation par codage d'impulsions (PCM).
|
||||
|
||||
----------
|
||||
|
||||
#
|
||||
|
||||
Un convertisseur sigma-delta est un type de convertisseur
|
||||
analogique-numérique (CAN) qui utilise une boucle de contre-réaction pour
|
||||
échantillonner et quantifier un signal analogique en un signal numérique.
|
||||
Le principe de fonctionnement d'un convertisseur sigma-delta peut être
|
||||
expliqué en plusieurs étapes :
|
||||
|
||||
1. Échantillonnage : Tout d'abord, le signal analogique est échantillonné
|
||||
à une fréquence d'échantillonnage beaucoup plus élevée que la bande
|
||||
passante du signal. Cette haute fréquence d'échantillonnage permet de
|
||||
réduire considérablement les effets des distorsions et du bruit dans le
|
||||
domaine fréquentiel.
|
||||
2. Quantification : Le signal échantillonné est ensuite quantifié en un
|
||||
signal numérique à l'aide d'un comparateur qui compare le signal avec une
|
||||
référence de tension fixe. La sortie du comparateur est une suite de bits
|
||||
représentant la valeur du signal analogique à chaque instant.
|
||||
3. Contre-réaction : Le signal quantifié est ensuite utilisé pour régler
|
||||
un oscillateur numérique qui produit un signal d'erreur. Ce signal
|
||||
d'erreur est ajouté au signal d'entrée dans la boucle de contre-réaction,
|
||||
ce qui permet de compenser les erreurs de quantification et de réduire le
|
||||
bruit dans le signal quantifié.
|
||||
4. Suréchantillonnage : Le signal numérique produit par le convertisseur
|
||||
sigma-delta est suréchantillonné à une fréquence beaucoup plus élevée que
|
||||
la fréquence d'échantillonnage initiale, ce qui permet de réduire encore
|
||||
davantage les effets du bruit dans le signal.
|
||||
5. Décimation : Enfin, le signal suréchantillonné est décimé à la
|
||||
fréquence d'échantillonnage désirée en utilisant un filtre passe-bas
|
||||
numérique qui élimine les composantes de hautes fréquences indésirables.
|
||||
|
||||
Le résultat final est un signal numérique représentant fidèlement le
|
||||
signal analogique d'origine, avec une faible distorsion et un faible
|
||||
bruit. La boucle de contre-réaction permet au convertisseur sigma-delta
|
||||
d'atteindre des performances supérieures à celles des autres types de
|
||||
convertisseurs analogiques-numérique en termes de résolution et de
|
||||
précision.
|
||||
|
||||

|
||||
|
||||
----------
|
||||
@@ -1,339 +0,0 @@
|
||||
<header><h1>
|
||||
Filtres
|
||||
</h1></header>
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
# Diagramme de Bode
|
||||
|
||||
## H0 - Gain statique
|
||||
$$H_{0}(s)=A$$
|
||||

|
||||
|
||||
## H1 - Coupe bas, amplis haut
|
||||
$$H_{1}(s)=\frac{s}{\omega_{0}}$$
|
||||

|
||||
|
||||
## H2 - Ampli haut, coupe bas
|
||||
$$H_{2}(s)=\frac{\omega_{0}}{s}$$
|
||||

|
||||
|
||||
## H3 - Ampli haut
|
||||
$$H_{3}(s)=\frac{\omega_{0}+1}{\omega_{0}} = 1+ \frac{s}{\omega_{0}}$$
|
||||

|
||||
|
||||
## H4 - Coupe haut
|
||||
$$H_{4}(s)=\frac{\omega_{0}}{\omega_{0}+s} = \frac{1}{1+\frac{s}{\omega_{0}}}$$
|
||||

|
||||
|
||||
## Résonance
|
||||

|
||||
|
||||
----------
|
||||
|
||||
# Filtres numériques
|
||||
|
||||

|
||||
|
||||
## Filtres numériques récursifs (IIR)
|
||||
|
||||
Les filtres numériques récursifs (IIR) sont des filtres numériques qui utilisent des rétroactions.
|
||||
Ils sont basés sur des équations aux différences.
|
||||
Les filtres récursifs sont utilisés pour des applications où la phase du filtre n'est pas critique.
|
||||
Ils sont utilisés dans les télécommunications et la régulation.
|
||||
|
||||
Les intérêts des filtres récursifs :
|
||||
- Leur faible coût en calcul
|
||||
- Faible retard (télécommunication et la régulation)
|
||||
|
||||
Les inconvénients des filtres récursifs :
|
||||
- Ils possèdent une phase non-linéaire
|
||||
- Ils peuvent avoir une instabilité numérique
|
||||
|
||||
***Fonction de transfert en $z$ :***
|
||||
$$F(z) = \frac{Y(z)}{U(z)} = \frac{b_{0}+b_{1}z^{-1}+\dots+b_{n}z^{-n}}{1+a_{1}z^{-1}+\dots+a_{n}z^{-n}}$$
|
||||
Numérisation par conversion DA et AD d'un filtre analogique :
|
||||
$$F(z) = (1-z^{-1})Z \left\{ L^{-1}\left[ \frac{F(s)}{s} \right] \right\} $$
|
||||
La fonction de transfert en *z* permet ensuite de tirer l'équation aux différences qui servira à réaliser le filtre sous forme d'un algorithme récursif
|
||||
$$y[k]= b_{n}u[k-n]+ \dots + b_{1}u[k-1]+b_{0}u[k]-a_{n}y[k-n]-\dots-a_{1}y[k-1]$$
|
||||
|
||||
Si on souhaite connaître la réponse fréquentielle d'un filtre, il suffit de remplacer l'opérateur *z* par sa transformée de Fourier :
|
||||
$$a\cdot z^{n}=a\cdot \exp(n\cdot s\cdot T_{s})=a\cdot \exp(n\cdot \mathbf{j}\omega T_{s})$$
|
||||
$T_{s}$ : La période d'échantillonnage
|
||||
|
||||
Comme l’exponentielle imaginaire est périodique, la réponse fréquentielle d’un filtre numérique est également périodique
|
||||
de fréquence, et sa représentation se fait dans le domaine de fréquences allant de $0$ à $fs/2$. <br>
|
||||
Le gain statique d’un filtre peut être calculé selon :
|
||||
$$\text{Gain statique d'un filtre} = \lim_{ s \to 0 } F(s) = \lim_{ z \to 1 } F(z)$$
|
||||
|
||||
> [!NOTE|label:Passage pôle et zéro s ↔ z]
|
||||
> $$\begin{align}
|
||||
z&=\exp(s\cdot T_{s}) \\
|
||||
s&=\frac{1}{T_{s}\ln(z)}
|
||||
\end{align}$$
|
||||
|
||||

|
||||

|
||||
|
||||
### Transformation analogique-numérique
|
||||
La transformation bilinéaire : transformation de Tustin ou approximation de Padé de premier ordre
|
||||
$$s = \frac{2}{T_{s}} \frac{z-1}{z+1} \qquad \qquad z = \frac{1+\frac{1}{2}sT_{s}}{1-\frac{1}{2}sT_{s}}$$
|
||||
|
||||
***Pour un filtre analogique de premier ordre :***
|
||||
$$F(s)=\frac{d_{0}+d_{1}s}{c_{0}+c_{1}s} \to F(z) = \frac{b_{1}z^{-1}+b_{0}}{a_{1}z^{-1}+1} $$
|
||||
$$
|
||||
a_{1} = \frac{c_{0}-\frac{2c_{1}}{T_{s}}}{c_{0}+\frac{2c_{1}}{T_{s}}}
|
||||
\qquad \qquad
|
||||
b_{0} = \frac{d_{0}+\frac{2d_{1}}{T_{s}}}{c_{0}+\frac{2c_{1}}{T_{s}}}
|
||||
\qquad \qquad
|
||||
b_{1} = \frac{d_{0}-\frac{2d_{1}}{T_{s}}}{c_{0}+\frac{2c_{1}}{T_{s}}}
|
||||
$$
|
||||
|
||||
***Pour un filtre analogique de second ordre :***
|
||||
$$F(s)=\frac{Y(s)}{U(s)}=\frac{d_{0}+d_{1}s+d_{2}s^{2}}{c_{0}+c_{1}s+c_{2}s^{2}}
|
||||
\to F(z) = \frac{Y(z)}{U(z)}=\frac{b_{2}z^{-2}+b_{1}z^{-1}+b_{0}}{a_{2}z^{-2+}a_{1}z^{-1}+1} $$
|
||||
$$\begin{gather}
|
||||
C = c_{0}+\frac{2c_{1}}{T_{s}}+\frac{4c_{2}}{T_{s}^{2}} \\
|
||||
\\
|
||||
a_{1} = \frac{2\left( c_{0}-\frac{4c_{2}}{T_{s}^{2}} \right)}{C}
|
||||
\qquad \qquad
|
||||
a_{2} = \frac{ c_{0}-\frac{2c_{1}}{T_{s}}+\frac{4c_{2}}{T_{s}^{2}} }{C}
|
||||
\qquad \qquad
|
||||
b_{0} = \frac{d_{0}+\frac{2d_{1}}{T_{s}}+\frac{4d_{2}}{T_{s}^{2}}}{C} \\
|
||||
\\
|
||||
b_{1} = \frac{2\left( d_{0}-\frac{4d_{2}}{T_{s}^{2}} \right)}{C}
|
||||
\qquad \qquad
|
||||
b_{2} = \frac{d_{0}-\frac{2d_{1}}{T_{s}}+\frac{4d_{2}}{T_{s}^{2}}}{C}
|
||||
\end{gather}$$
|
||||
|
||||
## Filtre numérique non-récursif (FIR)
|
||||
|
||||
Les filtres numériques non-récursifs (FIR) sont des filtres numériques qui n'utilisent pas de rétroactions.
|
||||
Ils sont basés sur des équations aux différences.
|
||||
Les filtres non-récursifs sont utilisés pour des applications où la phase du filtre est critique.
|
||||
Ils sont utilisés dans le traitement du signal et la conception de filtres.
|
||||
|
||||
Les intérêts des filtres non-récursifs :
|
||||
- Les fonctions de transfert ne possèdent que des coefficients $b_i$. ($\forall a_{i} = 0$)
|
||||
- Ils possèdent une phase linéaire exacte
|
||||
- Ils sont stables
|
||||
|
||||
Les inconvénients des filtres non-récursifs :
|
||||
- Ils sont plus coûteux en calcul
|
||||
- Ils nécessitent plus de matériel
|
||||
|
||||
Coefficients symétrique: pairs $b_{n-i} = b_{i}$, impairs $b_{n-i} = -b_{i}$, $b_{n/2}=0$ <br>
|
||||
|
||||
**Symétrie paire:**
|
||||
$$H(\mathbf{j}\omega)
|
||||
= \sum^{n}_{i=o}b_{i}e^{-\frac{\mathbf{j}i\omega}{f_{s}}}
|
||||
= e^{\frac{-\mathbf{j}n\omega}{2f_{s}}} \sum^{n/2}_{i=0}b_{i}\cos\left[ \frac{\left( i-\frac{n}{2} \right)\omega}{f_{s}} \right]$$
|
||||
|
||||
**Symétrie impaire:**
|
||||
$$H(\mathbf{j}\omega)=\mathbf{j}e^{\frac{-\mathbf{j}n\omega}{2f_{s}}}\sum^{n/2}_{i=0}b_{i}\sin\left( \frac{(i-\frac{n}{2})\omega}{f_{s}} \right)$$
|
||||
|
||||
Les filtres FIR à coefficients symétriques ont une phase linéaire.
|
||||
$$
|
||||
\text{Symétrie paire : }\varphi(\omega)=-\frac{n\omega}{2f_{s}}
|
||||
\qquad \qquad
|
||||
\text{Symétrie impaire : } \varphi(\omega)= -\frac{n\omega}{2f_{s}}+\frac{\pi}{2}
|
||||
$$
|
||||
$$\text{Temps de propagation : }t_{gr}=-\frac{d\varphi}{d\omega}=\frac{n}{2f_{s}}=\frac{n}{2}T_{s}$$
|
||||
Il y a donc 4 types de filtres FIR à phase linéaire : n resp. symétrie paire/impaire
|
||||
|
||||
|
||||
***Fonction de transfert en $z$ :***
|
||||
$$F(z) = \frac{Y(z)}{U(z)} = b_{0}+b_{1}z^{-1}+\dots+b_{n}z^{-n}$$
|
||||
|
||||
***Équation aux différences :***
|
||||
$$y[k]= b_{n}u[k-n]+ \dots + b_{1}u[k-1]+b_{0}u[k]$$
|
||||
|
||||
***Réponse en fréquence :***
|
||||
$$z=\exp(\mathbf{j}\omega\cdot T_{s})$$
|
||||
|
||||
### Filtres FIR à symétrie paire :
|
||||
$$b_{n-k} = b_{k}$$
|
||||
|
||||
Exemple : FIR à symétrie paire d'ordre 6
|
||||
$$\begin{align}
|
||||
F(z) &= \frac{Y(z)}{U(z)} \\
|
||||
&= 1+2z^{-1}+3z^{-2}+4z^{-3}+3z^{-4}+2z^{-5}+z^{-6} \\
|
||||
&= \frac{z^{6}+2z^{5}+3z^{4}+4z^{3}+3z^{2}+2z^{1}+1}{z^{6}}
|
||||
\end{align}$$
|
||||
Les filtres FIR à symétrie paire sont toujours de type passe-bas ou coupe-bande. <br>
|
||||
Ils possèdent autant de pôles que l'order du filtre. Ces derniers sont à (0,0) sur le cercle unité. <br>
|
||||
Ils possèdent autant de zéros que l'ordre du filtre. Ces derniers sont sur le cercle unité. <br>
|
||||
|
||||
### Filtres FIR à symétrie impaire :
|
||||
$$b_{n-k} = -b_{k}$$
|
||||
Exemple : FIR à symétrie impaire d'ordre 6
|
||||
$$\begin{align}
|
||||
F(z) &= \frac{Y(z)}{U(z)} \\
|
||||
&= -1-2z^{-1}-3z^{-2}+0z^{-3}+3z^{-4}+2z^{-5}+z^{-6} \\
|
||||
&= \frac{z^{6}+2z^{5}+3z^{4}+0z^{3}-3z^{2}-2z^{1}-1}{z^{8}}
|
||||
\end{align}$$
|
||||
Les filtres FIR à symétrie paire sont toujours de type passe-haut ou passe-bande. <br>
|
||||
Ils possèdent autant de pôles que l'ordre du filtre. Ces derniers sont à (0,0) sur le cercle unité. <br>
|
||||
Ils possèdent autant de zéros que l'ordre du filtre. <br>
|
||||
|
||||
## Filtres numériques FIR idéaux
|
||||
$$\omega_{c_{d}} = 2\pi \frac{f_{c_{a}}}{f_{s}}$$
|
||||
|
||||
### Filtre passe-bas idéal
|
||||
$$H(\mathbf{j}\omega)=\begin{cases}
|
||||
1 &\text{ si } |\omega| \leq \omega_{c} \\
|
||||
0 &\text{ si } |\omega| > \omega_{c}
|
||||
\end{cases} $$
|
||||
La transformée de Fourier inverse donne la réponse impulsionnelle d’un filtre passe-bas idéal.
|
||||
|
||||
$$h[n] = \begin{cases}
|
||||
\frac{\omega_{c}}{\pi} \frac{\sin((n-N_{s})\omega_{c})}{(n-N_{s})\omega_{c}} &\text{ si } 0\leq n \leq N \\
|
||||
\\
|
||||
\frac{\omega_{c}}{\pi} &\text{ si } n=N_{s} \\
|
||||
\\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
|
||||
On tronque à $N=ordre$ du filtre. $N_{s}=\frac{N}{2}$ <br>
|
||||
Ces coefficients ne sont rien d’autre que les coefficients de la fonction de transfert en *z* du filtre passe-bas idéal.
|
||||
|
||||
### Filtre passe-haut idéal
|
||||
$$H(\mathbf{j}\omega)=\begin{cases}
|
||||
0 &\text{ si } |\omega| \leq \omega_{c} \\
|
||||
1 &\text{ si } |\omega| > \omega_{c}
|
||||
\end{cases}$$
|
||||
La transformée de Fourier inverse donne la réponse impulsionnelle d’un filtre passe-bas idéal.
|
||||
|
||||
$$h[n] = \begin{cases}
|
||||
-\frac{\omega_{c}}{\pi} \frac{\sin((n-N_{s})\omega_{c})}{(n-N_{s})\omega_{c}} &\text{ si } 0\leq n \leq N \\
|
||||
\\
|
||||
1-\frac{\omega_{c}}{\pi} &\text{ si } n=N_{s} \\
|
||||
\\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
|
||||
On tronque à $N=ordre$ du filtre. $N_{s}=\frac{N}{2}$ <br>
|
||||
Ces coefficients ne sont rien d’autre que les coefficients de la fonction de transfert en *z* du filtre passe-haut idéal.
|
||||
|
||||
### Filtre passe-bande idéal
|
||||
$$H(\mathbf{j}\omega)=\begin{cases}
|
||||
1 &\text{ si } \omega_{c_{1}} \leq \omega \leq \omega_{c_{2}} \\
|
||||
0 &\text{ si } \omega \leq \omega_{c_{1}} \quad et \quad \omega\geq \omega_{c_{2}}
|
||||
\end{cases}$$
|
||||
La transformée de Fourrier inverse donne la réponse impulsionnelle d’un filtre passe-bande idéal.
|
||||
|
||||
$$h[n] = \begin{cases}
|
||||
\frac{\omega_{c_{2}}}{\pi} \frac{\sin((n-N_{s})\omega_{c_{2}})}{(n-N_{s})\omega_{c_{2}}}-\frac{\omega_{c_{1}}}{\pi} \frac{\sin((n-N_{s})\omega_{c_{1}})}{(n-N_{s})\omega_{c_{1}}} &\text{ si } 0\leq n \leq N \\
|
||||
\\
|
||||
\frac{\omega_{c_{2}}-\omega_{c_{1}}}{\pi} &\text{ si } n=N_{s}\\
|
||||
\\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
|
||||
On tronque à $N=ordre$ du filtre. $N_{s}=\frac{N}{2}$ <br>
|
||||
Ces coefficients ne sont rien d’autre que les coefficients de la fonction de transfert en *z* du filtre passe-bande idéal.
|
||||
|
||||
### Filtre coupe-bande idéal
|
||||
$$H(\mathbf{j}\omega)=\begin{cases}
|
||||
0 &\text{ si } \omega_{c_{1}} \leq \omega \leq \omega_{c_{2}} \\
|
||||
1 &\text{ si } \omega \leq \omega_{c_{1}} \quad et \quad \omega\geq \omega_{c_{2}}
|
||||
\end{cases}$$
|
||||
La transformée de Fourrier inverse donne la réponse impulsionnelle d’un filtre coupe-bande idéal.
|
||||
|
||||
$$h[n] = \begin{cases}
|
||||
\frac{\omega_{c_{1}}}{\pi} \frac{\sin((n-N_{s})\omega_{c_{1}})}{(n-N_{s})\omega_{c_{1}}}
|
||||
-\frac{\omega_{c_{2}}}{\pi} \frac{\sin((n-N_{s})\omega_{c_{2}})}{(n-N_{s})\omega_{c_{2}}}
|
||||
&\text{ si } 0\leq n \leq N \\
|
||||
\\
|
||||
1-\frac{\omega_{c_{2}}-\omega_{c_{1}}}{\pi} &\text{ si } n=N_{s}\\
|
||||
\\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
|
||||
On tronque à $N=ordre$ du filtre. $N_{s}=\frac{N}{2}$ <br>
|
||||
Ces coefficients ne sont rien d’autre que les coefficients de la fonction de transfert en *z* du filtre coupe-bande idéal.
|
||||
|
||||
## Synthèse par fenêtrage
|
||||
Le point de départ de la synthèse des filtres FIR est donné par la considération des réponses impulsionnelles des filtres idéaux.
|
||||
Comme celles-ci sont infiniment longues et donc non réalisables, on va se contenter d’une approximation de leurs réponses.
|
||||
|
||||
### Méthode de fenêtrage
|
||||
- **Fenêtre rectangulaire :** $$W[n] = \begin{cases}
|
||||
1 &\text{ si } 0\leq n \leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
- **Fenêtre triangulaire (Bartlett) :** $$W[n] = \begin{cases}
|
||||
2 \frac{n}{N} &\text{ si } 0\leq n \leq \frac{N}{2} \\
|
||||
2-2 \frac{n}{N} &\text{ si } \frac{N}{2}\leq n \leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
- **Fenêtre sinusoïdale (Hann) :** $$W[n] = \begin{cases}
|
||||
0,5\left( 1-\cos\left( \frac{2\pi n}{N} \right) \right) &\text{ si } 0\leq n\leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
- **Fenêtre de Hamming :** $$W[n] = \begin{cases}
|
||||
0,54-0,46\cos\left( \frac{2\pi n}{N} \right) &\text{ si } 0\leq n\leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
- **Fenêtre de Blackman :** $$W[n] = \begin{cases}
|
||||
0,42-0,5\cos\left( \frac{2\pi n}{N} \right)+0,08\cos\left( \frac{4\pi n}{N} \right) &\text{ si } 0\leq n\leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
- **Fenêtre de Kaiser-Bessel :** $$W[n] = \begin{cases}
|
||||
\frac{I_{0}\left( \beta\sqrt{1-\left( \frac{1-n}{N_{s}} \right)^{2}} \right)}{I_{0}(\beta)} &\text{ si } 0\leq n\leq N \\
|
||||
0 &\text{ sinon}
|
||||
\end{cases}$$
|
||||
$I_{0}$ : Fonction de Bessel modifiée d’ordre 0 <br>
|
||||
$\beta$ : Paramètre de forme
|
||||
|
||||
***Caractéristiques spectrales de quelques fenêtres***
|
||||
|
||||
| Fenêtres | Largeur du 1er lobe | Atténuation du 1er lobe [dB] | Décroissance des lobes suivants | Ondulation Rp [dB] | Atténuation max [dB] | Bande de transition<br>[rad/sec]<br>$\Delta\omega$ |
|
||||
|---------------------|---------------------|------------------------------|---------------------------------|--------------------|----------------------|----------------------------------------------------|
|
||||
| Rectangulaire | $4\pi/N$ | $-13$ | $20 dB/dec$ | $0,74$ | $21$ | $1,8\pi/N$ |
|
||||
| Triangulaire | $8\pi/N$ | $-27$ | $40 dB/dec$ | $0$ | $25$ | $6,1\pi/N$ |
|
||||
| Cosinus | $8\pi/N$ | $-32$ | $60 dB/dec$ | $0,055$ | $44$ | $6,2\pi/N$ |
|
||||
| Hamming | $8\pi/N$ | $-43$ | $20 dB/dec$ | $0,014$ | $53$ | $6,6\pi/N$ |
|
||||
| Blackman | $12\pi/N$ | $-58$ | $60 dB/dec$ | $0,0017$ | $74$ | $11\pi/N$ |
|
||||
| Kaiser $\beta=4,54$ | $7,2\pi/N$ | $-30$ | $20 dB/dec$ | $0,025$ | $50$ | $5,8\pi/N$ |
|
||||
| Kaiser $\beta=5,66$ | $8,4\pi/N$ | $-42$ | $20 dB/dec$ | $0,009$ | $60$ | $7,2\pi/N$ |
|
||||
|
||||
|
||||
|
||||
### Comment calculer son filtre
|
||||
|
||||

|
||||
|
||||
$H(\mathbf{j}\omega)$ : Gain <br>
|
||||
$\delta_{1}$ : L'erreur de gain tolérable <br>
|
||||
$\delta_{2}$ : L'erreur d'atténuation tolérable <br>
|
||||
$\omega_{p}$ : Fréquence de pulsation de passage <br>
|
||||
$\omega_{a}$ : Fréquence de pulsation d'arrêt <br>
|
||||
|
||||
1. En fonction du gabarit, déterminer le type de filtre désiré (`passe-bas`, `passe-haut`, `passe-bande`, `coupe-bande`)
|
||||
2. Calculer $f_{c} \to \omega_{c}$ et $\Delta f \to \Delta\omega$
|
||||
$$\begin{gather}
|
||||
f_{c} = \frac{f_{a}+f_{p}}{2} \qquad & \qquad \Delta f = f_{a}-f_{p} \\
|
||||
\omega_{c} = 2\pi \frac{f_{c}}{f_{s}} \quad & \quad \Delta\omega = \frac{2\pi\Delta f}{f_{s}}
|
||||
\end{gather}$$
|
||||
3. Choisir une fenêtre $W[n]$ satisfaisante du point de vue de l’atténuation et du reste du cahier des charges.(voir tableau ci-dessus)
|
||||
4. Calculer l'ordre du filtre à l'aide de la bande de transition du filtre choisit. Arrondir au nombre pair supérieur.
|
||||
5. Calculer la réponse impulsionnelle $h_{f}[n]$ du filtre avec $h[n]*W[n]$
|
||||
6. Calculer les coefficients $b_{n}$ du filtre avec $h_{f}[n]$
|
||||
|
||||
<details>
|
||||
<summary>Démarche selon les slides</summary>
|
||||
|
||||
1. Connaissant le gabarit du filtre désiré, choisir le filtre idéal correspondant
|
||||
2. Calculer les pulsations caractéristiques se situant au centre des bandes de transitions
|
||||
3. Rechercher la réponse impulsionnelle $h[n]$ du filtre idéal, si celle-ci n’est pas connue, on peut la calculer par transformation de Fourier inverse
|
||||
4. Choisir une fenêtre $W[n]$ satisfaisante du point de vue de l’atténuation
|
||||
5. Connaissant la largeur de la bande de transition , calculer l’ordre du filtre $N$
|
||||
6. Calculer les coefficients du filtre en multipliant la réponse impulsionnelle par la fenêtre choisie
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
|
||||
----------
|
||||
@@ -1,9 +0,0 @@
|
||||
<header>
|
||||
<h1>Électronique</h1>
|
||||
</header>
|
||||
|
||||
# Convertisseurs
|
||||
- [AD/DA](/infotro/Eln/ADDA)
|
||||
- [Signaux et échantillonnage](/infotro/Eln/SignauxEtEchantillonnage)
|
||||
- [Filtres](/infotro/Eln/Filtres)
|
||||
- [RF](/infotro/Eln/RF)
|
||||
@@ -1,145 +0,0 @@
|
||||
<header><h1>
|
||||
Radio Fréquence
|
||||
</h1></header>
|
||||
|
||||
----------
|
||||
|
||||
# Skin effect
|
||||
Le courant électrique circule principalement à la surface des conducteurs.
|
||||
|
||||
Pour un fil de cuivre rond, la résistance en continue est donnée par la formule suivante:
|
||||
$$R_{DC} = \rho\frac{l}{\pi r²}$$
|
||||
avec:
|
||||
- $R_{DC}$: résistance en continue
|
||||
- $\rho$: résistivité du cuivre
|
||||
- $l$: longueur du fil
|
||||
- $r$: rayon du fil
|
||||
|
||||
La résistance en courant alternatif est donnée par la formule suivante:
|
||||
$$R_{AC} = R_{DC} \frac{r}{2\delta}$$
|
||||
avec:
|
||||
- $R_{AC}$: résistance en courant alternatif
|
||||
- $R_{DC}$: résistance en continue
|
||||
- $r$: rayon du fil
|
||||
- $\delta$: profondeur de pénétration du courant
|
||||
|
||||
La profondeur de pénétration du courant est donnée par la formule suivante:
|
||||
$$\delta = \sqrt{\frac{\rho}{\pi f \mu_0}}$$
|
||||
avec:
|
||||
- $\delta$: profondeur de pénétration du courant
|
||||
- $\rho$: résistivité du cuivre
|
||||
- $f$: fréquence
|
||||
- $\mu_0$: perméabilité magnétique du vide $= 4\pi 10^{-7} H/m$
|
||||
|
||||
On peut en déduire la résistance en courant alternatif:
|
||||
$$R_{AC} = \frac{l\sqrt{\rho\pi\mu_0}}{2\pi r} \sqrt{f} \qquad \qquad R_{AC(f_{2})} = R_{AC(f_{1})} \cdot \sqrt{ \frac{f_{2}}{f_{1}} }$$
|
||||
|
||||
Pour calculer l'inductance et la capacité d'un fil, on peut utiliser les formules suivantes :
|
||||
$$\begin{align}
|
||||
L&=\frac{\mu_{0}l}{\pi}acosh\left( \frac{d}{2r} \right) \\
|
||||
C&=\frac{\pi\epsilon_{0}l}{acosh\left( \frac{d}{2r} \right)}
|
||||
\end{align}$$
|
||||
avec :
|
||||
- $\mu_{0}$: perméabilité magnétique du vide $=4\pi\cdot 10^{-7}\left[ \frac{H}{m} \right]$
|
||||
- $\epsilon_{0}$: permittivité du vide $=8.854\cdot 10^{-12}\left[ \frac{F}{m} \right]$
|
||||
|
||||
# Model à haute fréquence
|
||||
|
||||
## Inductance
|
||||

|
||||
|
||||
<u>Facteur de qualité :</u>
|
||||
$$Q(f) = \frac{X_{L}(f)}{R(f)} \qquad \qquad R_{AC}(f)= \frac{l\sqrt{ \rho \pi \mu_{0} }}{2\pi r}\sqrt{ f}$$
|
||||
|
||||
<u>Réactance & Impédance:</u>
|
||||
$$X_{L}=\omega L=2\pi fL \qquad \qquad Z_{L}=jX_{L}=j\omega L$$
|
||||
|
||||
<u>Résonance :</u> $\omega_{r} ≃\omega_{0}=\frac{1}{\sqrt{ LC }}$ <br>
|
||||
SRF: Self Resonant Frequency = $\frac{\omega_{r}}{2\pi}$
|
||||
|
||||
$$Z_{L,tot} = (j\omega L+R) \mid \mid \frac{1}{j\omega C} =\frac{sL+R}{1+sRC+s^{2}LC}$$
|
||||
|
||||

|
||||
|
||||
# Capacité
|
||||

|
||||
|
||||
<u>Facteur de qualité :</u>
|
||||
$$Q(f) = \frac{X_{C}(f)}{R} \qquad \qquad \omega_{r} ≃\omega_{0}=\frac{1}{\sqrt{ LC }}$$
|
||||
|
||||
<u>Réactance & Impédance:</u>
|
||||
$$X_{C}=\frac{1}{\omega C} \qquad \qquad Z_{C}=\frac{1}{j\omega C}$$
|
||||
|
||||

|
||||
|
||||
# Résistance
|
||||

|
||||
|
||||
|
||||
# Transmission line
|
||||
|
||||
$$ k = \frac{2\pi}{\lambda} \qquad \qquad \lambda = \frac{c_{mat}}{f} $$
|
||||
|
||||
$\Gamma$ (Gamma) : coefficient de réflexion
|
||||
|
||||
# Smith Chart
|
||||
|
||||
|
||||
## Ajouter une ligne de réflexion
|
||||
|
||||
|
||||
1. Placer $Z1$
|
||||
1. Normaliser ($\hat{Z_{1}}=\frac{Z_{1}}{Z_{0}}$)
|
||||
2. Placer le point au croisement de l'axe réel et imaginaire (Impédance = bleu)
|
||||
2. Chercher $\Gamma$ (indice de reflexion)
|
||||
1. Tracer l'angle ("Angle of reflexion coefficient in degrees")
|
||||
2. Tracer le module (cercle au compas + rapport sur les échelles du bas)
|
||||
3. Ajouter la ligne selon le sens
|
||||
- $\circlearrowright$ : toward generator
|
||||
- $\circlearrowleft$ : toward load
|
||||
4. Lire les nouvelles valeures
|
||||
1. Tracer le nouvel angle $\to \angle(\Gamma_{2})$
|
||||
2. Tracer et lire $\hat{Z_{2}}$ puis $Z_{2}$
|
||||
|
||||

|
||||
|
||||
# Impedance Matching
|
||||
$Z_{LD} \to Z_S^*$, sans résistance
|
||||
- Capacité + inductance
|
||||
- ligne de transmission + capacité / inductance + ligne de transmission
|
||||
- ligne de transmission + stub + ligne de transmission
|
||||
-
|
||||

|
||||
|
||||
## Stub
|
||||
Pour trouver la longueur du stub :
|
||||
1. Pour un short stub, placer une inductance parallèle et pour un open stub un condensateur parallèle
|
||||
2. Trouver la valeur normalisée de cette admittance.
|
||||
3. Pour un short stub, placer le delta dans les négatifs (parti du dessus) et pour un open stub dans les positifs (partie du dessous).
|
||||
4. Regarder la longueur d'onde. Partir du côté open circuit pour un open stub et du côté short circuit pour un short stub.
|
||||
|
||||
# Filtres
|
||||
|
||||
Filter components <br>
|
||||
 <br>
|
||||

|
||||
|
||||
Normalized value for Butterworth order 1 to 10 <br>
|
||||

|
||||
|
||||
Normalized value for Chebyshev 0.5dB Ripple <br>
|
||||

|
||||
|
||||
Normalized value for Chebyshev 3dB Ripple <br>
|
||||

|
||||
|
||||
## Filtres passe-bas en microstrip
|
||||
$l$ : longueur physique en mm <br>
|
||||
$\theta$ : longueur électrique / degré <br>
|
||||
$$ l_l = g_k * \frac{\lambda_{gl} * Z_0}{2\pi *Z_{0l}} $$
|
||||
$$ l_c = g_k * \frac{\lambda_{gc} * Z_{0c}}{2\pi *Z_0} $$
|
||||
$$ \theta_l = g_k * \frac{360 * Z_0}{2\pi *Z_{0l}} $$
|
||||
$$ \theta_c = g_k * \frac{360* Z_{0c}}{2\pi *Z_0} $$
|
||||
|
||||

|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
<header><h1>
|
||||
Signaux et échantillonnage
|
||||
</h1></header>
|
||||
|
||||
-----------
|
||||
|
||||
# Fourier
|
||||
Pour comprendre les signaux, il est important de comprendre la transformée de Fourier.
|
||||
La transformée de Fourier est une technique de traitement du signal utilisée pour analyser et manipuler les signaux continus.
|
||||
Elle permet d'exprimer un signal dans le temps en domaine fréquentiel.
|
||||
|
||||
## Transformée de Fourier
|
||||
La transformée de Fourier est une technique de traitement du signal utilisée pour analyser et manipuler les signaux continus.
|
||||
Elle permet d'exprimer un signal dans le temps en domaine fréquentiel.
|
||||
|
||||
La transformée de Fourier est définie par la formule suivante :
|
||||
|
||||
Soit $f(x)$ une fonction non périodique définie sur $\mathbb{R}$.
|
||||
$$\begin{align}
|
||||
\text{Transformée de Fourier de }f(x) &: \qquad \widehat{f}(\omega) = \int^{\infty}_{-\infty}f(x)e^{-\mathbf{i}\omega x}dx \qquad (\omega\in \mathbb{R})\\
|
||||
\text{Intégrale de Fourier de } f(x) &: \qquad \frac{1}{2\pi}\int_{-\infty}^{\infty} \widehat{f}(\omega) e^{\mathbf{i}x\omega}\, d\omega \qquad(x \in\mathbb{R}).
|
||||
\end{align}$$
|
||||
**Théorème** : Si $\int_{-\infty}^{\infty} |f(x) \, dx < \infty$ et si $f$ est une fonction de variation bornée sur tout $\mathbb{R}$, alors
|
||||
$$
|
||||
\frac{1}{2\pi}\int_{-\infty}^{\infty} \widehat{f}(\omega)e^{\mathbf{i}x\omega} \, d\omega = \begin{cases}
|
||||
f(x) &\text{si }f \text{ est continue au point }x \\
|
||||
\text{milieu du saut de } f(x) &\text{si }f \text{ a un saut en }x
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
|
||||
## Transformée de Fourier discrète
|
||||
|
||||
La transformée de Fourier discrète (DFT - Discret Fourier Transform) est une transformation discrète utilisée pour analyser le spectre fréquentiel
|
||||
d'un signal discret.
|
||||
Elle est l'équivalent discret de la transformée de Fourier, qui est utilisée pour analyser les signaux continus.
|
||||
|
||||
On pose: $\Omega := \omega \cdot T_{s}$
|
||||
La transformation de Fourier de $x[k]$ est donnée par la formule suivante :
|
||||
$$\mathcal{F}\{x[k]\} = X(\Omega) \quad := \quad\sum^{+\infty}_{k=-\infty}x[k]\cdot e^{-jk\Omega}= \mathcal{F}\{x_{s}(t)\}$$
|
||||
|
||||
|
||||
$$\mathcal{F\{x[k]\} = \mathcal{Z}\{x[k]\}}|_{z=e^{j\Omega}}$$
|
||||

|
||||
$$x[k]=\frac{1}{2\pi} \int^{+\pi}_{-\pi}X(\Omega)e^{jk\Omega} d\Omega$$
|
||||
|
||||
|
||||
|
||||
|
||||
La DFT est utilisée dans de nombreux domaines, notamment en traitement du signal, en télécommunications, en traitement d'images
|
||||
et en compression de données.
|
||||
|
||||
> [!NOTE|label:FFT]
|
||||
> La transformée de Fourier rapide (FFT) est un algorithme efficace pour calculer la DFT. Elle est largement utilisée dans
|
||||
> de nombreuses applications, notamment en traitement du signal et en compression de données.
|
||||
|
||||
----------
|
||||
|
||||
# Réponse impulsionnelle
|
||||
La réponse impulsionnelle $h[k]$ d'un système discret LIT est sa réponse à l'impulsion unitaire à l'origine $δ[k]$.
|
||||
|
||||

|
||||
|
||||
Avant l'impulsion, la réponse impulsionnelle $h$ est nulle.
|
||||
Après l'impulsion, la réponse impulsionnelle $h$ est la réponse du système à l'impulsion unitaire à l'origine.
|
||||
|
||||
*Causal* = le futur n'a pas d'influence sur le passé.
|
||||
|
||||
$$ h[k] = \mathcal{H} \{ δ[k] \} $$
|
||||
|
||||
$\mathcal{H}$ : Heaviside (peut aussi se noter $\varepsilon$ pour un heaviside en discret) <br>
|
||||
$y[k]$ : réponse du système <br>
|
||||
$x[k]$ : signal d'entrée <br>
|
||||
$h[k]$ : réponse impulsionnelle <br>
|
||||
|
||||
$$
|
||||
\forall x \in \mathbb{R}, \mathcal{H}(x)=\begin{cases}
|
||||
0 &\text{ si } x<0 \\
|
||||
1 &\text{ si } x \geq 0
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
Pour un système causal
|
||||
|
||||
$$\begin{align}
|
||||
y[k] &= \sum^{k}_{n=-\infty}(x[n]\cdot h[k-n]) \\
|
||||
\\
|
||||
y[k] &= \sum^{+\infty}_{n=0}(x[k-n]\cdot h[n])
|
||||
\end{align}$$
|
||||
|
||||
La réponse impulsionnelle caractérise <u>ENTIÈREMENT</u> un système LIT.
|
||||
|
||||
## Système LIT stable
|
||||
Un système LIT (Linéaire et Invariant dans le Temps) est stable si et seulement si sa réponse impulsionnelle est sommable.
|
||||
|
||||
> [!NOTE]
|
||||
> $\sum^{+\infty}_{k=-\infty}|h[k]|<+\infty$
|
||||
|
||||
## Système LIT causal à réponse impulsionnelle finie
|
||||
Un système LIT (Linéaire et Invariant dans le Temps) est causal à réponse impulsionnelle finie si et seulement si sa
|
||||
réponse impulsionnelle est nulle pour $k<0$ et sommable pour $k\geq0$.
|
||||
|
||||
> [!NOTE]
|
||||
> fini: $h(k) |_{k<\infty} = 0$
|
||||
|
||||
$$\begin{align}
|
||||
y[k] &= \sum^{N}_{n=0}x[k-n]\cdot h[n] \\
|
||||
&= b_{0}x[k]+b_{1}x[k-1]+\dots+b_{N}x[k-N]
|
||||
\end{align}$$
|
||||
$N$ : Limite de notre réponse impulsionnelle.
|
||||
|
||||

|
||||
|
||||
En faisant la somme, la réponse impulsionnelle se transforme en facteurs $(b_0, b_1, b_2, ..., b_n)$.
|
||||
On peut retrouver la réponse impulsionnelle à partir de ces facteurs.
|
||||
La somme décomposée est appelée *équation aux différences*.
|
||||
|
||||
Pour obtenir la réponse indicielle depuis la réponse impulsionnelle, on utilise la somme cumulée de la réponse impulsionnelle.
|
||||
On peut aussi mathématiquement obtenir la réponse impulsionnelle en multipliant la réponse indicielle par Heaviside ($F(z)\cdot \varepsilon(z)$).
|
||||
|
||||
----------
|
||||
|
||||
# Transformée en Z
|
||||
La transformée en Z, également connue sous le nom de transformée en fréquence Z, est une technique de traitement du
|
||||
signal utilisée pour analyser et manipuler les signaux numériques.
|
||||
Elle permet de décomposer un signal en fonction de la fréquence et de fournir des informations sur la nature du signal.
|
||||
|
||||
La principale différence entre la transformée en Z et la transformation de Laplace (ou S) est que la transformée en Z
|
||||
est adaptée aux signaux discrets, c'est-à-dire aux valeurs qui ne varient pas continuellement dans le temps.
|
||||
En revanche, la transformation de Laplace est utilisée pour les signaux continus et permet d'étudier leur réponse en
|
||||
fréquence et en phase.
|
||||
|
||||
En résumé, la transformée en Z est conçue pour les signaux discrets et les systèmes numériques, tandis que la
|
||||
transformation de Laplace s'applique aux signaux continus et aux systèmes analogiques.
|
||||
|
||||
[Tableau des transformées en Z](/infotro/Eln/Ztransform)
|
||||
|
||||
> [!NOTE|label:S]
|
||||
> Domaine fréquentiel continue avec Laplace.
|
||||
|
||||
> [!NOTE|label:Z]
|
||||
> Domaine fréquentiel discret.
|
||||
|
||||
Avec $z:=e^{sT_{s}}$ on a
|
||||
$$\mathcal{Z}\{x[k]\} = X(z) \quad := \quad \sum^{+\infty}_{k=-\infty}(x[k]\cdot z^{-k})=\mathcal{L\{x_{s}(t)\}}$$
|
||||
qui est la transformée en Z de $x[k]$.<br>
|
||||
|
||||
$T_{s}$ : Période d'échantillonnage. <br>
|
||||
$x[k]$ : Fonction du [Tableau des transformées en Z](/infotro/Eln/Ztransform) ou valeur du graphe des réponses impulsionnelle<br>
|
||||
|
||||
On a aussi la propriété suivante :
|
||||
$$\mathcal{Z}\{x[k-n]\}=z^{-n}\cdot X(z)$$
|
||||
|
||||
## Marche à suivre - Transformée en *Z*
|
||||
1. Avoir une équation aux différences.
|
||||
2. Pour chaque terme de l'équation, appliquer la transformée en Z.
|
||||
Avec :
|
||||
- $f[k] => F(z)$
|
||||
- $f[k-n] => z^{-n}\cdot F(z)$
|
||||
3. Isoler $Y(z)$.
|
||||
4. $H(z) = \frac{Y(z)}{X(z)}$ est la fonction de transfert.
|
||||
|
||||
> [!NOTE|label:Example]
|
||||
> L'équation aux différences est donnée par $$\tag{1} y[k] = \frac{1}{2}(x[k]+y[k-1])$$
|
||||
> devient
|
||||
> $$\begin{align}
|
||||
\tag{2} Y(z) &= \frac{1}{2}(X(z)+z^{-1}\cdot Y(z)) \\
|
||||
Y(z)-\frac{1}{2}\cdot z^{-1}\cdot Y(z) &= \frac{1}{2}\cdot X(z) \\
|
||||
\tag{3} Y(z) &= \frac{1}{2-z^{-1}}X(z) \\
|
||||
\tag{4} H(z) &= \frac{Y(z)}{X(z)} = \frac{1}{2-z^{-1}}
|
||||
\end{align}$$
|
||||
|
||||
> [!TIP]
|
||||
> $h[k]\stackrel{\text{Z tabel}}{\longleftrightarrow}H(z)$
|
||||
>
|
||||
> Exemple: $h[k] = 2^{-(k+1)}$ devient
|
||||
> $$\begin{align}
|
||||
2^{-(k+1)} &= 2^{-k}\cdot 2^{-1} = \frac{1}{2}\cdot 2^{-k} = \frac{1}{2}\cdot (2^{-1})^k \\
|
||||
&\to \frac{1}{2}\cdot \frac{z}{z-\frac{1}{2}} =\frac{z}{2z-1} = \frac{1}{2-\frac{1}{z}} = \frac{1}{2-z^{-1}}
|
||||
\end{align}$$
|
||||
|
||||
> [!WARNING]
|
||||
> Les propriétés de la transformée en Z se trouvent dans le [Tableau des transformées en Z](/infotro/Eln/Ztransform)
|
||||
|
||||
|
||||
L'équation aux différences
|
||||
$$y[k] + a_{1}y[k-1]+\dots+a_{M}y[k-M] \quad = \quad b_{0}x[k]+b_{1}x[k-1]+\dots+b_{N}x[k-N]$$
|
||||
devient par la transformation en z
|
||||
$$H(z) = \frac{Y(z)}{X(z)} = \frac{b_{0}+b_{1}z^{-1}+\dots+b_{N}z^{-1}}{1+a_{1}z^{-1}+\dots+a_{M}z^{-M}} \quad a_{m}, b_{n} \space \text{réels}$$
|
||||
|
||||
Les zéros et les pôles de la fonction de transfert donnent donc :
|
||||
$$H(z) = K \frac{(z-o_{1})\cdot (z-o_{2})\cdot \space \dots \space \cdot (z-o_{N})}{(z-p_{1})\cdot (z-p_{2})\cdot \space \dots \space \cdot (z-p_{M}) }\quad o_{n}, p_{n} \space \text{complexes}$$
|
||||
|
||||
|
||||
## Marche à suivre - Transformée en *Z* inverse
|
||||
1. Avoir une fonction de transfert : $H(z) = \frac{Y(z)}{X(z)}$.
|
||||
2. Effectuer le numérateur et le dénominateur.
|
||||
3. Mettre les $z$ en puissance négative : $\cdot \frac{z^{-1}}{z^{-1}}$
|
||||
4. Effectuer le produit en croix.
|
||||
5. Pour chaque terme, appliquer la transformée en Z inverse.
|
||||
- $F(z) \implies f[k]$
|
||||
- $F(z)\cdot z^{-n} \implies f[k-n]$
|
||||
6. Isoler $y[k]$. Et on a notre équation aux différences.
|
||||
|
||||
----------
|
||||
|
||||
# Approximation bilinéaire
|
||||
L'approximation bilinéaire est une technique utilisée pour convertir une fonction de transfert continue en une fonction
|
||||
de transfert discrète.
|
||||
Elle est utilisée dans le domaine du traitement du signal pour convertir un système continu en un système discret.
|
||||
|
||||
Développement en série de la fonction $ln$ autour de 1 ($x=0$) :
|
||||
$$\ln\left( \frac{1+x}{1-x} \right) = 2\left(x + \frac{x^{3}}{3} + \frac{x^{5}}{5} + \dots \right)$$
|
||||
donne l'approximation
|
||||
$\ln\left( \frac{1+x}{1-x} \right) \approx 2x$
|
||||
|
||||
## Approximation bilinéaire - Pôles et zéros
|
||||
$$p_{di} = e^{p_{ai} \cdot T} \qquad z_{di} = e^{z_{ai} \cdot T} $$
|
||||
- $k_a$, $p_{ai}$ and $z_{ai}$ sont le gain, les pôles et les zéros de notre filtre analogique
|
||||
Attention, $k_a$ n'est pas le gain DC
|
||||
- $k_d$, $p_{di}$ and $z_{di}$ sont le gain, des pôles et les zéros de notre filtre numérique.
|
||||
Pour un filtre passe bas sans zéros, le gain numérique est:
|
||||
$$k_{d} = k_{a}\cdot \prod_{i} \frac{1-e^{P_{ai}\cdot T}}{P_{ai}}$$
|
||||
|
||||
On peut appliquer la transformation bilinéaire aux pôles et zéros du filtre analogique pour obtenir les pôles et zéros du filtre numérique.
|
||||
$$
|
||||
p_{di} = \frac{1+p_{ai}\cdot \frac{T}{2}}{1-p_{ai}\cdot \frac{T}{2}}
|
||||
\qquad \qquad
|
||||
z_{di} = \frac{1+z_{ai}\cdot \frac{T}{2}}{1-z_{ai}\cdot \frac{T}{2}}
|
||||
$$
|
||||
Pour un filtre passe bas, le gain "bilinear" est :
|
||||
$$k_{d} = k_{a}\cdot \frac{1}{\prod_{i}\left( \frac{2}{T}-p_{ai} \right)}$$
|
||||
|
||||
Pour passer de $\omega_o'$ à $\omega_o$ et vice-versa, on utilise la relation suivante :
|
||||
$$\begin{align}
|
||||
\omega_{0} &= 2f_{s}\tan\left( \frac{\omega_{0}'}{2f_{s}} \right) \\
|
||||
\\
|
||||
\omega_{0}'&=2f_{S}\cdot \arctan\left( \frac{\omega_{0}}{2fs} \right)
|
||||
\end{align}$$
|
||||
|
||||
|
||||
## Autres méthodes d'approximation d'un système continu LIT par un syst. discret
|
||||
On détermine la réponse impulsionnelle du système discret LIT à partir de celle du système continu en appliquant la relation :
|
||||
$$h[k] = T_{s}\cdot(kT_{s})$$
|
||||
$h(kT_s)$ : Réponse impulsionnelle en continu <br>
|
||||
|
||||
On peut du coup approximer l'équation différentielle par une équation aux différences.
|
||||
Par exemple pour la dérivée première de $x(t)$, on a
|
||||
$$x'[k]= \frac{x[k]-x[k-1]}{T_{s}}$$
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----------
|
||||
@@ -1,30 +0,0 @@
|
||||
<header><h1>
|
||||
Tableau des transformées en Z
|
||||
</h1></header>
|
||||
|
||||
-----------
|
||||
|
||||
$$f[k] =\varepsilon[x]=\begin{cases}
|
||||
1^{x} &\text{ si } x\geq 0 \\
|
||||
0 &\text{ si } x<0
|
||||
\end{cases} \qquad \forall x=k,k-n
|
||||
$$
|
||||
$\varepsilon$ : fonction de Heaviside en discret
|
||||
|
||||
----------
|
||||
|
||||
| Nr. | $f[k]$ | $F(z)$ | $f(t)$ | $F(s)$ |
|
||||
|------|-------------------|----------------------------------------|-------------|--------------------|
|
||||
| 1 | $\delta_{k}, 0^k$ | 1 | $\delta(t)$ | 1 |
|
||||
| 2 | $1^k$ | $\frac{z}{z-1}$ | $1$ | $\frac{1}{s}$ |
|
||||
| 3 | $kT$ | $\frac{Tz}{z-1}^{2}$ | $t$ | $\frac{1}{s^{2}}$ |
|
||||
| 4 | $(kT)^{2}$ | $\frac{T^{2}z(z+1)}{(z-1)^{3}}$ | $t^{2}$ | $\frac{2!}{s^{3}}$ |
|
||||
| 5 | $(kT)^{3}$ | $\frac{T^{3}z(z^{2}+4z+1)}{(z-1)^{4}}$ | $t^{3}$ | $\frac{3!}{s^{4}}$ |
|
||||
| 6 | $e^{akT}$ | $\frac{z}{z-e^{aT}}$ | $e^{at}$ | $\frac{1}{s-a}$ |
|
||||
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
Dans la théorie Eln la partie transformée en Z commence à la slide 23 de "Eln2_LTI_0910_f.pdf".
|
||||
@@ -1,133 +0,0 @@
|
||||
<header><h1>
|
||||
Lab 06 - Filters
|
||||
</h1></header>
|
||||
|
||||
----------
|
||||
|
||||
In this laboratory on digital filters, the main objective is to understand how digital filters work.
|
||||
We will use MATLAB to simulate and implement these filters on a digital signal processor (DSP).
|
||||
|
||||
# Analog filter design
|
||||
|
||||
We must design Butterworth low-pass filter that cuts at 1kHz as shown with the following equation :
|
||||
|
||||
$$F(s) = \frac{1}{\frac{s^{2}}{\omega_c^{2}} + s\cdot\frac{1.414}{\omega_c} + 1} $$
|
||||
|
||||
In according to the sample theory, we have to choose at least 2 times the maximum frequency of the signal.
|
||||
In this case, the maximum frequency is 1 kHz, so we have to choose a sample frequency at least at 2 kHz.
|
||||
We take 10 time $f_c$ for the sample frequency, so we choose 20 kHz.
|
||||
|
||||
|
||||
## Transfer function
|
||||
For calculate the transfer function, we have to calculate the poles and zeros of the filter.
|
||||
|
||||
$$p_{di} = e^{p_{ai} \cdot T} \qquad z_{di} = e^{z_{ai} \cdot T} $$
|
||||
|
||||
- $k_a$, $p_{ai}$ and $z_{ai}$ are the gain, poles and zeros of the analog filter that the representation *zpk*.
|
||||
Be careful, $k_a$ is not the DC gain.
|
||||
- $k_d$, $p_{di}$ and $z_{di}$ are the gain, poles and zeros of the digital filter.
|
||||
|
||||
For a low pass filter without zeros, the "digital" gain is :
|
||||
$$k_{d} = k_{a}\cdot \prod_{i} \frac{1-e^{P_{ai}\cdot T}}{P_{ai}}$$
|
||||
|
||||
We can apply the bilinear transformation to the poles and zeros of the analog filter to obtain the poles and zeros of the digital filter.
|
||||
$$
|
||||
p_{di} = \frac{1+p_{ai}\cdot \frac{T}{2}}{1-p_{ai}\cdot \frac{T}{2}}
|
||||
\qquad \qquad
|
||||
z_{di} = \frac{1+z_{ai}\cdot \frac{T}{2}}{1-z_{ai}\cdot \frac{T}{2}}
|
||||
$$
|
||||
And for the low pass filter, the "bilinear" gain is :
|
||||
$$k_{d} = k_{a}\cdot \frac{1}{\prod_{i}\left( \frac{2}{T}-p_{ai} \right)}$$
|
||||
|
||||
On the following diagram, we can see the bode diagram of the analog, digital filter and the filter with the bilinear transformation.
|
||||
|
||||

|
||||
|
||||
# IIR filter design
|
||||
|
||||
We must design an IIR filter with the following specifications :
|
||||
|
||||
- $H_0 = 0 [dB]$
|
||||
- $H_1 = -3 [dB]$
|
||||
- $H_2 = -40 [dB]$
|
||||
- $f_{pass} = 7 [kHz]$
|
||||
- $f_{stop} = 9 [kHz]$
|
||||
- $f_s = 48 [kHz]$
|
||||
|
||||
We first calculate the analog filter for search the order with the function *ellipord*.
|
||||
Then, we calculate the poles and zeros with the function *ellip*.
|
||||
We apply the bilinear transformation to the poles and zeros to obtain the poles and zeros of the digital filter.
|
||||
Finally, we can plot the bode diagram of the digital filter and the filter with the bilinear transformation.
|
||||
|
||||

|
||||
|
||||
|
||||
# DSP implementation
|
||||
|
||||
We will implement the previously designed digital filter using a C program that will run in real-time on a fixed-point DSP.
|
||||
We will employ a cascade structure, consisting of 2 second-order stages.
|
||||
We will use the TMS320C6211 DSK from Texas Instruments.
|
||||
|
||||
The following equation represent an universal transfer function of a second-order filter :
|
||||
$$H(z) = \frac{b_{0}+b_{1}\cdot z^{-1}+b_{2}\cdot z^{-2}}{a_{0}+a_{1}\cdot z^{-1}+a_{2}\cdot z^{-2}}$$
|
||||
|
||||
The following figure depicts the block diagram of this implementation in the case where a0 = 1
|
||||
This structure is commonly referred to as "Direct Form I." It requires the storage of past values of both the input and output.
|
||||
|
||||

|
||||
|
||||
So we have the difference equation :
|
||||
$$y(n) = b_{0}\cdot x(n)+b_{1}\cdot x(n-1)+b_{2}\cdot x(n-2)-a_{1}\cdot y(n-1)-a_{2}\cdot y(n-2)$$
|
||||
|
||||
It exists a second structure called "Direct Form II" which is more efficient in terms of computation and memory usage.
|
||||
|
||||

|
||||
|
||||
The difference equation is :
|
||||
$$\begin{align}
|
||||
y(n) &= tmp\cdot b_{0}+w[0]\cdot b_{1}+w[1]\cdot b_{2} \\
|
||||
\\
|
||||
tmp &= x(n)-a_{1}\cdot w[0]-a_{2}\cdot w[1] \\
|
||||
\\
|
||||
w[0] &= tmp_{prev} \\
|
||||
\\
|
||||
w[1] &= w[0]_{prev}
|
||||
\end{align}$$
|
||||
|
||||
The variables can be translated into code by shifting the coefficients 14 positions to the left to enhance precision
|
||||
after the decimal point, as the DSP only works with integers.
|
||||
|
||||
Structures for the coefficients and the state variables are defined as follows :
|
||||
```c
|
||||
typedef struct {
|
||||
short b0;
|
||||
short b1;
|
||||
short b2;
|
||||
short a0;
|
||||
short a1;
|
||||
short a2;
|
||||
} coeff_sec_order;
|
||||
|
||||
short w3[2];
|
||||
short w4[2];
|
||||
|
||||
coeff_sec_order stage1 = { 469, 349, 469, 16384, -23699, 10692};
|
||||
coeff_sec_order stage2 = {16384, -11187, 16384, 16384, -19657, 14890};
|
||||
```
|
||||
|
||||
The following code is the implementation of the Direct Form II function :
|
||||
```c
|
||||
void iir_sec_order_dir_s(coeff_sec_order Coeff, short*w, short* Input, short* Output) {
|
||||
register short tmp;
|
||||
|
||||
tmp = (long)* Input - (( (Long)Coeff.a1 * (long)w[0]) >> SHIFT) - (( (long)Coeff.a2 * (long)w[1]) >> SHIFT);
|
||||
*Output = (( (long)Coeff.b0 * (long)tmp >> SHIFT) + (( (long)Coeff.b1 * (long)w[0]) >> SHIFT) + (( (long)Coeff.b2 * (long)w[1]) >> SHIFT);
|
||||
|
||||
w[1] = w[0];
|
||||
w[0] = tmp;
|
||||
}
|
||||
```
|
||||
|
||||
For a better effect of the filter, the function can be called several times.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Do not edit this file with editors other than draw.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="840px" height="286px" viewBox="-0.5 -0.5 840 286" content="<mxfile host="app.diagrams.net" modified="2024-02-03T22:03:26.442Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0" etag="Da41eLOf3AT8AQXfy7Nj" version="22.1.22"><diagram name="Page-1" id="CkdF-7UHB_tb0CRjxC_V">7V1bc6M2FP41nmkfkkHcbB7XTtLtTDrTaWbb5mkHxzKmxcjFcmLvr68wkgGJq0EGOyQPQQddz/k4+nQkyEibrfe/BPZm9RtaQG+kKov9SHsYqSrQNZX8CSWHSDI2lEjgBO6CZooFL+4PSIUs285dwG0qI0bIw+4mLXxDvg/fcEpmBwH6SGdbIi/d6sZ2oCB4ebM9UfqXu8ArKjUVJb7xFbrOijZtsRtr+5Q5EmxX9gJ9RKJjHu1xpM0ChHB0td7PoBcqj+klqugp5+6pYwH0cZUCT8D2/dmvz1N4mKu+M3/5tvzzzqB9wwc2YLgg46dJFOAVcpBve4+xdBqgnb+AYa0KScV5nhHaECEgwn8gxgdqTHuHERGt8Nqjd7c4QP/CGfJQcGxTmyjh7+kOUzRR0TTqX9ip3GEz/aJd8AYLxkqRiO3Agbggn34yDkE1RGuIgwMpF0DPxu57uh82hZdzyhdbgFxQI9QwCO3ku+3taEsj1fRId6dLRAZMEGzTQZr/7ULoTL/Ndz7exWly5dC/x3LzWEBGpv8YGY+j8fQOjMYPkYRlJP2d84WJLGqXiTm0YLjHhRZdup7H5D7yQ/zYnuv4JPlGLAiJfPoOA+ySR+4LvbF2F4sj0j5WLoYvm2jEH8TBCOhLoCrsZzVMhTkpOIHB0nRMCk0/2WvXC30WVS8be9hXuC/GoogdWmDCnAN1hhp9ZJWP2LXoVLRKOBUmax1t+oC2z4M2Xe8YbVYG2kJIzL8rDBu8wQPX9p1c6yTtIVo+HyYCJnKtSdpzfYdakqb+oKoCRTNWI8tZatpyqtmx5RgbE0x3Z38H12i744PnuZuvjJJEt5/hUq5lwclGbAqYdG1aIFiuZxQQ+osvIZOP0UEkT244zGPzRPXB4e9k4jVM3Bss+bBP3nw40FR1allKGa2GlJEW/R25x1mXOXDeDUw4EESUl5ZKUn++IpOrSOMqigYoVETUbh8S2TZhhm1Bh/XsDuf2qzg/uYh6EKP7pNwGgFf7D3gG6BjDrykISwe0KgXQ47YAPba4iixJgM7psFyAai2y8nBq3v/k/yyQ7/pEmzrfgWMn5nNz0jemlrWki0j2VRK1y5Hs7plY74NxXTMx6QxLs6z7yXlTkgAnsSpJLCtuqSrPEkpImsjMXF+kDr6oyBcZSte+aJxhOs5WDvE9m9zB0w0Ze86yK3WVApS+eegsPPMe2/PczbYCfu3tJtqvWrr70H/XA/SZKCywtWiGrrRcAXriRJTQbHpKbHmz6VwNGhkaZLKGkxj/lJy5puKqAVw1OZNXW3PFZDB6LaNraWupZxodFFcj2ehgIhr5czBeuHdxIsJDUq+0d+F1XChM1A/vlG6FN93irsamDWDdj83zgCkwooy6JAV5jPRBCknc2Oo/9JtBGySAHcNcOrRZAKQ0xGlKeQasSQ5nrR2z5zy8blzWN7OH4hoAmg6Tg04B2hXuzPZ8r7DwquF8W4MfKCeEF1+Adh7dZUq4sRVonh06U3PWDky/VyMVVDgsQUusnrV1Mlg93+o3sQZVzVvnOek1ptISzykl4mZFPhQ9Za0TIqDwmx48ga6MT8Af2700FR9fD0T7s1aUTrHVcYsUO6sy2bCqEHOVTrFB385DqlmnlG+AYk9KmcJF1cy4wzWRrXIVDhS7xOoVVvWD1W+NYms3H+tW2uUvhdN1eQRckUJ/7njXYd2bphL/qMZ52NTrVSsbqxUibBcnRZ3HHbUqAbHrI0VaztKzMzVfXwSqggoHUlRidWOw+ickRVcUd+xPUKdCPDGaqyTEE9sM/4Dzwz91j7eIbZW8lVdWQM6BGC3r2Cd9xVi9xtPivXnFuPPD5NoVHPNruMUixdWVrvS0nCOojV0dd4ip0WESwPlN/cITLYP1DaNPwgZf+6ACRriyT0Hh7MNxmZUpF8ZV7z+c0KKn0dWK7Etvyr6afc8oK3ozfNAowYmu+WVroHFTSfffz2rzVf0Bb33HW+df0NLFNVDP5pybe0UcqPyJfiumHrV5i1ahsrbW/Rp/bCluK79/pWXkrP712/n2AagHbLlvgpVHyyQtIXmg62dTfb3CYrS1R4Zv6xKvgun9jwR3+0qXrIgu79nP/hyV6NYrxjgaA/QioVm99+dPu2MHeQZp7ueEmtrCkD4W9x6skr6VFZEEvKwTqk2+hHYYvoQmbbEk4FfeoRWSjL+bH4Et/u8D2uP/</diagram></mxfile>"><defs><style type="text/css">@import url(https://fonts.googleapis.com/css2?family=Architects+Daughter:wght@400;500);
</style><style xmlns="http://www.w3.org/1999/xhtml" id="MJX-SVG-styles">
mjx-container[jax="SVG"] {
 direction: ltr;
}

mjx-container[jax="SVG"] > svg {
 overflow: visible;
 min-height: 1px;
 min-width: 1px;
}

mjx-container[jax="SVG"] > svg a {
 fill: blue;
 stroke: blue;
}

mjx-container[jax="SVG"][display="true"] {
 display: block;
 text-align: center;
 margin: 1em 0;
}

mjx-container[jax="SVG"][display="true"][width="full"] {
 display: flex;
}

mjx-container[jax="SVG"][justify="left"] {
 text-align: left;
}

mjx-container[jax="SVG"][justify="right"] {
 text-align: right;
}

g[data-mml-node="merror"] > g {
 fill: red;
 stroke: red;
}

g[data-mml-node="merror"] > rect[data-background] {
 fill: yellow;
 stroke: none;
}

g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
 stroke-width: 70px;
 fill: none;
}

g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
 stroke-width: 70px;
 fill: none;
}

g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
 stroke-dasharray: 140;
}

g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
 stroke-linecap: round;
 stroke-dasharray: 0,140;
}

g[data-mml-node="mtable"] > g > svg {
 overflow: visible;
}

[jax="SVG"] mjx-tool {
 display: inline-block;
 position: relative;
 width: 0;
 height: 0;
}

[jax="SVG"] mjx-tool > mjx-tip {
 position: absolute;
 top: 0;
 left: 0;
}

mjx-tool > mjx-tip {
 display: inline-block;
 padding: .2em;
 border: 1px solid #888;
 font-size: 70%;
 background-color: #F8F8F8;
 color: black;
 box-shadow: 2px 2px 5px #AAAAAA;
}

g[data-mml-node="maction"][data-toggle] {
 cursor: pLine truncated
|
||||
|
Before Width: | Height: | Size: 82 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Do not edit this file with editors other than draw.io -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="520px" height="366px" viewBox="-0.5 -0.5 520 366" content="<mxfile host="app.diagrams.net" modified="2024-02-03T22:16:47.416Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0" etag="gL6vpL3MQfkpyT-y1ADy" version="22.1.22"><diagram name="Page-1" id="CkdF-7UHB_tb0CRjxC_V">7Vxtj6M2EP41SO2HXfFmEj5esru9Slup6up6t59OJPESWoJTQjbJ/fqa2A7BQDAsDpBDd7rDg23MPI/H4/EQxZiu9r+Fznr5B1pAX9HVxV4xHhRdt8dj/G8sOBAB0EZE4Ibegoi0RPDi/YBUqFLp1lvATapihJAfeeu0cI6CAM6jlMwJQ7RLV3tDfvqpa8eFGcHL3PGz0q/eIlpSqaWqyY3P0HOX9NE2u7FyTpWJYLN0FmhHRMc6xqNiTEOEInK12k+hH+uO6YV09FRw9zSwEAaRSIMnzQmC6e/PE3iY6YE7e/ny9vedTnp5d/wtfWFFt3zc3+QN4W6xwpw5uWH9t41HOvky2wbRNinjK5f+f2w3SwQm/vtDAY/KaHKnKaMHImEV8UhnfGMsI89l4qPiogNDI4L7WL6JQvQvnCIfhUe5MVbjP/GoPd9n8gAFuNnE8T03wMU51hPE8sk7DCMPI/yJ3lh5i0X8gMlu6UXwZU3eeIfpjGUh2gYLGKsw7n4ZrXx8qVH95I2AjI1xRac1KbE1wMr0nVRafnJWnh9PEape9u7xWOG+EHHtxCM8/yBawSg84CqsgUopodLJZ7DyLqGySUXLMxIzmUMnj3vqOuEXvqAUq0A3c6DbT0Q302qZbnYO3WJOzL6rjBw84qHnBG4hPOeAZKEv5kmGFIVw4ud5gUuhpKW/qKq0fMAbgW6chk5v21Kw9T+D3Z3zXesjeMep53vrz3QI9PYzfJMLrW1xa8C4bWS1DHBwgX0uWkRhtEQuChz/MZFydjGp84zQmir0HxhFB2r4nG2E0ogXw5vVOgwWn2LXMSEHljx58WseH481Hx6+nRde48I9YMWH/fnNhwMtkfeOX/YyltjFdUIXRmWWLYt5CH0n8t7T/echSJv+ibzjsnsyAyZnBngWbNA2nEPa7NzZzPRkpHvSbK4n8oqZnrDincNZtXVcYXNpyFb+kItHdrkBviBjSBh+UvAHSK93n/T1SA33XvSNPi++PmuFS0mjuFB9IhC+XVKsIThjdF3KlBnpDc2YMe+rWx+dMCkr3QSHjQa993gB3/8S/Jpx0qs75NRGD7742ao/6pw/l7f1I754L925K/ri7XtsViF2+oDdReyA2TZ2oxzsOLBcbOnWhW9PI6vOjFVXK2tF69oeJI/QvHvm483aRoDAzmZNAs9v3j5eLaoxuiYNL2CdhaEtLQtQL7vVOtNsegGursOLDmZdDYIcDTLZB31J3gOs6Uly3WhifmRTXuJ4AL0S6Nw+Wa8Juna5G8mgs9duc5UZd83lZTq4sVWmCIbW1Jy3Le22xRFQ4bDMlKCet58cUL/xdcYQMKnXXmda38wYIgawf+sMwbo764zRP4sjoMJhnSlBHQyo/4TrTF7ogmYf6H0MeXYl+6D1eKjZ/ewDeqCaHKK+KmdnqLkHqsnhbXJe+0qf3UxGQulBrNHyQaxm8H4Zf4AqbHr4nkyejZKtj5k1Ll3jqKQMmZaTCZjZL+UwORFrnsM6z2Hbvreshmic11ljWThc4hCwyrJwLjeQk4VjWt2fWPUM+WnR6PjEIauEhImjcv7iuCnjbwi6nlWnjK22MQFGnZ8ANVYA+fmYpRMA2IITwBxLmgCAmwC1MzeNsp5aT0Qz884YP5KIdhgS0aR9FKLz6bZtH8uZ9m3bwLTjkFg96TaQRcbKd4ByTKDKraiGWjv2xKfBj667AWQWuPsUTQcptM5TVHSVlsRQLjhqNsZQk4+uy2ZoThS8Ywy9zRAFi4uXhyjkcHjMx/dBTQpn0nX4jhoLTYD8SVc8ty43kLMzA9lD3J0CJqoCHgbns346NmcmRc/q+T1/Y84nyB4ixzBrA8xNwix6yiQP5uypcbRaDwg3hrDoLlIewv0JJZ9ix9UcnJZCycIOjqRQsj3imMZ/uCzq4PAdGdfeRHY/1lvLRRc+CqmwNW2Qv6YgfYGsPSaXOQN42onvMe2SnqQdHoKqh4fgGh66wJctV/+SrvVoKjv4uLHkU1BwTNPaB4uqgJq7lYYooMIh+bQEdYHPqgbUby351OpBbLWfsVNR30xSdpfGR6PqumY2f45wbbtk9Ieh9fKo2mKo1TJDDd6xtu9ZtkVlI6qNyjuTTdQeZNL20pSKpjsBST9Up/E/u1U3SpPdUQn+UF1jFAWdp2g/P0gQNaWSKGpzTqRRO47Ie6ONxRFxMfn5cVI9+Q134/F/</diagram></mxfile>"><defs><style type="text/css">@import url(https://fonts.googleapis.com/css2?family=Architects+Daughter:wght@400;500);
</style><style xmlns="http://www.w3.org/1999/xhtml" id="MJX-SVG-styles">
mjx-container[jax="SVG"] {
 direction: ltr;
}

mjx-container[jax="SVG"] > svg {
 overflow: visible;
 min-height: 1px;
 min-width: 1px;
}

mjx-container[jax="SVG"] > svg a {
 fill: blue;
 stroke: blue;
}

mjx-container[jax="SVG"][display="true"] {
 display: block;
 text-align: center;
 margin: 1em 0;
}

mjx-container[jax="SVG"][display="true"][width="full"] {
 display: flex;
}

mjx-container[jax="SVG"][justify="left"] {
 text-align: left;
}

mjx-container[jax="SVG"][justify="right"] {
 text-align: right;
}

g[data-mml-node="merror"] > g {
 fill: red;
 stroke: red;
}

g[data-mml-node="merror"] > rect[data-background] {
 fill: yellow;
 stroke: none;
}

g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
 stroke-width: 70px;
 fill: none;
}

g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
 stroke-width: 70px;
 fill: none;
}

g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
 stroke-dasharray: 140;
}

g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
 stroke-linecap: round;
 stroke-dasharray: 0,140;
}

g[data-mml-node="mtable"] > g > svg {
 overflow: visible;
}

[jax="SVG"] mjx-tool {
 display: inline-block;
 position: relative;
 width: 0;
 height: 0;
}

[jax="SVG"] mjx-tool > mjx-tip {
 position: absolute;
 top: 0;
 left: 0;
}

mjx-tool > mjx-tip {
 display: inline-block;
 padding: .2em;
 border: 1px solid #888;
 font-size: 70%;
 background-color: #F8F8F8;
 color: black;
 box-shadow: 2px 2px 5px #AAAAAA;
}

g[data-mml-node="maction"][data-toggle] {
 cursor: pLine truncated
|
||||
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 38 KiB |