mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-10-24 14:39:14 +00:00
Publish the Obsidian vault's Knowledge folder as /knowledge (881 notes, 67 folders), with a Knowledge item in the nav and footer. - scripts/import_knowledge.py regenerates data/knowledge from the vault: slugged paths, folder notes as index pages, wikilinks resolved to site links (or plain text), callouts, highlights, and Dataview tables rendered statically. Personal notes are withheld via an exclusion list plus a privacy guard. - tuftecms/core/knowledge.py builds a concept index from note titles and aliases. First mentions on ordinary pages link to their Knowledge note (capped, never in headings, code, links, or sidenotes); Knowledge pages list related notes and where they're mentioned across the site; other pages get a Related Knowledge block. - Knowledge URLs join sitemap.xml (so the sweep test covers them), never displace existing legacy slugs, list alphabetically, and mermaid diagrams render client-side only on pages that have them.
629 lines
33 KiB
HTML
629 lines
33 KiB
HTML
<!doctype html>
|
|
<html lang="en" class="loaded">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="color-scheme" content="light dark" />
|
|
<title>{% block title %}{{ title }} - Kenneth Reitz{% endblock %}</title>
|
|
|
|
<!-- SEO and Meta -->
|
|
<meta name="description" content="{% block description %}Kenneth Reitz - Creator of Requests and Certifi, trusted by millions of developers worldwide. Thoughts on technology, philosophy, and building software for humans.{% endblock %}" />
|
|
<meta name="author" content="Kenneth Reitz" />
|
|
<link rel="canonical" href="{% block canonical_url %}https://kennethreitz.org{{ request.path if request.path != '/' else '' }}{% endblock %}" />
|
|
|
|
<!-- Mobile & App Meta -->
|
|
<meta name="theme-color" content="#fffff8" media="(prefers-color-scheme: light)" />
|
|
<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)" />
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png" />
|
|
<link rel="icon" href="data:," />
|
|
|
|
<!-- OpenGraph -->
|
|
<meta property="og:title" content="{% block og_title %}{{ title }} - Kenneth Reitz{% endblock %}" />
|
|
<meta property="og:description" content="{% block og_description %}Creator of Requests and Certifi - libraries trusted by millions of developers worldwide{% endblock %}" />
|
|
<meta property="og:type" content="{% block og_type %}website{% endblock %}" />
|
|
<meta property="og:url" content="{% block og_url %}https://kennethreitz.org{% endblock %}" />
|
|
<meta property="og:image" content="{% block og_image %}https://kennethreitz.org/static/images/social-card.jpg{% endblock %}" />
|
|
<meta property="og:image:width" content="1200" />
|
|
<meta property="og:image:height" content="630" />
|
|
<meta property="og:image:alt" content="{% block og_image_alt %}Kenneth Reitz - Software developer and open source creator{% endblock %}" />
|
|
<meta property="og:site_name" content="Kenneth Reitz" />
|
|
<meta property="og:locale" content="en_US" />
|
|
{% block extra_meta %}{% endblock %}
|
|
|
|
<!-- Identity verification (IndieWeb / Mastodon) -->
|
|
<link rel="me" href="https://github.com/kennethreitz" />
|
|
<link rel="me" href="https://twitter.com/kennethreitz42" />
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="{% block twitter_card %}summary_large_image{% endblock %}" />
|
|
<meta name="twitter:creator" content="@kennethreitz42" />
|
|
<meta name="twitter:title" content="{% block twitter_title %}{{ title }} - Kenneth Reitz{% endblock %}" />
|
|
<meta name="twitter:description" content="{% block twitter_description %}Creator of Requests and Certifi - libraries trusted by millions of developers worldwide{% endblock %}" />
|
|
<meta name="twitter:image" content="{% block twitter_image %}https://kennethreitz.org/static/images/social-card.jpg{% endblock %}" />
|
|
|
|
<!-- RSS Feed -->
|
|
<link rel="alternate" type="application/rss+xml" title="Kenneth Reitz - Essays & AI Writings" href="/feed.xml" />
|
|
|
|
<!-- Preload critical resources -->
|
|
<link rel="preload" href="/static/tufte/tufte.css" as="style" />
|
|
<link rel="preload" href="/static/custom.css?v={{ static_v }}" as="style" />
|
|
<link rel="preload" href="/static/tufte/et-book/et-book-roman-line-figures/et-book-roman-line-figures.woff" as="font" type="font/woff" crossorigin />
|
|
<link rel="preload" href="/static/tufte/et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.woff" as="font" type="font/woff" crossorigin />
|
|
|
|
<!-- Tufte CSS -->
|
|
<link rel="stylesheet" href="/static/tufte/tufte.css" />
|
|
|
|
<!-- Custom Site CSS -->
|
|
<link rel="stylesheet" href="/static/custom.css?v={{ static_v }}" />
|
|
|
|
<!-- Site CSS (layout, nav, dark mode, etc.) -->
|
|
<link rel="stylesheet" href="/static/site.css?v={{ static_v }}" />
|
|
|
|
|
|
<!-- Structured Data (JSON-LD) -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "{% block schema_type %}WebSite{% endblock %}",
|
|
"name": "{% block schema_name %}Kenneth Reitz{% endblock %}",
|
|
"url": "https://kennethreitz.org{% block schema_url %}/{% endblock %}",
|
|
"description": "{% block schema_description %}Kenneth Reitz - Creator of Requests and Certifi, trusted by millions of developers worldwide. Thoughts on technology, philosophy, and building software for humans.{% endblock %}",
|
|
"author": {
|
|
"@type": "Person",
|
|
"name": "Kenneth Reitz",
|
|
"url": "https://kennethreitz.org",
|
|
"sameAs": [
|
|
"https://github.com/kennethreitz",
|
|
"https://twitter.com/kennethreitz42"
|
|
],
|
|
"jobTitle": "Python Developer",
|
|
"worksFor": {
|
|
"@type": "Organization",
|
|
"name": "Independent"
|
|
},
|
|
"knowsAbout": [
|
|
"Python Programming",
|
|
"API Design",
|
|
"Software Architecture",
|
|
"Open Source Development",
|
|
"Artificial Intelligence",
|
|
"Mental Health Advocacy"
|
|
]
|
|
},
|
|
"publisher": {
|
|
"@type": "Person",
|
|
"name": "Kenneth Reitz",
|
|
"url": "https://kennethreitz.org"
|
|
}{% block schema_extra %}{% endblock %}
|
|
}
|
|
</script>
|
|
|
|
{% block extra_head %}{% endblock %}
|
|
|
|
<!-- Breadcrumb Structured Data -->
|
|
{% if current_path and current_path != '/' %}
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "BreadcrumbList",
|
|
"itemListElement": [
|
|
{
|
|
"@type": "ListItem",
|
|
"position": 1,
|
|
"name": "Home",
|
|
"item": "https://kennethreitz.org/"
|
|
}{% if breadcrumbs %}{% for crumb in breadcrumbs %},
|
|
{
|
|
"@type": "ListItem",
|
|
"position": {{ loop.index + 1 }},
|
|
"name": "{{ crumb.name }}",
|
|
"item": "https://kennethreitz.org{{ crumb.url }}"
|
|
}{% endfor %}{% elif current_path %}{% set path_parts = current_path.strip('/').split('/') %}{% for part in path_parts if part %},
|
|
{
|
|
"@type": "ListItem",
|
|
"position": {{ loop.index + 1 }},
|
|
"name": "{{ part | replace('-', ' ') | replace('_', ' ') | title }}",
|
|
"item": "https://kennethreitz.org/{{ path_parts[:loop.index]|join('/') }}"
|
|
}{% endfor %}{% endif %}
|
|
]
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
|
|
<!-- Analytics -->
|
|
{% if not (config.get('DISABLE_ANALYTICS') or request.environ.get('DISABLE_ANALYTICS')) %}
|
|
<!-- Umami (self-hosted) -->
|
|
<script defer src="https://analytics.kennethreitz.org/script.js" data-website-id="41b1fd15-82b1-4211-aeff-b3caa2d50ea3"></script>
|
|
{% endif %}
|
|
|
|
<!-- Minimal syntax styling: comments only, keeps code literary -->
|
|
</head>
|
|
<body>
|
|
<!-- Apply theme + color scheme before first paint to avoid a flash
|
|
of the wrong mode. Runs here, not at the end of the body. -->
|
|
<script>
|
|
(function() {
|
|
const body = document.body;
|
|
|
|
const savedTheme = localStorage.getItem('theme');
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
if (savedTheme) {
|
|
body.classList.add(savedTheme);
|
|
} else if (prefersDark) {
|
|
body.classList.add('dark-mode');
|
|
} else {
|
|
body.classList.add('light-mode');
|
|
}
|
|
|
|
const lightColorSchemes = [
|
|
'scheme-ocean',
|
|
'scheme-forest',
|
|
'scheme-sunset',
|
|
'scheme-lavender',
|
|
'scheme-rose',
|
|
'scheme-sage',
|
|
'scheme-amber'
|
|
];
|
|
|
|
// Change scheme occasionally (20% chance on page load)
|
|
let scheme = localStorage.getItem('current-color-scheme');
|
|
if (!scheme || Math.random() < 0.2) {
|
|
scheme = lightColorSchemes[Math.floor(Math.random() * lightColorSchemes.length)];
|
|
localStorage.setItem('current-color-scheme', scheme);
|
|
}
|
|
body.classList.add(scheme);
|
|
})();
|
|
</script>
|
|
|
|
<a class="skip-link" href="#main-content">Skip to content</a>
|
|
|
|
<!-- Reading progress bar -->
|
|
<div class="reading-progress" id="reading-progress"></div>
|
|
|
|
{% set github_base = 'https://github.com/kennethreitz/kennethreitz.org' %}
|
|
{% if github_file %}
|
|
<a href="{{ github_base }}/blob/main/{{ github_file }}" class="github-corner" aria-label="View source on GitHub" target="_blank">
|
|
{% else %}
|
|
<a href="{{ github_base }}" class="github-corner" aria-label="View source on GitHub" target="_blank">
|
|
{% endif %}
|
|
<img src="/static/images/github-banner.svg" alt="View on GitHub" class="github-corner-img">
|
|
</a>
|
|
|
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">
|
|
<svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
|
<svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
</button>
|
|
|
|
<article>
|
|
<header>
|
|
<nav>
|
|
<a href="/">Home</a>
|
|
<a href="/archive">Archive</a>
|
|
<a href="/search">Search</a>
|
|
<div class="nav-dropdown">
|
|
<span class="nav-dropdown-trigger" tabindex="0" role="button" aria-haspopup="true">Themes</span>
|
|
<div class="nav-dropdown-content" id="themes-dropdown">
|
|
{% for t in nav_themes %}
|
|
<div class="tree-item tree-file">{% if t.icon %}<img src="{{ t.icon }}" class="tree-item-icon" alt="">{% endif %}<a href="{{ t.path }}">{{ t.name }}</a></div>
|
|
{% endfor %}
|
|
<div class="tree-item tree-folder" style="border-top: 1px solid rgba(128,128,128,0.2); margin-top: 0.25rem; padding-top: 0.25rem;"><a href="/themes">All Themes →</a></div>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<a href="/knowledge" class="nav-dropdown-trigger" aria-haspopup="true">Knowledge</a>
|
|
<div class="nav-dropdown-content" id="knowledge-dropdown">
|
|
{% for k in nav_knowledge %}
|
|
<div class="tree-item tree-folder">{% if k.icon %}<img src="{{ k.icon }}" class="tree-item-icon" alt="">{% endif %}<a href="{{ k.path }}">{{ k.name }}</a></div>
|
|
{% endfor %}
|
|
<div class="tree-item tree-folder" style="border-top: 1px solid rgba(128,128,128,0.2); margin-top: 0.25rem; padding-top: 0.25rem;"><a href="/knowledge">All Knowledge →</a></div>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<span class="nav-dropdown-trigger" style="font-style: italic;" tabindex="0" role="button" aria-haspopup="true">etc.</span>
|
|
<div class="nav-dropdown-content" id="browse-dropdown">
|
|
{% for item in nav_browse %}
|
|
<div class="tree-item {{ 'tree-folder' if item.is_dir else 'tree-file' }}">{% if item.icon %}<img src="{{ item.icon }}" class="tree-item-icon" alt="">{% endif %}<a href="{{ item.path }}">{{ item.name }}</a></div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{% if current_path and current_path != '/' %}
|
|
{% set path_parts = current_path.strip('/').split('/') %}
|
|
{% if path_parts|length > 1 %}
|
|
<div class="breadcrumbs">
|
|
<a href="/">~</a>
|
|
{% for part in path_parts[:-1] %}
|
|
<span class="breadcrumb-separator">/</span>
|
|
{% set partial_path = '/' + path_parts[:loop.index]|join('/') %}
|
|
<a href="{{ partial_path }}">{{ part|replace('-', ' ')|replace('_', ' ')|title }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</header>
|
|
|
|
<main id="main-content">
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="footer-content">
|
|
<nav class="footer-map">
|
|
<a href="/essays">Essays</a><span class="sep">·</span><a href="/themes">Themes</a><span class="sep">·</span><a href="/knowledge">Knowledge</a><span class="sep">·</span><a href="/software">Software</a><span class="sep">·</span><a href="/photography">Photography</a><span class="sep">·</span><a href="/music">Music</a><span class="sep">·</span><a href="/contact">Contact</a><span class="sep">·</span><a href="/feed.xml">RSS</a>
|
|
</nav>
|
|
<div class="footer-note">
|
|
<p>© {{ "now"|strftime("%Y") }} Kenneth Reitz. <a href="https://kjvstudy.org/topics/Love">Made with love</a>.</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</article>
|
|
|
|
{% block extra_scripts %}{% endblock %}
|
|
|
|
<!-- Progressive enhancements: link previews, heading anchors, theme-color sync -->
|
|
<script defer src="/static/js/enhance.js?v={{ static_v }}"></script>
|
|
<script>
|
|
// Mermaid diagrams (used by Knowledge notes): load the renderer only when a page has one.
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var blocks = document.querySelectorAll('pre > code.language-mermaid');
|
|
if (!blocks.length) return;
|
|
blocks.forEach(function (code) {
|
|
var div = document.createElement('div');
|
|
div.className = 'mermaid';
|
|
div.textContent = code.textContent;
|
|
code.parentNode.replaceWith(div);
|
|
});
|
|
import('https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs').then(function (m) {
|
|
var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
m.default.initialize({ startOnLoad: false, theme: dark ? 'dark' : 'neutral', securityLevel: 'strict' });
|
|
m.default.run({ querySelector: '.mermaid' });
|
|
}).catch(function () {});
|
|
});
|
|
</script>
|
|
|
|
<!-- Theme Toggle (theme itself is applied at the top of the body) -->
|
|
<script>
|
|
(function() {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const body = document.body;
|
|
|
|
themeToggle.addEventListener('click', function() {
|
|
if (body.classList.contains('light-mode')) {
|
|
body.classList.remove('light-mode');
|
|
body.classList.add('dark-mode');
|
|
localStorage.setItem('theme', 'dark-mode');
|
|
} else {
|
|
body.classList.remove('dark-mode');
|
|
body.classList.add('light-mode');
|
|
localStorage.setItem('theme', 'light-mode');
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
// Main DOMContentLoaded handler
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// First priority: Add loaded class to reveal content
|
|
document.documentElement.classList.add('loaded');
|
|
|
|
// Find all pre elements (code blocks)
|
|
const codeBlocks = document.querySelectorAll('pre');
|
|
|
|
// Function to add subtle comment highlighting
|
|
function highlightComments(pre) {
|
|
const code = pre.querySelector('code') || pre;
|
|
let html = code.innerHTML;
|
|
|
|
// Python/Shell comments (# comment) - both line start and inline
|
|
html = html.replace(/(^|\s+)(#.*)$/gm, '$1<span style="color: #888; font-style: italic;">$2</span>');
|
|
|
|
// Python docstrings (""" or ''') — italic but readable, not grayed out
|
|
html = html.replace(/("""[\s\S]*?""")/g, '<span style="font-style: italic;">$1</span>');
|
|
html = html.replace(/('''[\s\S]*?''')/g, '<span style="font-style: italic;">$1</span>');
|
|
|
|
// Python class names (class ClassName)
|
|
html = html.replace(/(\bclass\s+)([A-Za-z_][A-Za-z0-9_]*)/g, '$1<span style="font-weight: bold;">$2</span>');
|
|
|
|
// Variable assignment targets (name = ...)
|
|
html = html.replace(/^(\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s)/gm, '$1<span style="font-weight: bold;">$2</span>$3');
|
|
|
|
// Shell prompt ($ command) - bold the $
|
|
html = html.replace(/^(\$ )/gm, '<span style="font-weight: bold;">$1</span>');
|
|
|
|
// JavaScript/C++/Java comments (// comment) - only at line start, not in URLs
|
|
html = html.replace(/^(\s*\/\/.*)$/gm, '<span style="color: #888; font-style: italic;">$1</span>');
|
|
|
|
// CSS/C/Java block comments (/* comment */)
|
|
html = html.replace(/(\/\*[\s\S]*?\*\/)/g, '<span style="color: #888; font-style: italic;">$1</span>');
|
|
|
|
// HTML comments (<!-- comment -->)
|
|
html = html.replace(/(<!--[\s\S]*?-->)/g, '<span style="color: #888; font-style: italic;">$1</span>');
|
|
|
|
code.innerHTML = html;
|
|
}
|
|
|
|
codeBlocks.forEach(function(pre) {
|
|
// Add subtle comment highlighting first
|
|
highlightComments(pre);
|
|
|
|
// Skip if already wrapped
|
|
if (pre.parentElement.classList.contains('code-block-wrapper')) {
|
|
return;
|
|
}
|
|
|
|
// Create wrapper div
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'code-block-wrapper';
|
|
|
|
// Create copy button
|
|
const copyButton = document.createElement('button');
|
|
copyButton.className = 'copy-button';
|
|
copyButton.textContent = 'Copy';
|
|
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
|
|
|
|
// Add click handler
|
|
copyButton.addEventListener('click', async function() {
|
|
try {
|
|
// Get the text content of the pre element
|
|
const codeText = pre.textContent || pre.innerText;
|
|
|
|
// Use modern clipboard API if available
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(codeText);
|
|
} else {
|
|
// Fallback for older browsers
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = codeText;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
document.execCommand('copy');
|
|
textArea.remove();
|
|
}
|
|
|
|
// Show feedback
|
|
const originalText = copyButton.textContent;
|
|
copyButton.textContent = 'Copied!';
|
|
copyButton.classList.add('copied');
|
|
|
|
setTimeout(function() {
|
|
copyButton.textContent = originalText;
|
|
copyButton.classList.remove('copied');
|
|
}, 2000);
|
|
|
|
} catch (err) {
|
|
console.error('Failed to copy code: ', err);
|
|
|
|
// Show error feedback
|
|
const originalText = copyButton.textContent;
|
|
copyButton.textContent = 'Failed';
|
|
setTimeout(function() {
|
|
copyButton.textContent = originalText;
|
|
}, 2000);
|
|
}
|
|
});
|
|
|
|
// Wrap the pre element and add the button
|
|
pre.parentNode.insertBefore(wrapper, pre);
|
|
wrapper.appendChild(pre);
|
|
wrapper.appendChild(copyButton);
|
|
});
|
|
|
|
// Initialize reading progress and article icons
|
|
const currentPath = window.location.pathname;
|
|
const isIndexPage = currentPath === '/' ||
|
|
currentPath === '/archive' ||
|
|
currentPath.endsWith('/index') ||
|
|
currentPath.match(/^\/(archive|search|sidenotes|outlines|connections|quotes|terms|graph|random)\/?$/);
|
|
|
|
if (!isIndexPage) {
|
|
// Load article icons
|
|
loadArticleIcons();
|
|
}
|
|
|
|
// Check if this is a content page (essay/article) rather than an index
|
|
const isContentPage = document.querySelector('main').textContent.length > 2000;
|
|
|
|
if (isContentPage) {
|
|
window.addEventListener('scroll', updateReadingProgress);
|
|
window.addEventListener('resize', updateReadingProgress);
|
|
updateReadingProgress(); // Initial call
|
|
}
|
|
|
|
// Lazy loading for images
|
|
if ('IntersectionObserver' in window) {
|
|
const imageObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const img = entry.target;
|
|
if (img.dataset.src) {
|
|
img.src = img.dataset.src;
|
|
img.removeAttribute('data-src');
|
|
imageObserver.unobserve(img);
|
|
}
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '50px 0px'
|
|
});
|
|
|
|
// Apply lazy loading to all images with data-src
|
|
document.querySelectorAll('img[data-src]').forEach(img => {
|
|
imageObserver.observe(img);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Reading progress indicator
|
|
function updateReadingProgress() {
|
|
const progressBar = document.getElementById('reading-progress');
|
|
if (!progressBar) return;
|
|
|
|
const mainContent = document.querySelector('main');
|
|
if (!mainContent) return;
|
|
|
|
const scrollTop = window.scrollY;
|
|
const docHeight = document.documentElement.scrollHeight;
|
|
const winHeight = window.innerHeight;
|
|
const scrollPercent = scrollTop / (docHeight - winHeight);
|
|
const scrollPercentRounded = Math.round(scrollPercent * 100);
|
|
|
|
progressBar.style.width = scrollPercentRounded + '%';
|
|
|
|
// Only show progress bar if there's meaningful content to scroll
|
|
if (docHeight > winHeight * 1.5) {
|
|
progressBar.style.opacity = '1';
|
|
} else {
|
|
progressBar.style.opacity = '0';
|
|
}
|
|
}
|
|
|
|
// Load icons for article links at the beginning of paragraphs
|
|
function loadArticleIcons() {
|
|
// Find all paragraphs that start with a link
|
|
const paragraphs = document.querySelectorAll('p');
|
|
|
|
paragraphs.forEach(paragraph => {
|
|
// Skip paragraphs that are sidenotes or contain sidenote elements
|
|
if (paragraph.classList.contains('sidenote-entry') ||
|
|
paragraph.querySelector('.sidenote') ||
|
|
paragraph.querySelector('.marginnote') ||
|
|
paragraph.querySelector('.margin-toggle')) {
|
|
return;
|
|
}
|
|
|
|
// Skip paragraphs inside directory listings or item lists
|
|
if (paragraph.closest('.items-list') || paragraph.closest('.directory-listing') || paragraph.closest('.item')) {
|
|
return;
|
|
}
|
|
|
|
// Check if the paragraph starts with a link (no text before it)
|
|
const firstNode = paragraph.firstChild;
|
|
let firstElement = null;
|
|
|
|
// Skip any whitespace/text nodes at the beginning
|
|
for (let node = firstNode; node; node = node.nextSibling) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
firstElement = node;
|
|
break;
|
|
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
|
|
// If there's non-whitespace text before any element, don't add icon
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check if first element is a link, or if it's a strong/b tag containing a link
|
|
let linkElement = null;
|
|
if (firstElement && firstElement.tagName === 'A') {
|
|
linkElement = firstElement;
|
|
} else if (firstElement && (firstElement.tagName === 'STRONG' || firstElement.tagName === 'B')) {
|
|
// Check if the strong/b tag contains a link as its first child
|
|
const strongFirstChild = firstElement.firstElementChild;
|
|
if (strongFirstChild && strongFirstChild.tagName === 'A') {
|
|
linkElement = strongFirstChild;
|
|
}
|
|
}
|
|
|
|
if (linkElement) {
|
|
const href = linkElement.getAttribute('href');
|
|
|
|
// Check if it's an internal link
|
|
if (href && href.startsWith('/') && !href.startsWith('//')) {
|
|
// Skip if icon already exists
|
|
if (paragraph.querySelector('.article-link-icon')) return;
|
|
|
|
// Fetch icon with simple error handling
|
|
fetch(`/api/icon${href}`)
|
|
.then(response => response.ok ? response.json() : null)
|
|
.then(data => {
|
|
if (data && data.success && data.icon) {
|
|
// Validate the data URL format
|
|
if (!data.icon.startsWith('data:image/svg+xml;base64,')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Decode the SVG to insert as inline SVG instead of data URL
|
|
const base64Part = data.icon.split(',')[1];
|
|
const svgContent = atob(base64Part);
|
|
|
|
// Create a container div for the SVG
|
|
const iconContainer = document.createElement('div');
|
|
iconContainer.className = 'article-link-icon';
|
|
iconContainer.style.cssText = `
|
|
display: inline-block;
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 0.75rem;
|
|
margin-left: -2.25rem;
|
|
margin-top: -0.25em;
|
|
vertical-align: middle;
|
|
flex-shrink: 0;
|
|
`;
|
|
|
|
// Add CSS to hide on mobile and tablet - only add once
|
|
if (!window.articleIconStylesAdded) {
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@media (max-width: 760px) {
|
|
.article-link-icon,
|
|
.article-link-icon.fallback {
|
|
display: none !important;
|
|
visibility: hidden !important;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
window.articleIconStylesAdded = true;
|
|
}
|
|
|
|
// Insert the SVG content directly
|
|
iconContainer.innerHTML = svgContent;
|
|
|
|
// Insert icon before the first element (which contains the link)
|
|
paragraph.insertBefore(iconContainer, firstElement);
|
|
|
|
// Add some styling to the paragraph
|
|
paragraph.style.position = 'relative';
|
|
} catch (e) {
|
|
// If decoding fails, skip silently
|
|
}
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<!-- Client-side oEmbed -->
|
|
<script>
|
|
(function() {
|
|
document.querySelectorAll('.oembed-placeholder').forEach(function(el) {
|
|
var url = el.getAttribute('data-oembed-url');
|
|
if (!url) return;
|
|
fetch('/api/oembed?url=' + encodeURIComponent(url))
|
|
.then(function(r) { return r.ok ? r.json() : Promise.reject(); })
|
|
.then(function(data) {
|
|
if (data.html) {
|
|
el.innerHTML = data.html;
|
|
}
|
|
})
|
|
.catch(function() {
|
|
// Fallback link is already in place — do nothing.
|
|
});
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|