mirror of
https://github.com/kennethreitz/kennethreitz.org.git
synced 2026-06-21 06:50:56 +00:00
a7b18f0d84
- Browse nav item renamed to etc. with italic styling - Directory template now uses the actual title variable instead of raw path segment, fixing missing titles on gallery pages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1309 lines
56 KiB
HTML
1309 lines
56 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" />
|
|
|
|
<!-- 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" as="style" />
|
|
|
|
<!-- Tufte CSS -->
|
|
<link rel="stylesheet" href="/static/tufte/tufte.css" />
|
|
|
|
<!-- Custom Site CSS -->
|
|
<link rel="stylesheet" href="/static/custom.css" />
|
|
|
|
<style>
|
|
/* Prevent FOUC by hiding content until stylesheets load */
|
|
html {
|
|
visibility: hidden;
|
|
opacity: 0;
|
|
}
|
|
|
|
html.loaded {
|
|
visibility: visible;
|
|
opacity: 1;
|
|
transition: opacity 0.1s ease-in;
|
|
}
|
|
|
|
/* Only page-specific or dynamic styles that can't be in external CSS */
|
|
|
|
/* Legend dots for content guide */
|
|
.legend-dot {
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
margin-right: 0.3rem;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* Reading progress indicator */
|
|
.reading-progress {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 0%;
|
|
height: 2px;
|
|
background: #333;
|
|
z-index: 1000;
|
|
transition: width 0.1s ease-out;
|
|
}
|
|
|
|
/* Header and nav z-index for dropdown stacking */
|
|
header {
|
|
position: relative;
|
|
z-index: 9999;
|
|
}
|
|
|
|
nav {
|
|
position: relative;
|
|
z-index: 9999;
|
|
}
|
|
|
|
nav > a, nav > .nav-dropdown {
|
|
margin-right: 1rem;
|
|
}
|
|
|
|
.nav-dropdown {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
.nav-dropdown-trigger {
|
|
cursor: pointer;
|
|
color: inherit;
|
|
text-decoration: underline;
|
|
padding-bottom: 1rem;
|
|
display: inline-block;
|
|
}
|
|
|
|
.nav-dropdown-content {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
min-width: 250px;
|
|
max-width: 400px;
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
z-index: 10000;
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.nav-dropdown:hover .nav-dropdown-content,
|
|
.nav-dropdown-content:hover {
|
|
display: block;
|
|
}
|
|
|
|
.tree-item {
|
|
padding: 0.35rem 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.tree-item-icon {
|
|
flex-shrink: 0;
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.tree-item a {
|
|
color: #333;
|
|
text-decoration: none;
|
|
flex: 1;
|
|
}
|
|
|
|
.tree-item a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.tree-folder a {
|
|
font-weight: 500;
|
|
}
|
|
|
|
body.dark-mode .nav-dropdown-content {
|
|
background: #1a1a1a;
|
|
border-color: #333;
|
|
}
|
|
|
|
body.dark-mode .tree-item a {
|
|
color: #ccc;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.nav-dropdown-content {
|
|
background: #1a1a1a;
|
|
border-color: #333;
|
|
}
|
|
|
|
.tree-item a {
|
|
color: #ccc;
|
|
}
|
|
}
|
|
|
|
.theme-toggle {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 1.5rem;
|
|
padding: 0.5rem;
|
|
margin: 0;
|
|
opacity: 0.15;
|
|
transition: opacity 0.3s ease;
|
|
line-height: 1;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.theme-toggle:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.sun-icon,
|
|
.moon-icon {
|
|
display: none;
|
|
}
|
|
|
|
body.light-mode .sun-icon {
|
|
display: inline;
|
|
}
|
|
|
|
body.dark-mode .moon-icon {
|
|
display: inline;
|
|
}
|
|
|
|
/* Dark mode styles using body class */
|
|
body.dark-mode {
|
|
background-color: #0d1117;
|
|
color: #c9d1d9;
|
|
}
|
|
|
|
/* Light mode styles to override media query */
|
|
body.light-mode {
|
|
background-color: #fffff8;
|
|
color: #111;
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.theme-toggle {
|
|
font-size: 1.3rem;
|
|
bottom: 1rem;
|
|
right: 1rem;
|
|
padding: 0.4rem;
|
|
}
|
|
}
|
|
|
|
main {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
article {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Dropdown navigation styles */
|
|
.nav-dropdown {
|
|
position: relative;
|
|
display: inline-block;
|
|
padding: 0.5rem;
|
|
margin: -0.5rem;
|
|
z-index: 9999;
|
|
}
|
|
|
|
|
|
.nav-dropdown .dropdown-content {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background-color: #fff;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
z-index: 999999;
|
|
min-width: 280px;
|
|
padding: 0.5rem 0;
|
|
margin-top: -1px;
|
|
}
|
|
|
|
.nav-dropdown:hover .dropdown-content {
|
|
display: block;
|
|
}
|
|
|
|
.nav-dropdown .dropdown-content a {
|
|
display: block;
|
|
padding: 0.5rem 1rem;
|
|
color: #333;
|
|
text-decoration: none;
|
|
border-bottom: none;
|
|
font-size: 0.9rem;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.nav-dropdown .dropdown-content a:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.nav-dropdown .dropdown-content a .index-description {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
color: #666;
|
|
margin-top: 0.1rem;
|
|
}
|
|
|
|
.nav-dropdown > a::after {
|
|
content: "\00a0▼";
|
|
font-size: 0.7rem;
|
|
color: #999;
|
|
}
|
|
</style>
|
|
|
|
|
|
<!-- 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')) %}
|
|
<!-- Google tag (gtag.js) -->
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-RB9QHYEG2X"></script>
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
|
|
gtag('config', 'G-RB9QHYEG2X');
|
|
</script>
|
|
|
|
<script type="text/javascript">
|
|
var _gauges = _gauges || [];
|
|
(function() {
|
|
var t = document.createElement('script');
|
|
t.type = 'text/javascript';
|
|
t.async = true;
|
|
t.id = 'gauges-tracker';
|
|
t.setAttribute('data-site-id', '65529a9abd1a3b3101979d52');
|
|
t.setAttribute('data-track-path', 'https://track.gaug.es/track.gif');
|
|
t.src = 'https://d2fuc4clr7gvcn.cloudfront.net/track.js';
|
|
var s = document.getElementsByTagName('script')[0];
|
|
s.parentNode.insertBefore(t, s);
|
|
})();
|
|
</script>
|
|
{% endif %}
|
|
|
|
<!-- Minimal syntax styling: comments only, keeps code literary -->
|
|
</head>
|
|
<body>
|
|
<!-- Reading progress bar -->
|
|
<div class="reading-progress" id="reading-progress"></div>
|
|
|
|
<button id="theme-toggle" class="theme-toggle" aria-label="Toggle dark mode">
|
|
<span class="sun-icon">☀️</span>
|
|
<span class="moon-icon">🌙</span>
|
|
</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">Themes</span>
|
|
<div class="nav-dropdown-content" id="themes-dropdown">
|
|
<div style="text-align: center; padding: 1rem; color: #999;">Loading...</div>
|
|
</div>
|
|
</div>
|
|
<div class="nav-dropdown">
|
|
<span class="nav-dropdown-trigger" style="font-style: italic;">etc.</span>
|
|
<div class="nav-dropdown-content" id="browse-dropdown">
|
|
<div style="text-align: center; padding: 1rem; color: #999;">Loading...</div>
|
|
</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>
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<footer>
|
|
<div class="footer-content">
|
|
<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 %}
|
|
|
|
<!-- Theme Toggle -->
|
|
<script>
|
|
(function() {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const html = document.documentElement;
|
|
const body = document.body;
|
|
|
|
// Check for saved theme preference or default to system preference
|
|
const savedTheme = localStorage.getItem('theme');
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
|
|
// Set initial theme
|
|
if (savedTheme) {
|
|
body.classList.add(savedTheme);
|
|
} else if (prefersDark) {
|
|
body.classList.add('dark-mode');
|
|
} else {
|
|
body.classList.add('light-mode');
|
|
}
|
|
|
|
// Toggle theme
|
|
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>
|
|
|
|
<!-- Color System -->
|
|
<script>
|
|
(function() {
|
|
|
|
const lightColorSchemes = [
|
|
'scheme-ocean',
|
|
'scheme-forest',
|
|
'scheme-sunset',
|
|
'scheme-lavender',
|
|
'scheme-rose',
|
|
'scheme-sage',
|
|
'scheme-amber'
|
|
];
|
|
|
|
// Get or generate a scheme based on the current page
|
|
let scheme = localStorage.getItem('current-color-scheme');
|
|
|
|
// Change scheme occasionally (20% chance on page load)
|
|
if (!scheme || Math.random() < 0.2) {
|
|
scheme = lightColorSchemes[Math.floor(Math.random() * lightColorSchemes.length)];
|
|
localStorage.setItem('current-color-scheme', scheme);
|
|
}
|
|
|
|
// Apply the scheme
|
|
document.body.className = (document.body.className + ' ' + scheme).trim();
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
// Render items as tree structure
|
|
function renderItems(items) {
|
|
if (!items || items.length === 0) {
|
|
return '<div style="padding: 1rem; color: #999;">No content found</div>';
|
|
}
|
|
|
|
let html = '';
|
|
for (const item of items) {
|
|
const cssClass = item.is_dir ? 'tree-folder' : 'tree-file';
|
|
|
|
html += `<div class="tree-item ${cssClass}">`;
|
|
|
|
// Add icon if present
|
|
if (item.icon) {
|
|
html += `<img src="${item.icon}" class="tree-item-icon" alt="">`;
|
|
}
|
|
|
|
html += `<a href="${item.path}">${item.name}</a>`;
|
|
html += `</div>`;
|
|
}
|
|
return html;
|
|
}
|
|
|
|
// Load and render directory listing
|
|
async function loadDirectoryTree() {
|
|
try {
|
|
const response = await fetch('/api/directory-tree');
|
|
const data = await response.json();
|
|
const dropdown = document.getElementById('browse-dropdown');
|
|
|
|
if (data.items && data.items.length > 0) {
|
|
dropdown.innerHTML = renderItems(data.items);
|
|
} else {
|
|
dropdown.innerHTML = '<div style="padding: 1rem; color: #999;">No content found</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading directory tree:', error);
|
|
document.getElementById('browse-dropdown').innerHTML =
|
|
'<div style="padding: 1rem; color: #999;">Error loading content</div>';
|
|
}
|
|
}
|
|
|
|
|
|
// Load and render themes listing
|
|
async function loadThemes() {
|
|
try {
|
|
const response = await fetch('/api/themes');
|
|
const data = await response.json();
|
|
const dropdown = document.getElementById('themes-dropdown');
|
|
|
|
if (data.themes && data.themes.length > 0) {
|
|
let html = '';
|
|
for (const t of data.themes) {
|
|
html += `<div class="tree-item tree-file">`;
|
|
if (t.icon) {
|
|
html += `<img src="${t.icon}" class="tree-item-icon" alt="">`;
|
|
}
|
|
html += `<a href="${t.path}">${t.name}</a>`;
|
|
html += `</div>`;
|
|
}
|
|
html += `<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;">`;
|
|
html += `<a href="/themes">All Themes →</a>`;
|
|
html += `</div>`;
|
|
dropdown.innerHTML = html;
|
|
} else {
|
|
dropdown.innerHTML = '<div style="padding: 1rem; color: #999;">No themes found</div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading themes:', error);
|
|
document.getElementById('themes-dropdown').innerHTML =
|
|
'<div class="tree-item tree-folder"><a href="/themes">View All Themes</a></div>';
|
|
}
|
|
}
|
|
|
|
// Main DOMContentLoaded handler
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// First priority: Add loaded class to reveal content
|
|
document.documentElement.classList.add('loaded');
|
|
|
|
// Load directory tree for Browse dropdown
|
|
loadDirectoryTree();
|
|
loadThemes();
|
|
|
|
// 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';
|
|
}
|
|
}
|
|
|
|
// Create a simple fallback icon based on text
|
|
function createFallbackIcon(text) {
|
|
// Simple hash function for consistent colors
|
|
let hash = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
hash = ((hash << 5) - hash + text.charCodeAt(i)) & 0xffffffff;
|
|
}
|
|
|
|
// Generate colors based on hash
|
|
const hue = Math.abs(hash) % 360;
|
|
const saturation = 60 + (Math.abs(hash >> 8) % 30); // 60-90%
|
|
const lightness = 45 + (Math.abs(hash >> 16) % 20); // 45-65%
|
|
|
|
// Get first letter of text for simple icon
|
|
const letter = text.charAt(0).toUpperCase();
|
|
|
|
return `<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="10" cy="10" r="8" fill="hsl(${hue}, ${saturation}%, ${lightness}%)" opacity="0.8"/>
|
|
<text x="10" y="14" text-anchor="middle" fill="white" font-family="serif" font-size="11" font-weight="bold">${letter}</text>
|
|
</svg>`;
|
|
}
|
|
|
|
// 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(() => {
|
|
// If API call fails, try to generate a fallback icon based on link text
|
|
const linkText = linkElement.textContent.trim();
|
|
if (linkText) {
|
|
try {
|
|
// Create a simple fallback icon using the link text
|
|
const fallbackIcon = createFallbackIcon(linkText);
|
|
|
|
const iconContainer = document.createElement('div');
|
|
iconContainer.className = 'article-link-icon fallback';
|
|
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;
|
|
opacity: 0.6;
|
|
`;
|
|
|
|
iconContainer.innerHTML = fallbackIcon;
|
|
paragraph.insertBefore(iconContainer, firstElement);
|
|
paragraph.style.position = 'relative';
|
|
} catch (e) {
|
|
// If even fallback fails, just skip
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
<!-- Link Preview Tooltip -->
|
|
<div id="link-preview-tooltip" class="link-preview-tooltip">
|
|
<div id="preview-content"></div>
|
|
</div>
|
|
|
|
<style>
|
|
.link-preview-tooltip {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
z-index: 10000;
|
|
width: 400px;
|
|
max-width: calc(100vw - 4rem);
|
|
background: rgba(255, 255, 255, 0.98);
|
|
backdrop-filter: blur(10px);
|
|
-webkit-backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
border-radius: 8px;
|
|
padding: 1.25rem;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
|
pointer-events: auto;
|
|
cursor: pointer;
|
|
font-family: et-book, Palatino, "Palatino Linotype", "Palatino LT STD", "Book Antiqua", Georgia, serif;
|
|
opacity: 0;
|
|
transform: translateY(100%);
|
|
transition: opacity 0.08s ease, transform 0.08s ease;
|
|
}
|
|
|
|
.link-preview-tooltip:hover {
|
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.link-preview-tooltip.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
|
|
a.link-preview-active {
|
|
background: rgba(243, 156, 18, 0.15);
|
|
box-shadow: 0 0 8px rgba(243, 156, 18, 0.4);
|
|
border-radius: 3px;
|
|
padding: 0.1rem 0.2rem;
|
|
margin: -0.1rem -0.2rem;
|
|
}
|
|
|
|
.preview-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.preview-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
flex-shrink: 0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.preview-title {
|
|
font-size: 1.05rem;
|
|
font-weight: 600;
|
|
color: #222;
|
|
line-height: 1.4;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.preview-meta {
|
|
font-size: 0.8rem;
|
|
color: #888;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.preview-meta-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.preview-excerpt {
|
|
font-size: 0.875rem;
|
|
color: #555;
|
|
line-height: 1.6;
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
body.dark-mode .link-preview-tooltip {
|
|
background: rgba(26, 26, 26, 0.98);
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
body.dark-mode .preview-title {
|
|
color: #e5e5e5;
|
|
}
|
|
|
|
body.dark-mode .preview-meta {
|
|
color: #999;
|
|
}
|
|
|
|
body.dark-mode .preview-excerpt {
|
|
color: #bbb;
|
|
border-top-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.link-preview-tooltip {
|
|
background: rgba(26, 26, 26, 0.98);
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
.preview-title {
|
|
color: #e5e5e5;
|
|
}
|
|
|
|
.preview-meta {
|
|
color: #999;
|
|
}
|
|
|
|
.preview-excerpt {
|
|
color: #bbb;
|
|
border-top-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.link-preview-tooltip {
|
|
bottom: 1rem;
|
|
right: 1rem;
|
|
width: calc(100vw - 2rem);
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function() {
|
|
const tooltip = document.getElementById('link-preview-tooltip');
|
|
const previewContent = document.getElementById('preview-content');
|
|
let currentLink = null;
|
|
let hideTimeout = null;
|
|
const cache = {};
|
|
|
|
// Find all links in article content (internal and external)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Disable tooltips on index/archive pages
|
|
const path = window.location.pathname;
|
|
if (path.match(/\/(sidenotes|outlines|connections|quotes|terms|archive)/)) {
|
|
return; // Don't attach tooltip listeners on these pages
|
|
}
|
|
|
|
const articleLinks = document.querySelectorAll('article a[href]');
|
|
|
|
articleLinks.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
|
|
// Skip links in nav, breadcrumbs, etc.
|
|
if (link.closest('nav') || link.closest('.breadcrumbs')) return;
|
|
|
|
// Skip links in special index pages (sidenotes, outlines, etc.)
|
|
if (link.closest('.sidenotes-list') || link.closest('.outline-list') || link.closest('.sidenote-entry')) return;
|
|
|
|
// Skip sidenote links (links with # anchors to same page)
|
|
if (link.classList.contains('sidenote-link') || link.classList.contains('outline-link')) return;
|
|
|
|
// Skip anchor links and mailto/tel links
|
|
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
|
|
|
|
// Skip any link that contains a hash (likely anchor link)
|
|
if (href.includes('#')) return;
|
|
|
|
// Skip alternate format links (.md, .pdf)
|
|
if (href.endsWith('.md') || href.endsWith('.pdf')) return;
|
|
|
|
// Support internal links and external http/https links
|
|
if (href.startsWith('/') || href.startsWith('http://') || href.startsWith('https://')) {
|
|
link.addEventListener('mouseenter', handleLinkHover);
|
|
link.addEventListener('mouseleave', handleLinkLeave);
|
|
}
|
|
});
|
|
});
|
|
|
|
function handleLinkHover(e) {
|
|
const link = e.currentTarget;
|
|
const href = link.getAttribute('href');
|
|
|
|
clearTimeout(hideTimeout);
|
|
currentLink = link;
|
|
|
|
// Show tooltip after slight delay
|
|
setTimeout(() => {
|
|
if (currentLink === link) {
|
|
showPreview(link, href);
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
function handleLinkLeave() {
|
|
currentLink = null;
|
|
tooltip.classList.remove('visible');
|
|
hideTimeout = setTimeout(() => {
|
|
tooltip.style.display = 'none';
|
|
}, 200);
|
|
}
|
|
|
|
async function showPreview(link, href) {
|
|
// Check cache first
|
|
if (cache[href]) {
|
|
displayPreview(link, cache[href]);
|
|
return;
|
|
}
|
|
|
|
// Handle external links differently (can't fetch due to CORS)
|
|
const isExternal = href.startsWith('http://') || href.startsWith('https://');
|
|
|
|
if (isExternal) {
|
|
try {
|
|
const url = new URL(href);
|
|
const data = {
|
|
title: link.textContent || url.hostname,
|
|
excerpt: `External link: ${url.hostname}`,
|
|
url: href,
|
|
isExternal: true
|
|
};
|
|
cache[href] = data;
|
|
displayPreview(link, data);
|
|
} catch (e) {
|
|
console.error('Invalid URL:', e);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Fetch internal page
|
|
const response = await fetch(href);
|
|
const html = await response.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
// Extract metadata
|
|
const title = doc.querySelector('h1')?.textContent ||
|
|
doc.querySelector('title')?.textContent ||
|
|
'Untitled';
|
|
|
|
// Try to find reading time badge
|
|
const readingTimeBadge = doc.querySelector('.reading-time-badge');
|
|
const readingTime = readingTimeBadge?.textContent || null;
|
|
|
|
// Try to find post date
|
|
const dateElement = doc.querySelector('.post-subtitle');
|
|
const dateMatch = dateElement?.textContent.match(/[A-Z][a-z]+ \d{1,2}, \d{4}/);
|
|
const date = dateMatch ? dateMatch[0] : null;
|
|
|
|
// Get first meaningful paragraph
|
|
const paragraphs = doc.querySelectorAll('article section p:not(.subtitle):not(.sidenote):not(.marginnote)');
|
|
let excerpt = '';
|
|
for (const p of paragraphs) {
|
|
const text = p.textContent.trim();
|
|
if (text.length > 50) {
|
|
excerpt = text;
|
|
break;
|
|
}
|
|
}
|
|
if (excerpt.length > 200) {
|
|
excerpt = excerpt.substring(0, 200).trim() + '...';
|
|
}
|
|
|
|
// Try to get icon
|
|
const icon = doc.querySelector('.post-title-icon')?.src ||
|
|
doc.querySelector('.archive-post-icon')?.src ||
|
|
doc.querySelector('.article-icon')?.src ||
|
|
null;
|
|
|
|
// Calculate word count
|
|
const articleText = doc.querySelector('article section')?.textContent || '';
|
|
const wordCount = Math.round(articleText.trim().split(/\s+/).filter(w => w.length > 0).length);
|
|
|
|
const data = {
|
|
title,
|
|
readingTime,
|
|
date,
|
|
excerpt,
|
|
icon,
|
|
wordCount
|
|
};
|
|
|
|
cache[href] = data;
|
|
displayPreview(link, data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch preview:', error);
|
|
}
|
|
}
|
|
|
|
function displayPreview(link, data) {
|
|
let html = '<div class="preview-header">';
|
|
|
|
if (data.icon) {
|
|
html += `<img src="${data.icon}" class="preview-icon" alt="">`;
|
|
}
|
|
|
|
html += `<div>`;
|
|
html += `<div class="preview-title">${escapeHtml(data.title || 'Untitled')}</div>`;
|
|
|
|
const metaParts = [];
|
|
if (data.readingTime) metaParts.push(data.readingTime);
|
|
else if (data.wordCount && data.wordCount > 0) {
|
|
const estReadingTime = Math.ceil(data.wordCount / 200);
|
|
metaParts.push(`~${estReadingTime} min read`);
|
|
}
|
|
if (data.wordCount && data.wordCount > 0) metaParts.push(`${data.wordCount} words`);
|
|
if (data.date) metaParts.push(data.date);
|
|
|
|
if (metaParts.length > 0) {
|
|
html += `<div class="preview-meta">${metaParts.join(' • ')}</div>`;
|
|
}
|
|
|
|
html += `</div></div>`;
|
|
|
|
if (data.excerpt && data.excerpt.length > 0) {
|
|
html += `<div class="preview-excerpt">${escapeHtml(data.excerpt)}</div>`;
|
|
}
|
|
|
|
previewContent.innerHTML = html;
|
|
|
|
// Show tooltip in fixed position
|
|
tooltip.style.display = 'block';
|
|
|
|
// Trigger animation
|
|
setTimeout(() => {
|
|
tooltip.classList.add('visible');
|
|
}, 10);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|