Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ffc6a14d2
|
||
|
|
19f6007113
|
||
|
|
6472890fa2
|
||
|
|
fc1b45058c
|
||
|
|
3601def1f6
|
||
|
|
1292597e09
|
||
|
|
d607e5df4d | ||
|
|
4199eb0e42 | ||
|
|
0f4556a8a0 | ||
|
|
dc62de5b6b | ||
|
|
48296ab45c | ||
|
|
de2deb15ad | ||
|
|
c3d3479327 | ||
|
|
1bf4eeb242
|
||
|
|
6139748849 | ||
|
|
5637b4b14a
|
||
|
|
79d0f08273
|
||
|
|
c7aabc7560 | ||
|
|
c16086c8dc
|
||
|
|
525d5d466c | ||
|
|
0149f59c82 | ||
|
|
35ee5b72df | ||
|
|
47b6eb808e | ||
|
|
bdaa3e1ce4 | ||
|
|
ac64daca7f | ||
|
|
5a1314a97a | ||
|
|
7d45d9d8e4 | ||
|
|
991f49776d | ||
|
|
19acbe1a74 | ||
|
|
76ae3150a0 | ||
|
|
9c298cba2c | ||
|
|
f0784cfef6 | ||
|
|
60e881a27f | ||
|
|
06afb2e4d1 | ||
|
|
0c421a159c | ||
|
|
228da72eb1 | ||
|
|
0f8fcb4fe6 | ||
|
|
dbb7c8f33b | ||
|
|
c7d7b6ae8c | ||
|
|
343f7c7e3a | ||
|
|
ae4d5b8f33 | ||
|
|
fbef6eceee | ||
|
|
9a919151cc | ||
|
|
f52207eed3 | ||
|
|
7a6970f89e | ||
|
|
5228190c95 | ||
|
|
2035387730 | ||
|
|
04f2ca9aba | ||
|
|
b4ac04880a | ||
|
|
766013fd3c | ||
|
|
a9039fccc5 | ||
|
|
91e02ba46a | ||
|
|
1c40e87061 | ||
|
|
d3e4ab7876 | ||
|
|
a8a085b5aa | ||
|
|
9bcc548c9e | ||
|
|
c6cfe50a94 | ||
|
|
d0c08a35a2 | ||
|
|
f716270ab6 | ||
|
|
587fe17f36 | ||
|
|
f08600e8bc | ||
|
|
938af714d1 | ||
|
|
15639751af | ||
|
|
b2b6cf5c14 | ||
|
|
779a20386f | ||
|
|
959f3397f6 | ||
|
|
e5a00441e4 | ||
|
|
6f558c0dab | ||
|
|
bee1139aff | ||
|
|
c7a1f438d2 | ||
|
|
aa399dcc93 | ||
|
|
4d5693aa1d | ||
|
|
1e5a337f94 | ||
|
|
841c7082c9 | ||
|
|
9dec00c656 | ||
|
|
ecc766fb53 | ||
|
|
7cb0973700 | ||
|
|
ec57b65794 | ||
|
|
e10ab79b06 | ||
|
|
f55f01c352 | ||
|
|
f436b9de25 | ||
|
|
e03787ac7b | ||
|
|
85cf4d28da | ||
|
|
f0d9cff028 | ||
|
|
1092fb86cc | ||
|
|
9d6794a04e | ||
|
|
9ce3d6939c | ||
|
|
d9c1dfa147 | ||
|
|
60e2650707 | ||
|
|
415f7786ba | ||
|
|
80373fdd08 | ||
|
|
d7242688df | ||
|
|
285c888b8d | ||
|
|
33c1ffb7d2 | ||
|
|
95a5a19943 | ||
|
|
2bffbf4a60 | ||
|
|
1bb25ec1c1 | ||
|
|
fb6958da1e | ||
|
|
559e8bcc4d | ||
|
|
a4058a2882 | ||
|
|
813f64d922 | ||
|
|
3aea1a47b9 | ||
|
|
3d08888842 | ||
|
|
9a811a2d3d | ||
|
|
ec4a16209b | ||
|
|
54c40613e7 | ||
|
|
5e713e5151 | ||
|
|
12854429d1 | ||
|
|
8a7ce06fe6 | ||
|
|
295a0844be | ||
|
|
5da923e68b | ||
|
|
649c2c2f58 | ||
|
|
3446d70a59 | ||
|
|
a02898f301 | ||
|
|
c49ee22234 | ||
|
|
49c891377b | ||
|
|
d4dbc07c20 | ||
|
|
00b5dfea65 | ||
|
|
20b294a769 | ||
|
|
504472e7cc | ||
|
|
0d16b6fcaf | ||
|
|
9b0110b8d1 | ||
|
|
eaf4ed24fd | ||
|
|
b32065ca9c | ||
|
|
5feafd8877 | ||
|
|
a0a1aab660 | ||
|
|
c67bf20145 | ||
|
|
01d9d71ff5 | ||
|
|
8c4bdd9e41 | ||
|
|
f13f4faa02 |
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
.delta
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
docs
|
||||||
|
legacy_docs
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 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/
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#set document(title: [Not found])
|
|
||||||
#metadata((pdf: false)) <website-metadata>
|
|
||||||
|
|
||||||
#title()
|
|
||||||
|
|
||||||
The requested page does not exist.
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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"]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"/homelab/backup.html": [
|
||||||
|
"family"
|
||||||
|
],
|
||||||
|
"/security/ssh.html": [
|
||||||
|
"admin"
|
||||||
|
],
|
||||||
|
"/thermo/fluids.html": [
|
||||||
|
"family",
|
||||||
|
"admin"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#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.
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#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]
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 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...
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#import "../template.typ": project
|
||||||
|
#show: project
|
||||||
|
|
||||||
|
= Self-hosting
|
||||||
|
|
||||||
|
Public page placeholder about self-hosting: Docker/Podman, reverse
|
||||||
|
proxies, DNS, VLANs...
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#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.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#import "../template.typ": project
|
||||||
|
#show: project
|
||||||
|
|
||||||
|
= Opsec basics
|
||||||
|
|
||||||
|
Public page placeholder about operational security: threat models,
|
||||||
|
password managers, 2FA...
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 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...
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// 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...
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#import "../template.typ": project
|
||||||
|
#show: project
|
||||||
|
|
||||||
|
= Heat pumps
|
||||||
|
|
||||||
|
Public page placeholder about heat pumps: COP, seasonal performance,
|
||||||
|
refrigerant cycles...
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
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=
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#set document(title: [Cooking])
|
|
||||||
#metadata((title: "Cooking", translation_key: "home-cooking")) <website-metadata>
|
|
||||||
|
|
||||||
#title()
|
|
||||||
|
|
||||||
Placeholder page — the real content comes later.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#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>
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#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.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#set document(title: [Backup])
|
|
||||||
#metadata((title: "Backup", translation_key: "homelab-backup")) <website-metadata>
|
|
||||||
|
|
||||||
#title()
|
|
||||||
|
|
||||||
Placeholder page — the real content comes later.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#set document(title: [HomeLab])
|
|
||||||
#metadata((title: "Overview", translation_key: "homelab")) <website-metadata>
|
|
||||||
|
|
||||||
#title()
|
|
||||||
|
|
||||||
Placeholder page for the homelab category.
|
|
||||||
The real content comes later.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#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
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
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[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<header><h1>
|
||||||
|
Embedded Systems
|
||||||
|
</h1></header>
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<header><h1>
|
||||||
|
Nordic - nRF
|
||||||
|
</h1></header>
|
||||||
|
|
||||||
|
----------
|
||||||
|
|
||||||
|
[Install development environment](/Embed/nRF/development_environment.md)
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<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 -->
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1,95 @@
|
|||||||
|
<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"
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<header><h1>
|
||||||
|
Working environment at HES-SO
|
||||||
|
</h1></header>
|
||||||
|
|
||||||
|
- [OneDrive - Linux](HES-SO/OneDrive.md)
|
||||||
|
- [Teams](HES-SO/Teams.md)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<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}"
|
||||||
|
```
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
# Master of science in engineering in embedded system
|
||||||
|
|
||||||
|
## [SRE - Software Reverse Engineering](/MSE/SRE/)
|
||||||
|
- [Tools](/MSE/SRE/tools.md)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<header><h1>
|
||||||
|
SRE - Software Reverse Engineering
|
||||||
|
</h1></header>
|
||||||
|
|
||||||
|
- [Tools](/MSE/SRE/tools.md)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<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
|
||||||
|
|
||||||
|
```
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* [SRE - Software Reverse Engineering](/MSE/SRE/)
|
||||||
|
* [Tools](/MSE/SRE/tools.md)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<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)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<header><h1>
|
||||||
|
Security
|
||||||
|
</h1></header>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<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
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
* [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)
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
#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>
|
||||||
|
```
|
||||||
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,605 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
# 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>
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<!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>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
----------
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----------
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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)
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<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} $$
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
<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}}$$
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----------
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<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".
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<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.
|
||||||
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 78 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?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
|
||||||
|
After Width: | Height: | Size: 82 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?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
|
||||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 49 KiB |