1
0
mirror of https://github.com/avinal/avinal.github.io.git synced 2026-07-04 07:40:09 +05:30

13 Commits

Author SHA1 Message Date
dependabot[bot] 29d9e1a696 Bump vite from 7.3.3 to 7.3.5
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.3 to 7.3.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 08:57:28 +00:00
avinal 5df0a73c08 Add DevConf.CZ 2026 talk to events page
Links to the Reveal.js slides at /talks/devconf-2026/ and the
pretalx event page.
2026-06-16 13:55:20 +05:30
avinal a8c7e06330 Allow CDN scripts/styles for /talks/* in CSP header
Reveal.js, Mermaid, and highlight.js load from cdnjs.cloudflare.com
and cdn.jsdelivr.net. Add a path-specific CSP override for /talks/*
so these CDN resources are not blocked.
2026-06-16 13:55:20 +05:30
avinal 96ea6019ae Add Reveal.js slides for DevConf.CZ 2026 talk
"Lost in Transliteration: Why strlen("Dvořák") Returns 8"
Scheduled June 18, 2026 at 10:15 in room E104.

- Self-contained Reveal.js 5.1.0 deck loaded from CDN
- Markdown-based slides (slides.md) with HTML shell (index.html)
- IBM Carbon Design System theme with custom syntax highlighting
- Mermaid diagrams for gconv pipeline and iconv flow
- Speaker notes with full forms, translations, and delivery instructions
- Served at /talks/devconf-2026/ as a static page

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-06-16 13:37:28 +05:30
avinal 63ab0e12b2 update all packages, fix security vulnerabilities
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-19 12:14:02 +05:30
avinal df4f2e3863 switch to self-hosted Iosevka fonts, enable prefetch
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-19 12:04:05 +05:30
avinal f03f57f064 remove bookmarks page, add glibc contributions
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-19 11:46:11 +05:30
avinal 4f942563c1 add GCC match.pd contribution and sciezka project
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-04 18:05:37 +05:30
avinal f5e739494a update home page hero and nav links
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-02 18:18:33 +05:30
avinal 5f467665bc feat: add bookmarks page with image fetcher
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-02 18:18:33 +05:30
avinal 99f3fb5ec8 feat: add contributions page
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-02 18:18:32 +05:30
avinal 5fa9a10203 feat: add TOC, related posts, and reading progress bar
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-02 18:18:32 +05:30
avinal f613005a23 update resume and events data
Assisted by Claude Code

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
2026-05-02 18:18:32 +05:30
20 changed files with 3856 additions and 1140 deletions
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
site: "https://avinal.space", site: "https://avinal.space",
output: "static", output: "static",
integrations: [sitemap()], integrations: [sitemap()],
prefetch: true,
markdown: { markdown: {
shikiConfig: { shikiConfig: {
theme: "github-dark-default", theme: "github-dark-default",
+5
View File
@@ -14,3 +14,8 @@
Permissions-Policy = "camera=(), microphone=(), geolocation=()" Permissions-Policy = "camera=(), microphone=(), geolocation=()"
X-XSS-Protection = "1; mode=block" X-XSS-Protection = "1; mode=block"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cal.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' https: data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.listenbrainz.org https://coverartarchive.org https://itunes.apple.com https://api.github.com https://wakatime.com; frame-src https://cal.com;" Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cal.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' https: data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.listenbrainz.org https://coverartarchive.org https://itunes.apple.com https://api.github.com https://wakatime.com; frame-src https://cal.com;"
[[headers]]
for = "/talks/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com; img-src 'self' https: data:; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; connect-src 'self';"
+468 -1088
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -21,9 +21,11 @@
}, },
"homepage": "https://github.com/avinal/avinal.github.io#readme", "homepage": "https://github.com/avinal/avinal.github.io#readme",
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.15", "@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.2",
"astro": "^5.17.3", "@fontsource/iosevka": "^5.2.5",
"@fontsource/iosevka-aile": "^5.2.5",
"astro": "^6.3.5",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"unist-util-visit": "^5.1.0" "unist-util-visit": "^5.1.0"
+342
View File
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lost in Transliteration: Why strlen("Dvořák") Returns 8</title>
<meta name="description" content="DevConf.CZ 2026 talk by Avinal Kumar — character encoding, Unicode, and glibc's iconv internals" />
<meta name="author" content="Avinal Kumar" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/reveal.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/theme/black.min.css" id="theme" />
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap');
/* ============================================
IBM Carbon Design System — Color Tokens
============================================ */
:root {
/* Carbon Gray 80 background */
--r-background-color: #2f2f2f;
/* Carbon typography */
--r-main-font: 'IBM Plex Sans', system-ui, sans-serif;
--r-main-font-size: 34px;
--r-heading-font: 'IBM Plex Sans', system-ui, sans-serif;
--r-heading-color: #f4f4f4;
--r-heading-font-weight: 600;
--r-main-color: #c6c6c6;
--r-link-color: #78a9ff;
--r-link-color-hover: #a6c8ff;
--r-code-font: 'IBM Plex Mono', monospace;
--r-heading-text-transform: none;
--r-heading-letter-spacing: -0.01em;
/* Carbon color palette */
--carbon-blue-40: #78a9ff;
--carbon-blue-60: #0f62fe;
--carbon-purple-40: #be95ff;
--carbon-teal-20: #9ef0f0;
--carbon-teal-40: #08bdba;
--carbon-magenta-40: #ff7eb6;
--carbon-red-40: #ff8389;
--carbon-green-40: #42be65;
--carbon-yellow-30: #f1c21b;
--carbon-gray-10: #f4f4f4;
--carbon-gray-30: #c6c6c6;
--carbon-gray-50: #8d8d8d;
--carbon-gray-60: #6f6f6f;
--carbon-gray-70: #525252;
--carbon-gray-80: #393939;
--carbon-gray-90: #262626;
--carbon-gray-100: #161616;
}
.reveal {
font-weight: 400;
letter-spacing: 0;
}
.reveal h1, .reveal h2, .reveal h3 {
line-height: 1.2;
margin-bottom: 0.6em;
}
.reveal h2 { font-size: 2em; font-weight: 600; }
.reveal h3 { font-size: 1.4em; font-weight: 600; }
/* ---- Code blocks: Carbon snippet style ---- */
.reveal pre {
width: 100%;
font-size: 0.52em;
box-shadow: none;
border-radius: 0;
border: none;
background: var(--carbon-gray-100);
}
.reveal pre code {
padding: 1.2em 1.4em;
border-radius: 0;
max-height: 480px;
line-height: 1.65;
background: var(--carbon-gray-100);
color: #fff;
font-weight: 400;
}
.reveal code {
font-family: var(--r-code-font);
font-weight: 500;
}
.reveal p code, .reveal li code {
background: var(--carbon-gray-80);
border: none;
padding: 0.15em 0.45em;
border-radius: 0;
font-size: 0.88em;
color: var(--carbon-magenta-40);
}
/* ---- Carbon syntax highlighting (overrides highlight.js) ---- */
.reveal pre code .hljs-keyword,
.reveal pre code .hljs-type,
.reveal pre code .hljs-built_in { color: var(--carbon-purple-40); }
.reveal pre code .hljs-string,
.reveal pre code .hljs-doctag { color: var(--carbon-magenta-40); }
.reveal pre code .hljs-number,
.reveal pre code .hljs-literal { color: var(--carbon-blue-40); }
.reveal pre code .hljs-comment { color: var(--carbon-gray-60); font-style: normal; }
.reveal pre code .hljs-function,
.reveal pre code .hljs-title { color: var(--carbon-teal-20); }
.reveal pre code .hljs-variable,
.reveal pre code .hljs-attr { color: #fff; }
.reveal pre code .hljs-params { color: var(--carbon-gray-30); }
.reveal pre code .hljs-meta,
.reveal pre code .hljs-preprocessor { color: #569CD6; }
.reveal pre code .hljs-regexp { color: #D16969; }
.reveal pre code .hljs-symbol,
.reveal pre code .hljs-template-variable { color: var(--carbon-red-40); }
.hljs { background: var(--carbon-gray-100); color: #fff; }
/* ---- Utility classes: Carbon palette ---- */
.reveal .dim { opacity: 0.45; }
.reveal .accent { color: var(--carbon-blue-40); }
.reveal .green { color: var(--carbon-green-40); }
.reveal .yellow { color: var(--carbon-yellow-30); }
.reveal .orange { color: #f0883e; }
.reveal .red { color: var(--carbon-red-40); }
.reveal .purple { color: var(--carbon-purple-40); }
.reveal .teal { color: var(--carbon-teal-20); }
.reveal .magenta { color: var(--carbon-magenta-40); }
.reveal .big { font-size: 1.6em; font-weight: 600; letter-spacing: -0.02em; }
.reveal .medium { font-size: 1.15em; font-weight: 500; }
.reveal .small { font-size: 0.7em; }
.reveal .tiny {
font-size: 0.45em;
color: var(--carbon-gray-60);
font-family: var(--r-code-font);
letter-spacing: 0.02em;
}
/* ---- Tables ---- */
.reveal table { font-size: 0.72em; border-collapse: collapse; border-spacing: 0; }
.reveal table th {
color: var(--carbon-gray-10);
font-weight: 600;
background: var(--carbon-gray-80);
padding: 0.6em 1em;
border-bottom: 2px solid var(--carbon-gray-70);
text-align: left;
}
.reveal table td {
padding: 0.5em 1em;
border-bottom: 1px solid var(--carbon-gray-70);
}
.reveal table tr:hover td { background: rgba(255,255,255,0.04); }
/* ---- Custom blocks: Carbon surface style ---- */
.reveal .hex-display {
font-family: var(--r-code-font);
font-size: 0.62em;
background: var(--carbon-gray-100);
padding: 1em 1.4em;
border-radius: 0;
border: none;
display: inline-block;
line-height: 1.9;
color: #fff;
}
.reveal .diagram {
background: var(--carbon-gray-100);
border: none;
border-radius: 0;
padding: 1.2em 1.4em;
font-family: var(--r-code-font);
font-size: 0.58em;
line-height: 1.7;
color: #fff;
}
/* ---- Section label ---- */
.reveal .slide-title {
font-size: 0.45em;
color: var(--carbon-blue-40);
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 600;
margin-bottom: 0.3em;
opacity: 0.8;
}
/* ---- Blockquotes ---- */
.reveal blockquote {
background: var(--carbon-gray-80);
border-left: 4px solid var(--carbon-blue-60);
padding: 0.8em 1.2em;
font-style: italic;
width: 85%;
border-radius: 0;
}
/* ---- Grid layouts ---- */
.reveal .two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2em;
text-align: left;
}
.reveal .three-col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1.5em;
text-align: left;
font-size: 0.8em;
}
/* ---- Cards: Carbon tile style ---- */
.reveal .card {
background: var(--carbon-gray-80);
border: none;
border-radius: 0;
padding: 1.2em;
}
.reveal .card h4 {
margin-bottom: 0.5em;
}
/* ---- Lists ---- */
.reveal ul, .reveal ol { display: block; }
.reveal li {
margin-bottom: 0.5em;
line-height: 1.5;
}
.reveal ul li::marker { color: var(--carbon-blue-40); }
.reveal ol li::marker { color: var(--carbon-blue-40); font-weight: 600; }
/* ---- Glow effects for emphasis ---- */
.reveal .glow-blue {
text-shadow: 0 0 40px rgba(120,169,255,0.4), 0 0 80px rgba(120,169,255,0.15);
color: var(--carbon-blue-40);
}
.reveal .glow-red {
text-shadow: 0 0 40px rgba(255,131,137,0.4), 0 0 80px rgba(255,131,137,0.15);
color: var(--carbon-red-40);
}
.reveal .glow-green {
text-shadow: 0 0 40px rgba(66,190,101,0.4), 0 0 80px rgba(66,190,101,0.15);
color: var(--carbon-green-40);
}
/* ---- Badges: Carbon tag style ---- */
.reveal .badge {
display: inline-block;
font-size: 0.55em;
font-weight: 500;
padding: 0.15em 0.7em;
border-radius: 0;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.reveal .badge-blue { background: rgba(120,169,255,0.2); color: var(--carbon-blue-40); }
.reveal .badge-red { background: rgba(255,131,137,0.2); color: var(--carbon-red-40); }
.reveal .badge-green { background: rgba(66,190,101,0.2); color: var(--carbon-green-40); }
.reveal .badge-yellow { background: rgba(241,194,27,0.2); color: var(--carbon-yellow-30); }
.reveal .badge-purple { background: rgba(190,149,255,0.2); color: var(--carbon-purple-40); }
/* ---- HR ---- */
.reveal hr {
border: none;
height: 1px;
background: var(--carbon-gray-70);
margin: 1em 0;
}
/* ---- Progress bar ---- */
.reveal .progress span { background: var(--carbon-blue-60); }
/* ---- Auto-animate transitions ---- */
.reveal [data-auto-animate] .hex-display,
.reveal [data-auto-animate] .diagram,
.reveal [data-auto-animate] pre {
transition: all 0.6s ease;
}
/* ---- Slide number ---- */
.reveal .slide-number {
font-family: var(--r-code-font);
font-size: 0.5em;
color: var(--carbon-gray-60);
}
</style>
</head>
<body>
<div class="reveal">
<div class="slides">
<section
data-markdown="slides.md"
data-separator="^---$"
data-separator-vertical="^--$"
data-separator-notes="^Note:"
data-charset="utf-8">
</section>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/reveal.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/plugin/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/plugin/notes/notes.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/plugin/highlight/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reveal.js/5.1.0/plugin/zoom/zoom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/reveal.js-mermaid-plugin@11.15.0/plugin/mermaid/mermaid.js"></script>
<script>
Reveal.initialize({
mermaid: {
theme: 'dark',
themeVariables: {
darkMode: true,
background: '#2f2f2f',
primaryColor: '#393939',
primaryTextColor: '#c6c6c6',
primaryBorderColor: '#525252',
lineColor: '#78a9ff',
secondaryColor: '#262626',
tertiaryColor: '#161616',
fontFamily: "'IBM Plex Sans', system-ui, sans-serif",
fontSize: '18px',
},
},
hash: true,
slideNumber: 'c/t',
showSlideNumber: 'speaker',
transition: 'fade',
transitionSpeed: 'default',
backgroundTransition: 'fade',
center: true,
width: 1280,
height: 720,
margin: 0.08,
autoAnimateEasing: 'ease-in-out',
autoAnimateDuration: 0.8,
autoAnimateUnmatched: true,
zoomKey: 'alt',
plugins: [RevealMarkdown, RevealHighlight, RevealNotes, RevealZoom, RevealMermaid],
});
</script>
</body>
</html>
+923
View File
@@ -0,0 +1,923 @@
<!-- ===================================================== -->
<!-- SECTION 1: THE PROBLEM (mystery opening) -->
<!-- ===================================================== -->
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
```bash
$ printf 'Dvořák' | wc -c
```
<!-- .element: data-id="mystery-code" -->
Note:
**Do:** Walk on stage, put terminal on screen, no output yet. Pause 3-4 seconds. Ask: "What do you think this prints?"
- **wc** = word count; **-c** = count bytes (not characters)
- Dvořák = Czech composer surname, pronounced "DVOR-zhahk"
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
```bash
$ printf 'Dvořák' | wc -c
8
```
<!-- .element: data-id="mystery-code" -->
Note:
**Do:** Reveal the 8. Pause. Dvořák has 6 visible letters — why 8? Don't explain yet.
- wc -c counts bytes, not characters — this is POSIX behavior, not a bug
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
```bash
$ printf 'Dvořák' | wc -c
8
```
<!-- .element: data-id="mystery-code" -->
<br />
How many people think this is **wrong**?
<!-- .element: class="medium" -->
Note:
**Do:** Ask the question. Wait 5 seconds. Let hands go up. Do NOT answer yet.
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
```bash
$ printf 'Dvořák' | wc -c
8
$ python3 -c "print(len('Dvořák'))"
6
```
<!-- .element: data-id="mystery-code" -->
Note:
Two different answers for the same string. Let the confusion build.
- Python 3 len() counts Unicode code points, not bytes
- *Exception:* Python 2 len() counted bytes — this changed in 2→3
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
```bash
$ printf '😀' | wc -c
4
$ python3 -c "print(len('😀'))"
1
```
<!-- .element: data-id="mystery-code" -->
Note:
An emoji: 4 bytes vs 1 character.
- 😀 = U+1F600 "Grinning Face." Needs 4 bytes in UTF-8 (F0 9F 98 80) because it's above the **BMP** (Basic Multilingual Plane, U+0000U+FFFF)
- *Exception:* On macOS, `echo` appends a newline — use `printf` to avoid off-by-one
--
<!-- .slide: data-background-color="#2f2f2f" -->
Which one is **correct**?
<!-- .element: class="big" -->
All of them.
<!-- .element: class="fragment zoom-in glow-blue big" -->
Understanding why is basically the entire talk.
<!-- .element: class="fragment fade-up small dim" -->
Note:
**Do:** Pause before "All of them." Then: *"They're counting different things. wc counts bytes. Python counts code points. Both correct."*
**Key thesis:** bytes ≠ characters ≠ code points
---
<!-- ===================================================== -->
<!-- SECTION 2: INTRODUCTION -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="zoom" -->
## Lost in Transliteration
Why `strlen("Dvořák")` Returns **8**
<!-- .element: class="medium" style="opacity: 0.9" -->
<br />
Avinal Kumar · glibc contributor
<!-- .element: style="font-weight: 500" -->
<span class="badge badge-blue">DevConf.CZ 2026</span>
<!-- .element: class="small dim" -->
Note:
**Do:** Brief intro, under 30 seconds:
*"I'm Avinal. I contribute to glibc — the GNU C Library. I got into character encodings through an iconv bug at the glibc workshop here at DevConf. Today I'll take you through that journey."*
- **glibc** = GNU C Library — the standard C library on most Linux distros
- **iconv** = POSIX API for converting text between character encodings
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
### Today we'll answer
1. Why does `strlen("Dvořák")` return 8?
2. Why does Unicode exist?
3. How does the C library handle text?
4. How does `iconv` convert between encodings?
5. Does any of this still matter in 2026?
Note:
**Do:** Read out loud. Give the audience a roadmap. Don't linger.
- **strlen** = "string length" — counts bytes before the null terminator, NOT characters
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="fade" -->
<span class="glow-blue big">There is no such thing as plain text.</span>
<br />
If you remember one thing from this talk, remember that sentence.
<!-- .element: class="fragment fade-up small dim" -->
Note:
**Do:** Say this slowly. Pause. *"If you remember one thing, remember that sentence."*
- "Plain text" implies no encoding — but every byte sequence *has* an encoding. If you don't know it, you're guessing. Wrong guess = **mojibake** (文字化け, Japanese for garbled text, pronounced "mo-ji-ba-keh")
---
<!-- ===================================================== -->
<!-- SECTION 3: HISTORY -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">How we ended up with this mess</p>
### ASCII: The 7-bit world
<div class="two-col">
<div>
- 128 characters (0127)
- 7 bits per character
- English letters, digits, punctuation
- Bit 8 was "spare"
</div>
<div>
```text
0x41 = A
0x61 = a
0x30 = 0
0x20 = (space)
0x0A = (newline)
```
</div>
</div>
*"And all was good — if you spoke English."*
<!-- .element: class="fragment fade-up" -->
Note:
- **ASCII** = American Standard Code for Information Interchange (1963)
- 7 bits = 128 values. The 8th bit was for parity checking on noisy telegraph lines
- Only covers English — no accented chars, no Cyrillic, no CJK, no Arabic
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">How we ended up with this mess</p>
### Code Pages: Everyone fills bit 8 differently
If I send byte `0xE9` from Paris to Moscow, what character arrives?
<!-- .element: class="medium" -->
| Byte | CP-1252 (Western) | CP-866 (Russian) | CP-862 (Hebrew) |
|------|-------------------|-------------------|------------------|
| `0xE9` | é | щ | ט |
| `0xC4` | Ä | ─ | ד |
| `0xF1` | ñ | ё | ס |
<!-- .element: class="fragment fade-in" -->
CJK needed **thousands** — multi-byte encodings (Shift-JIS, EUC-KR, GB2312) where you can't even move backward in a string.
<!-- .element: class="fragment fade-up small" -->
Note:
**Do:** Ask *"If I send byte 0xE9 from Paris to Moscow, what character arrives?"* before revealing the table.
- **CP** = Code Page. CP-1252 = Windows Western. CP-866 = DOS Russian. CP-862 = DOS Hebrew
- Same byte, different characters — the bytes are correct, the *interpretation* is wrong
- **CJK** = Chinese, Japanese, Korean
- **Shift-JIS** = Shift Japanese Industrial Standards. **EUC-KR** = Extended Unix Code for Korean. **GB2312** = Chinese National Standard
- *Exception:* Multi-byte encodings have a "forward-only" problem — you can't tell if a byte is byte 1 or byte 2 of a character
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">How we ended up with this mess</p>
### Unicode: One number per character
```text
U+0041 = A U+00E9 = é U+010D = č
U+0639 = ع U+4E16 = 世 U+1F600 = 😀
```
- Code points are **abstract numbers**, not bytes <!-- .element: class="fragment fade-up" -->
- <span class="red">Not</span> "16-bit characters" — that's the myth <!-- .element: class="fragment fade-up" -->
- 154,998 characters across 168 scripts <!-- .element: class="fragment fade-up" -->
Unicode separated the *idea* of a character from how it's stored.
<!-- .element: class="fragment zoom-in accent" -->
Note:
- **Unicode** = Universal Coded Character Set (1991, Unicode Consortium)
- Code points are abstract numbers — how you *store* them is a separate question (that's what encodings answer)
- *Exception:* "Unicode is 16-bit" myth comes from Unicode 1.0 (1991) which only planned 65,536 chars. Unicode 2.0 (1996) expanded beyond 16 bits. Java and Windows adopted UTF-16 before that expansion, and are now stuck with it
- **BMP** = Basic Multilingual Plane (U+0000U+FFFF). Characters above it (emoji, rare scripts) are in supplementary planes
---
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">How we ended up with this mess</p>
### Encodings: Serialization formats
<div class="three-col">
<div class="card fragment fade-up" data-fragment-index="1">
<h4 class="glow-blue">UTF-8</h4>
- 14 bytes
- ASCII-compatible
- <span class="badge badge-blue">98% of the web</span>
</div>
<div class="card fragment fade-up" data-fragment-index="2">
<h4 class="yellow">UTF-16</h4>
- 2 or 4 bytes
- Needs BOM
- <span class="badge badge-yellow">Windows, Java</span>
</div>
<div class="card fragment fade-up" data-fragment-index="3">
<h4 class="green">UTF-32</h4>
- Fixed 4 bytes
- Simple but wasteful
- <span class="badge badge-green">glibc internal</span>
</div>
</div>
Note:
- **UTF** = Unicode Transformation Format
- **UTF-8:** Designed 1992 by Ken Thompson & Rob Pike. ASCII bytes are identical — this is why it won. 98.2% of websites (W3Techs, 2024)
- **UTF-16:** Uses surrogate pairs above U+FFFF. **BOM** = Byte Order Mark (U+FEFF) — indicates endianness
- **UTF-32:** Also called **UCS-4** (Universal Coded Character Set, 4-byte). "hello" = 20 bytes instead of 5
- *Exception:* UTF-32 and UCS-4 are technically from different standards (ISO 10646 vs Unicode), but identical in practice
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
<p class="slide-title">How we ended up with this mess</p>
### Encodings: Serialization formats
<div class="hex-display" data-id="encoding-hex">
"Dvořák" in UTF-8: &nbsp;44 76 6F <span class="red" style="font-weight:700;">C5 99</span> C3 A1 6B &nbsp;&nbsp;&nbsp;<span class="badge badge-blue">8 bytes</span><br />
"Dvořák" in UTF-32: 00000044 00000076 0000006F <span class="red" style="font-weight:700;">00000159</span> 000000E1 0000006B &nbsp;<span class="badge badge-green">24 bytes</span>
</div>
<span class="glow-blue big">There is no such thing as plain text.</span>
<!-- .element: class="fragment zoom-in" -->
Note:
UTF-8 breakdown:
- D, v, o, k = 1 byte each (ASCII range)
- ř = C5 99 (2 bytes, U+0159)
- á = C3 A1 (2 bytes, U+00E1)
- Total: 4×1 + 2×2 = **8 bytes** for 6 characters
UTF-32: every char = 4 bytes → 6×4 = **24 bytes**. Same string, 3× the size.
---
<!-- ===================================================== -->
<!-- SECTION 4: INTO C — real examples, not code -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="zoom" -->
<span class="badge badge-blue" style="font-size: 0.6em;">Part 2</span>
## Text in C: What actually happens
Note:
**Do:** *"Now we understand WHY bytes and characters differ. Let's see how C deals with it."*
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### C has two ways to see a string
<div class="two-col">
<div class="card">
#### `char` — bytes
- 1 byte per element, no encoding info
- `strlen("Dvořák")`**8**
- `strlen("😀")`**4**
- Indexing gives you bytes, not characters
</div>
<div class="card">
#### `wchar_t` — code points
- 4 bytes on Linux, <span class="red">2 on Windows</span>
- `wcslen(L"Dvořák")`**6**
- `wcslen(L"😀")`**1**
- Indexing gives you characters
</div>
</div>
<br />
`mbrtowc()` bridges between them. `setlocale()` tells it which encoding to expect.
<!-- .element: class="fragment fade-up small" -->
Note:
- **wchar_t** = "wide character type." Linux: 4 bytes (UCS-4). Windows: 2 bytes (UTF-16)
- **wcslen** = "wide character string length"
- **L"..."** prefix = wide string literal
- **mbrtowc** = "multibyte restartable to wide character" — converts one multibyte char to one wchar_t
- **setlocale** with LC_CTYPE tells mbrtowc the encoding. Without it → "C" locale = ASCII only
- *Exception:* On Windows, wcslen(L"😀") returns **2** (surrogate pair), not 1
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### What does "Dvořák" look like in memory?
```text
Character: D v o ř á k
UTF-8 hex: 44 76 6F C5 99 C3 A1 6B
Bytes: 1 1 1 2 2 1 = 8 bytes
Code points: 1 1 1 1 1 1 = 6 characters
```
`strlen` counts the top row. `wcslen` counts the bottom row.
<!-- .element: class="fragment fade-up small" -->
Now you know why `strlen("Dvořák")` returns 8.
<!-- .element: class="fragment fade-up accent" -->
Note:
**Do:** Point at the diagram: *"strlen counts bytes: 1+1+1+2+2+1 = 8. wcslen counts characters: always 1 each = 6. Both correct."*
This is the answer to the opening mystery.
---
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
<p class="slide-title">Text in C</p>
### `iconv` — converting between encodings
```bash
$ echo 'Dvořák' | iconv -f UTF-8 -t ASCII
iconv: illegal input sequence at position 3
```
<!-- .element: data-id="iconv-demo" -->
Note:
- **iconv** = both a C API (iconv_open/iconv/iconv_close in `<iconv.h>`) and a CLI tool
- **-f** = from, **-t** = to
- Position 3 = 4th byte (0-indexed) = where ř starts. ASCII only has 0127; C5 = 197 → fails
- **EILSEQ** = "illegal sequence" errno value
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
<p class="slide-title">Text in C</p>
### `iconv` — converting between encodings
```bash
$ echo 'Dvořák' | iconv -f UTF-8 -t ASCII
iconv: illegal input sequence at position 3
$ echo 'Dvořák' | iconv -f UTF-8 -t ASCII//TRANSLIT
Dvorak
$ echo 'Dvořák' | iconv -f UTF-8 -t ASCII//IGNORE
Dvok
```
<!-- .element: data-id="iconv-demo" -->
- **`//TRANSLIT`** — approximate: ř→r, á→a
- **`//IGNORE`** — drop what doesn't fit
Note:
- **//TRANSLIT** = transliteration. Appended to target encoding. Finds closest match: ř→r, á→a, ö→o, ñ→n
- **//IGNORE** = silently drop unconvertible chars. Notice "Dvok" — both ř AND á dropped
- *Exception:* //TRANSLIT is glibc-specific, not POSIX. musl libc (Alpine Linux) doesn't support it
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### Real encoding pairs from across the world
```bash
$ echo '東京' | iconv -f UTF-8 -t SHIFT_JIS | hexdump -C
00000000 93 8c 8b 9e 0a |.....|
$ echo 'こんにちは世界' | iconv -f UTF-8 -t EUC-JP | hexdump -C
00000000 a4 b3 a4 f3 a4 cb a4 c1 a4 cf c0 a4 b3 a6 0a |...............|
$ echo 'Ελληνικά κείμενο' | iconv -f UTF-8 -t ISO-8859-7 | hexdump -C
00000000 c5 eb eb e7 ed e9 ea dc 20 ea e5 df ec e5 ed ef |........ .......|
```
Same characters, completely different bytes — depending on the encoding.
<!-- .element: class="fragment fade-up small" -->
Note:
- 東京 = Tōkyō (Tokyo)
- こんにちは世界 = "Konnichiwa Sekai" = "Hello World"
- Ελληνικά κείμενο = "Elliniká keímeno" = "Greek text"
- **hexdump -C** = canonical hex+ASCII dump. Non-ASCII shows as dots
- Same text in Shift-JIS vs EUC-JP → completely different bytes. Without knowing the encoding, unreadable
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### When conversion fails
```bash
$ echo 'مرحبا' | iconv -f UTF-8 -t ISO-8859-1
iconv: illegal input sequence at position 0
$ echo 'Résumé' | iconv -f UTF-8 -t CP866
iconv: illegal input sequence at position 1
$ echo -ne '\xEF\xBB\xBFhello' | hexdump -C
00000000 ef bb bf 68 65 6c 6c 6f |...hello|
$ echo -ne '\xEF\xBB\xBFhello' | iconv -f UTF-8 -t ASCII//TRANSLIT
hello
```
- Arabic → Latin-1: impossible — the encoding can't hold it
- French Résumé → Russian CP866: `é` doesn't exist in that code page
- BOM: 3 invisible bytes at the start — your first "character" is garbage
Note:
- مرحبا = "marhaba" = "hello" in Arabic
- **ISO-8859-1** = Latin-1. Zero Arabic chars → fails at position 0
- **CP866** = DOS Cyrillic. é doesn't map → fails at position 1 (R is fine, é isn't)
- **BOM** = Byte Order Mark (U+FEFF, encoded EF BB BF in UTF-8). Windows Notepad adds it. Breaks JSON parsers, shell shebangs, and string comparisons
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### Longer text, bigger difference
```bash
$ printf 'Příliš žluťoučký kůň úpěl ďábelské ódy' | wc -c
53
$ python3 -c "print(len('Příliš žluťoučký kůň úpěl ďábelské ódy'))"
38
$ echo 'Příliš žluťoučký kůň úpěl ďábelské ódy' \
| iconv -f UTF-8 -t ASCII//TRANSLIT
Prilis zlutoucky kun upel dabelske ody
```
A Czech pangram: **38 characters**, **53 bytes** — a 40% difference.
<!-- .element: class="fragment fade-up" -->
`//TRANSLIT` strips all diacritics and produces valid ASCII.
<!-- .element: class="fragment fade-up small" -->
Note:
- **Translation:** "Too yellow a horse groaned devilish odes" — a Czech pangram (like "The quick brown fox" but for testing diacritics)
- 15 extra bytes from accented characters: each adds 1 byte in UTF-8
- Czech diacritics: **háček** (ˇ) = caron (ř, š, č, ž, ň, ď, ť, ě), **čárka** (´) = acute (á, é, í, ó, ú), **kroužek** (°) = ring (ů)
- **Do:** DevConf is in Brno — the audience will recognize this pangram
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Text in C</p>
### How many encodings?
```bash
$ iconv -l | wc -l
1180
$ find /usr/lib64/gconv -name '*.so' | wc -l
253
```
**1180** encoding names served by **253** shared libraries.
<!-- .element: class="fragment fade-up" -->
How does glibc manage this without writing thousands of converters?
<!-- .element: class="fragment fade-up accent" -->
Note:
**Do:** LIVE DEMO if possible.
- **iconv -l** = list all encodings. 1180 includes aliases (SHIFT-JIS, SJIS, MS_KANJI = same encoding)
- **/usr/lib64/gconv/** = where glibc stores converter .so files (Fedora/RHEL). Debian: /usr/lib/x86_64-linux-gnu/gconv/
- **.so** = shared object (dynamically loaded library)
- 1180 names, 253 plugins — far fewer than the 39,800 needed for N×N
---
<!-- ===================================================== -->
<!-- SECTION 5: HOW IT WORKS INSIDE -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="zoom" -->
<span class="badge badge-blue" style="font-size: 0.6em;">Part 3</span>
## Inside glibc's iconv
Note:
**Do:** *"We've seen what iconv does from the outside. Now let's look under the hood."*
- **gconv** = glibc's internal conversion framework ("g" = GNU, "conv" = conversion)
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Inside glibc</p>
### The naive approach: N×N converters
Suppose I support 200 encodings. How many converters do I need?
<!-- .element: class="medium" -->
```text
Shift-JIS → UTF-8 UTF-8 → Shift-JIS
Shift-JIS → EUC-KR EUC-KR → Shift-JIS
UTF-8 → EUC-KR EUC-KR → UTF-8
...
```
<!-- .element: class="fragment fade-in" -->
5 encodings = 20 converters. 200 encodings?
<!-- .element: class="fragment fade-up" -->
200 × 199 = <span class="red">39,800 converters</span>. That's not going to work.
<!-- .element: class="fragment zoom-in" -->
Note:
**Do:** Ask *"How many converters do I need?"* before revealing. Let them guess.
- Formula: N × (N-1) for directed pairs
- Nobody will write 39,800 converters
---
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
<p class="slide-title">Inside glibc</p>
### The smart approach: one universal pivot
What if every encoding just learned to convert to **one common format**?
<!-- .element: class="medium" -->
```text
Shift-JIS → ??? → UTF-8
```
<!-- .element: data-id="hub-text" class="fragment fade-in" -->
Note:
Hub-and-spoke architecture — same principle as airline routing through hub airports.
--
<!-- .slide: data-auto-animate data-background-color="#2f2f2f" -->
<p class="slide-title">Inside glibc</p>
### The smart approach: one universal pivot
glibc's gconv framework uses an internal **UCS-4 based representation** as the pivot.
```text
Shift-JIS → UCS-4 → UTF-8
```
<!-- .element: data-id="hub-text" -->
Now you need just **2 converters per encoding** (to UCS-4 and from UCS-4).
<!-- .element: class="fragment fade-up" -->
200 encodings × 2 = <span class="green">400 converters</span> instead of 39,800.
<!-- .element: class="fragment zoom-in" -->
Note:
- **UCS-4** = Universal Coded Character Set, 4-byte form (ISO 10646). Essentially UTF-32
- glibc calls it **INTERNAL** in gconv-modules config
- 2 converters per encoding → 400 total. 99% reduction
- *Exception:* glibc says "UCS-4 *based*" — the internal representation has nuances around stateful encodings
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Inside glibc</p>
### The lookup table: `gconv-modules`
<pre><code class="language-text" data-line-numbers data-ln-start-from="47"># iconvdata/gconv-modules
# from to module cost
module ISO-8859-1// INTERNAL ISO8859-1 1
module INTERNAL ISO-8859-1// ISO8859-1 1</code></pre>
<pre><code class="language-text" data-line-numbers data-ln-start-from="415"># iconvdata/gconv-modules-extra.conf
module SJIS// INTERNAL SJIS 1
module INTERNAL SJIS// SJIS 1</code></pre>
`INTERNAL` = the UCS-4 pivot
<!-- .element: class="fragment fade-up accent" -->
Each line maps an encoding to a `.so` plugin. `iconv_open` reads this file, loads the right plugins, and chains them.
<!-- .element: class="fragment fade-up small" -->
Note:
These are actual files from the glibc source tree.
- Format: `module FROM// TO MODULE_NAME COST`
- **INTERNAL** = glibc's name for UCS-4
- **Cost** = routing weight when multiple paths exist (lower = preferred)
- Each encoding has exactly 2 lines — one each direction. Hub-and-spoke in practice
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Inside glibc</p>
### The conversion pipeline
<div class="mermaid">
<pre>
flowchart TB
A["Shift-JIS bytes"] --> B["SJIS.so\n(gconv module)"]
B --> C["UCS-4\n(internal pivot)"]
C --> D["UTF-8 converter\n(built-in)"]
D --> E["UTF-8 bytes"]
style C fill:#0f62fe,stroke:#78a9ff,color:#fff
style B fill:#393939,stroke:#78a9ff,color:#c6c6c6
style D fill:#393939,stroke:#78a9ff,color:#c6c6c6
style A fill:#262626,stroke:#525252,color:#f1c21b
style E fill:#262626,stroke:#525252,color:#42be65
</pre>
</div>
Adding a new encoding = writing **one** `.so` plugin.
<!-- .element: class="fragment fade-up small" -->
Note:
**Do:** THIS IS THE MONEY SLIDE. Spend time here. Point at each box:
1. *"Shift-JIS bytes come in"*
2. *"SJIS.so converts to UCS-4"*
3. *"UTF-8 converter turns UCS-4 into UTF-8"*
4. *"UTF-8 bytes come out"*
Adding a new encoding = one .so that converts to/from UCS-4. People will photograph this.
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Inside glibc</p>
### The iconv flow
<div class="mermaid">
<pre>
sequenceDiagram
participant App as Your Code
participant glibc as glibc internals
App->>glibc: iconv_open("UTF-8", "SJIS")
Note right of glibc: look up gconv-modules
Note right of glibc: load SJIS.so + UTF-8
Note right of glibc: build step chain
glibc-->>App: return descriptor
App->>glibc: iconv(cd, &in, ...)
Note right of glibc: step[0]: SJIS → UCS-4
Note right of glibc: step[1]: UCS-4 → UTF-8
glibc-->>App: advance pointers
App->>glibc: iconv_close(cd)
Note right of glibc: free chain, unload modules
</pre>
</div>
Three calls. That's the entire API.
<!-- .element: class="fragment fade-up small" -->
Note:
The API in three calls:
1. **iconv_open** → returns descriptor (pointer to gconv_info struct with step chain)
2. **iconv** → walks the chain. Both in/out pointers advance. Errors: **EILSEQ** (illegal sequence), **E2BIG** (output buffer full — flush and retry, not a real error), **EINVAL** (incomplete sequence)
3. **iconv_close** → free chain, unload modules
- *Highlight:* E2BIG is the #1 mistake — people call iconv once and assume it's done
---
<!-- ===================================================== -->
<!-- SECTION 6: RELEVANCE TODAY -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="zoom" -->
<span class="badge badge-red" style="font-size: 0.6em;">Part 4</span>
## Does this still matter?
Note:
**Do:** *"Modern languages have Unicode strings by default. So why should anyone care about iconv in 2026?"*
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Relevance today</p>
### How modern languages handle encoding
| Language | Strings are... | Encoding conversion |
|----------|----------------|---------------------|
| **Python 3** | Unicode internally | Built-in codecs |
| **Go** | UTF-8 by definition | `golang.org/x/text` |
| **Rust** | Always valid UTF-8 | `encoding_rs` crate |
| **Java** | UTF-16 internally | `java.nio.charset` |
| **C/C++** | Just bytes — no encoding | **`iconv`** |
Modern languages solved this by making strings Unicode-native. C didn't — and can't, because it would break 50 years of code.
<!-- .element: class="fragment fade-up small" -->
Note:
- C can't change because `char = 1 byte` is baked into the language spec and **ABI** (Application Binary Interface)
- Even modern languages need encoding conversion at **I/O boundaries** — files, sockets, C library calls via **FFI** (Foreign Function Interface)
- Python's codecs, Go's x/text, Rust's encoding_rs all exist because the outside world isn't always UTF-8
---
<!-- .slide: data-background-color="#2f2f2f" data-transition="slide" -->
<p class="slide-title">Relevance today</p>
### Encoding bugs are alive and well
<div class="two-col">
<div class="card">
#### The Turkish İ problem
| Locale | `toupper('i')` |
|--------|----------------|
| en_US | I |
| tr_TR | <span class="red">İ</span> (dotted!) |
Tests pass in English, break in Turkish.
</div>
<div class="card">
#### `//IGNORE` inconsistency
```bash
$ echo 'héllo' | iconv \
-f UTF-8 -t ASCII//IGNORE
```
Some modules skip the bad byte. Some stop with an error.
**Same flag, different behavior.**
</div>
</div>
<br />
Every time a language reads a file, parses a socket, or calls a C library — encoding conversion still happens. These bugs still bite.
<!-- .element: class="fragment fade-up accent small" -->
Note:
**Turkish İ:**
- Turkish has 4 i's: i, İ, ı, I. toupper('i') → İ (U+0130), not I
- Any case-insensitive comparison using toupper/tolower is locale-dependent
**//IGNORE:**
- Behavior depends on *which* gconv module runs — inconsistent across encodings
- This is a real unfixed glibc bug. This is what got me into the codebase
---
<!-- ===================================================== -->
<!-- SECTION 7: GLIBC WORKSHOP -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="fade" -->
### glibc Development Workshop — Third Edition
Led by **Arjun Shankar** (Red Hat, glibc developer)
<span class="accent medium">Tomorrow, Friday June 19 · 10:15 AM · Room A218</span>
Pick a bug, get a cheat sheet, ship a patch.
6 patches in 2024 · 15+ in 2025 · **yours in 2026?**
Note:
**Do:** Tell the personal story:
*"Two years ago I walked into this workshop at DevConf. Arjun gave me a small iconv task. I got curious, fell down the rabbit hole, and that became this talk. That one task turned into 14 patches in glibc."*
- **Arjun Shankar** = Red Hat engineer, glibc developer. Runs this workshop yearly at DevConf.CZ
- Format: show up, get a cheat sheet with a small bug + pointers, experienced contributors help you submit
- Room A218, capacity 20. First come, first served
- *"If anything in this talk made you curious, room A218 tomorrow morning."*
---
<!-- ===================================================== -->
<!-- SECTION 8+9: REFERENCES + QUESTIONS -->
<!-- ===================================================== -->
<!-- .slide: data-background-color="#2f2f2f" data-transition="fade" -->
### Questions? · Resources
- **Joel Spolsky** — "The Absolute Minimum Every Software Developer Must Know About Unicode" <!-- .element: class="small" -->
- **GNU C Library Manual** — "Character Set Handling" chapter <!-- .element: class="small" -->
- **unicode.org** — the specification <!-- .element: class="small" -->
<span class="badge badge-blue">avinal.space</span> · <span class="badge badge-purple">@avinal</span>
Attendance at DevConf.CZ 2026 was supported by the **[GNU Toolchain Fund](https://my.fsf.org/civicrm/contribute/transact?reset=1&id=57)**, a part of the FSF's Working Together for Free Software Fund.
<!-- .element: class="small" -->
Note:
**Do:** Leave this up during Q&A.
- Joel Spolsky's article (2003) — the classic intro, entertaining
- glibc manual — authoritative API reference (sourceware.org/glibc/manual)
- **GNU Toolchain Fund** = part of the **FSF's** (Free Software Foundation) "Working Together for Free Software" fund
+7 -8
View File
@@ -27,17 +27,16 @@ interface Props {
const { name, role, bio, avatarUrl } = Astro.props; const { name, role, bio, avatarUrl } = Astro.props;
const about: Skill[] = [ const about: Skill[] = [
{ icon: "cloud", title: "Hybrid Cloud", desc: "Building infrastructure at Red Hat", svgPath: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>' }, { icon: "cloud", title: "Cloud Native", desc: "Leading Builds for OpenShift at Red Hat", svgPath: '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>' },
{ icon: "monitor", title: "Linux Enthusiast", desc: "Fedora daily driver, Arch tinkerer", svgPath: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>' }, { icon: "cpu", title: "Kernel & Toolchain", desc: "Linux kernel, GCC & glibc contributor", svgPath: '<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>' },
{ icon: "code", title: "Open Source", desc: "GSoC/GSoD mentor and contributor", svgPath: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>' }, { icon: "code", title: "Open Source", desc: "GSoC alumnus & mentor, Campus Expert", svgPath: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>' },
{ icon: "server", title: "Homelab", desc: "Self-hosting on Raspberry Pi", svgPath: '<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>' }, { icon: "server", title: "Self-hosting", desc: "Fedora daily driver, homelab everything", svgPath: '<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>' },
]; ];
const tools: Skill[] = [ const tools: Skill[] = [
{ icon: "terminal", title: "Languages", desc: "Go, Python, Elm, C/C++", svgPath: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>' }, { icon: "terminal", title: "Languages", desc: "C/C++, Go, Bash", svgPath: '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>' },
{ icon: "pen-tool", title: "Editors", desc: "Neovim, VS Code", svgPath: '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>' }, { icon: "pen-tool", title: "Editor", desc: "Helix, Zellij, lazygit", svgPath: '<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/>' },
{ icon: "database", title: "Infra", desc: "Kubernetes, OpenShift, Tekton", svgPath: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>' }, { icon: "settings", title: "Platforms", desc: "Fedora, Git, CMake, GitHub Actions", svgPath: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>' },
{ icon: "flask", title: "Learning", desc: "Elm, functional programming", svgPath: '<path d="M9 3h6v7l5 8a2 2 0 0 1-1.7 3H5.7a2 2 0 0 1-1.7-3l5-8V3z"/><line x1="8" y1="3" x2="16" y2="3"/>' },
]; ];
const links: SocialLink[] = [ const links: SocialLink[] = [
+1 -1
View File
@@ -4,8 +4,8 @@ const navLinks: { href: string; label: string; external?: boolean }[] = [
{ href: "/posts", label: "Posts" }, { href: "/posts", label: "Posts" },
{ href: "/resume", label: "Resume" }, { href: "/resume", label: "Resume" },
{ href: "/events", label: "Events" }, { href: "/events", label: "Events" },
{ href: "/contributions", label: "Contributions" },
{ href: "/meeting", label: "Meet" }, { href: "/meeting", label: "Meet" },
{ href: "https://todo.avinal.space/explore", label: "Memos", external: true },
]; ];
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
+157
View File
@@ -0,0 +1,157 @@
---
import type { CollectionEntry } from "astro:content";
interface Props {
posts: CollectionEntry<"posts">[];
}
const { posts } = Astro.props;
const fmtDate = (d: Date) =>
d.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
---
<aside class="related" aria-label="Related posts">
<h2 class="related-heading">Related Posts</h2>
<ul class="related-list">
{posts.map((post) => (
<li class="related-item">
<a href={`/posts/${post.id}/`} class="related-link">
<div class="related-thumb">
{post.data.image ? (
<img src={post.data.image} alt={post.data.title} class="thumb-img" loading="lazy" decoding="async" />
) : (
<span class="thumb-placeholder">{post.data.title.charAt(0)}</span>
)}
</div>
<div class="related-info">
<div class="related-meta">
<span class="badge">{post.data.category}</span>
<span class="text-muted text-xs">{fmtDate(post.data.date)}</span>
</div>
<strong class="related-title">{post.data.title}</strong>
{post.data.description && (
<p class="related-desc">{post.data.description}</p>
)}
</div>
</a>
</li>
))}
</ul>
</aside>
<style>
.related {
border-top: 1px solid var(--border);
padding-top: var(--space-8);
margin-top: var(--space-10);
}
.related-heading {
font-size: var(--text-lg);
margin-bottom: var(--space-4);
}
.related-list {
display: flex;
flex-direction: column;
gap: 1px;
}
.related-item {
border-top: 1px solid var(--border);
}
.related-item:first-child {
border-top: none;
}
.related-link {
display: grid;
grid-template-columns: 140px 1fr;
gap: var(--space-4);
padding: var(--space-3) var(--space-2);
text-decoration: none;
color: inherit;
transition: background-color var(--duration-fast) var(--ease-out);
}
.related-link:hover {
background-color: var(--bg-surface-hover);
}
.related-thumb {
aspect-ratio: 3 / 2;
overflow: hidden;
background-color: var(--bg-surface-hover);
}
.thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
filter: grayscale(100%);
transition: filter var(--duration-normal) var(--ease-out);
}
.related-link:hover .thumb-img {
filter: grayscale(0%);
}
.thumb-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: var(--text-lg);
font-weight: 700;
color: var(--text-muted);
opacity: 0.3;
}
.related-info {
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.related-meta {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-2);
}
.related-title {
font-size: var(--text-base);
color: var(--text);
display: block;
}
.related-desc {
font-size: var(--text-sm);
color: var(--text-muted);
margin-top: var(--space-1);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (max-width: 600px) {
.related-link {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.related-thumb {
aspect-ratio: 16 / 9;
}
}
</style>
+88
View File
@@ -0,0 +1,88 @@
---
interface Props {
headings: { depth: number; slug: string; text: string }[];
}
const { headings } = Astro.props;
---
<details class="toc">
<summary class="toc-toggle">
<span class="toc-label">Table of Contents</span>
<svg class="toc-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</summary>
<nav class="toc-nav" aria-label="Table of contents">
<ol class="toc-list">
{headings.map((h) => (
<li class:list={[`toc-depth-${h.depth}`]}>
<a href={`#${h.slug}`} class="toc-link">{h.text}</a>
</li>
))}
</ol>
</nav>
</details>
<style>
.toc {
border: 1px solid var(--border);
padding: var(--space-4) var(--space-5);
margin-bottom: var(--space-8);
background-color: var(--bg-surface);
}
.toc-toggle {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
list-style: none;
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
}
.toc-toggle::-webkit-details-marker {
display: none;
}
.toc-chevron {
transform: rotate(-90deg);
transition: transform var(--duration-fast) var(--ease-out);
}
.toc[open] .toc-chevron {
transform: rotate(0deg);
}
.toc-nav {
margin-top: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--border);
}
.toc-list {
list-style: none;
padding: 0;
}
.toc-list li {
padding: var(--space-1) 0;
}
.toc-depth-3 {
padding-left: var(--space-4);
}
.toc-link {
font-size: var(--text-sm);
color: var(--text-secondary);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
.toc-link:hover {
color: var(--accent);
}
</style>
+2 -2
View File
@@ -107,8 +107,8 @@ const theme: ThemeConfig = {
}, },
fonts: { fonts: {
sans: '"Inter", "Segoe UI", system-ui, -apple-system, sans-serif', sans: '"Iosevka Aile", system-ui, sans-serif',
mono: '"JetBrains Mono", "Fira Code", ui-monospace, monospace', mono: '"Iosevka", ui-monospace, monospace',
}, },
colors: { colors: {
File diff suppressed because it is too large Load Diff
+42 -16
View File
@@ -1,4 +1,22 @@
[ [
{
"name": "DevConf.CZ 2026",
"date": "2026-06-18",
"location": "Brno, Czech Republic",
"role": "speaker",
"talk": "Lost in Transliteration: Why strlen(\"Dvořák\") Returns 8",
"description": "A deep dive into character encoding, Unicode, and how glibc's iconv handles text conversion internally — from the ASCII era through code pages to the gconv pipeline.",
"links": [
{
"label": "Slides",
"url": "/talks/devconf-2026/"
},
{
"label": "Event Page",
"url": "https://pretalx.devconf.info/devconf-cz-2026/talk/review/JVY7TPFMDDAPWUKHAPDH3MDKVZUXLDEW"
}
]
},
{ {
"name": "Opportunity Open Source Conference 2024", "name": "Opportunity Open Source Conference 2024",
"date": "2024-08-25", "date": "2024-08-25",
@@ -7,8 +25,14 @@
"talk": "From First Commit to Mentor: Climbing the Open Source Ladder", "talk": "From First Commit to Mentor: Climbing the Open Source Ladder",
"description": "Presented at the Canonical/Ubuntu conference on the journey from first-time contributor to mentor in open source. Covered practical advice on starting out, staying motivated, transitioning to mentorship, and fostering stronger communities. Included a deep-dive into my GSoC experience at FOSSology.", "description": "Presented at the Canonical/Ubuntu conference on the journey from first-time contributor to mentor in open source. Covered practical advice on starting out, staying motivated, transitioning to mentorship, and fostering stronger communities. Included a deep-dive into my GSoC experience at FOSSology.",
"links": [ "links": [
{ "label": "Event Page", "url": "https://events.canonical.com/event/89/contributions/485/" }, {
{ "label": "Slides", "url": "https://www.canva.com/design/DAGNthYwrck/Gt7mv99p6zH-aVbx2AtEuQ/view" } "label": "Event Page",
"url": "https://events.canonical.com/event/89/contributions/485/"
},
{
"label": "Slides",
"url": "https://www.canva.com/design/DAGNthYwrck/Gt7mv99p6zH-aVbx2AtEuQ/view"
}
] ]
}, },
{ {
@@ -18,8 +42,14 @@
"role": "mentor", "role": "mentor",
"description": "Represented the FOSSology project at the annual GSoC Mentor Summit hosted at Google's Sunnyvale campus. Participated in unconference sessions on mentoring practices, community building, and project sustainability.", "description": "Represented the FOSSology project at the annual GSoC Mentor Summit hosted at Google's Sunnyvale campus. Participated in unconference sessions on mentoring practices, community building, and project sustainability.",
"links": [ "links": [
{ "label": "GSoC", "url": "https://summerofcode.withgoogle.com/" }, {
{ "label": "FOSSology", "url": "https://www.fossology.org" } "label": "GSoC",
"url": "https://summerofcode.withgoogle.com/"
},
{
"label": "FOSSology",
"url": "https://www.fossology.org"
}
] ]
}, },
{ {
@@ -29,17 +59,10 @@
"role": "organizer", "role": "organizer",
"description": "Organized and led a month-long series of talks and workshops as a GitHub Campus Expert. Topics ranged from introduction to open source, Git workflows, and writing GSoC proposals to lightning talks by contributors from projects like Julia and VideoLAN. Over 200 students participated across 7 sessions.", "description": "Organized and led a month-long series of talks and workshops as a GitHub Campus Expert. Topics ranged from introduction to open source, Git workflows, and writing GSoC proposals to lightning talks by contributors from projects like Julia and VideoLAN. Over 200 students participated across 7 sessions.",
"links": [ "links": [
{ "label": "Campus Expert Profile", "url": "https://githubcampus.expert/avinal/" } {
] "label": "Campus Expert Profile",
}, "url": "https://githubcampus.expert/avinal/"
{ }
"name": "GitHub Universe 2021",
"date": "2021-10-27",
"location": "Virtual",
"role": "attendee",
"description": "Attended GitHub's annual developer conference as a GitHub Campus Expert. Explored new features including Copilot, Codespaces, and Actions improvements.",
"links": [
{ "label": "GitHub Universe", "url": "https://githubuniverse.com/" }
] ]
}, },
{ {
@@ -50,7 +73,10 @@
"talk": "Campus Experts: Preparation, Application Tips, Perks & Benefits", "talk": "Campus Experts: Preparation, Application Tips, Perks & Benefits",
"description": "Delivered a live session on how to become a GitHub Campus Expert — covering the application process, preparation tips, community benefits, and what to expect from the program.", "description": "Delivered a live session on how to become a GitHub Campus Expert — covering the application process, preparation tips, community benefits, and what to expect from the program.",
"links": [ "links": [
{ "label": "Campus Expert Profile", "url": "https://githubcampus.expert/avinal/" } {
"label": "Campus Expert Profile",
"url": "https://githubcampus.expert/avinal/"
}
] ]
} }
] ]
+4 -5
View File
@@ -35,15 +35,14 @@
] ]
}, },
{ {
"name": "memodav", "name": "sciezka",
"owner": "avinal", "owner": "avinal",
"description": "Bidirectional task sync between Memos and CalDAV. Write tasks in Memos, check them off in any CalDAV client.", "description": "A browser extension for fuzzy searching tabs, history and bookmarks.",
"url": "https://github.com/avinal/memodav", "url": "https://github.com/avinal/sciezka",
"stars": 0, "stars": 0,
"forks": 0, "forks": 0,
"languages": [ "languages": [
{ "name": "Go", "color": "#00ADD8" }, { "name": "TypeScript", "color": "#3178C6" }
{ "name": "Shell", "color": "#89e051" }
] ]
}, },
{ {
+28 -11
View File
@@ -142,8 +142,9 @@
"Produced step-by-step tutorials with annotated screenshots, making the project accessible to new contributors and end users alike" "Produced step-by-step tutorials with annotated screenshots, making the project accessible to new contributors and end users alike"
], ],
"links": [ "links": [
{ "label": "GSoD Case Study", "url": "https://developers.google.com/season-of-docs" }, { "label": "Docs Website", "url": "https://docs.videolan.me/vlc-user/android/3.X/en/index.html" },
{ "label": "VideoLAN", "url": "https://www.videolan.org" } { "label": "Docs Repository", "url": "https://code.videolan.org/docs/vlc-user/-/tree/android/3.X?ref_type=heads" },
{ "label": "GSoD Report", "url": "/posts/blogs/gsod2020-report" }
] ]
}, },
{ {
@@ -162,18 +163,34 @@
] ]
}, },
{ {
"name": "GNU C Library", "name": "GNU Toolchain",
"position": "Contributor", "position": "Contributor",
"url": "https://sourceware.org/glibc/", "url": "https://gcc.gnu.org/",
"startDate": "2024-05-01", "startDate": "2024-05-01",
"summary": "GNU Project", "summary": "GNU Project — GCC & GNU C Library",
"location": "Remote", "location": "Remote",
"highlights": [ "highlights": [
"Contributing patches to glibc — one of the most critical pieces of the GNU/Linux ecosystem, used by virtually every Linux distribution", "Contributing patches to GCC and GNU C Library — two of the most critical pieces of the GNU/Linux ecosystem, used by virtually every Linux distribution",
"Working on bug fixes and improvements submitted via the Sourceware mailing list and reviewed by core glibc maintainers" "Working on compiler internals (tree-ssa-strlen optimizations) and C library bug fixes submitted via Sourceware and reviewed by core maintainers"
], ],
"links": [ "links": [
{ "label": "Patches", "url": "https://sourceware.org/cgit/glibc/log/?qt=author&q=avinal" } { "label": "GNU C Library Patches", "url": "https://sourceware.org/cgit/glibc/log/?qt=author&q=avinal" },
{ "label": "GCC Patches", "url": "https://gitlab.com/gnutools/gcc/-/commits/master?author=Avinal%20Kumar" }
]
},
{
"name": "Linux Kernel",
"position": "Contributor",
"url": "https://kernel.org/",
"startDate": "2026-04-01",
"summary": "DRM Subsystem",
"location": "Remote",
"highlights": [
"Contributing patches to the Linux kernel DRM subsystem — adding new MIPI DSI helper functions and migrating panel drivers to improved APIs",
"Patches reviewed and merged via the freedesktop.org GitLab DRM tree"
],
"links": [
{ "label": "Patches", "url": "https://gitlab.freedesktop.org/drm/misc/kernel/-/commits/drm-misc-next?author=Avinal%20Kumar" }
] ]
} }
], ],
@@ -433,9 +450,9 @@
"description": "C++ implementation of the Blowfish and Blowfish 2 symmetric block cipher with tests and APIs. Drop-in replacement for DES or IDEA." "description": "C++ implementation of the Blowfish and Blowfish 2 symmetric block cipher with tests and APIs. Drop-in replacement for DES or IDEA."
}, },
{ {
"name": "Xeus-BASIC", "name": "Sciezka",
"url": "https://github.com/avinal/xeus-basic", "url": "https://github.com/avinal/sciezka",
"description": "Jupyter Kernel for the BASIC language built using the Xeus Framework, C, and C++. Executes BASIC programs line by line in Jupyter Notebook." "description": "Browser extension for fuzzy searching tabs, history and bookmarks. Built with TypeScript."
} }
], ],
"meta": { "meta": {
+7 -4
View File
@@ -3,6 +3,13 @@ import Nav from "@/components/Nav.astro";
import Footer from "@/components/Footer.astro"; import Footer from "@/components/Footer.astro";
import theme, { generateThemeCSS } from "@/config/theme"; import theme, { generateThemeCSS } from "@/config/theme";
import "@/styles/global.css"; import "@/styles/global.css";
import "@fontsource/iosevka-aile/400.css";
import "@fontsource/iosevka-aile/500.css";
import "@fontsource/iosevka-aile/600.css";
import "@fontsource/iosevka-aile/700.css";
import "@fontsource/iosevka-aile/800.css";
import "@fontsource/iosevka/400.css";
import "@fontsource/iosevka/500.css";
interface Props { interface Props {
title: string; title: string;
@@ -56,10 +63,6 @@ const resolvedOgImage = ogImage || new URL("/og-default.svg", Astro.site).href;
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<link rel="alternate" type="application/rss+xml" title="Avinal Kumar" href="/rss.xml" /> <link rel="alternate" type="application/rss+xml" title="Avinal Kumar" href="/rss.xml" />
<!-- Font preloads -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<!-- Design tokens generated from theme config --> <!-- Design tokens generated from theme config -->
<style set:html={themeCSS} /> <style set:html={themeCSS} />
+41
View File
@@ -1,5 +1,8 @@
--- ---
import BaseLayout from "./BaseLayout.astro"; import BaseLayout from "./BaseLayout.astro";
import TableOfContents from "@/components/TableOfContents.astro";
import RelatedPosts from "@/components/RelatedPosts.astro";
import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
title: string; title: string;
@@ -10,6 +13,8 @@ interface Props {
tags?: string[]; tags?: string[];
image?: string; image?: string;
readingTime: string; readingTime: string;
headings?: { depth: number; slug: string; text: string }[];
relatedPosts?: CollectionEntry<"posts">[];
} }
const { const {
@@ -21,6 +26,8 @@ const {
tags = [], tags = [],
image, image,
readingTime, readingTime,
headings = [],
relatedPosts = [],
} = Astro.props; } = Astro.props;
const fmtDate = (d: Date) => const fmtDate = (d: Date) =>
@@ -49,6 +56,7 @@ const blogPostingLd = {
--- ---
<BaseLayout title={title} description={description} ogImage={image || undefined} jsonLd={blogPostingLd}> <BaseLayout title={title} description={description} ogImage={image || undefined} jsonLd={blogPostingLd}>
<div class="reading-progress" aria-hidden="true"></div>
<article class="post-page"> <article class="post-page">
{image && ( {image && (
<div class="post-hero-img"> <div class="post-hero-img">
@@ -83,9 +91,13 @@ const blogPostingLd = {
)} )}
</header> </header>
{headings.length > 0 && <TableOfContents headings={headings} />}
<div class="prose"> <div class="prose">
<slot /> <slot />
</div> </div>
{relatedPosts.length > 0 && <RelatedPosts posts={relatedPosts} />}
</article> </article>
</BaseLayout> </BaseLayout>
@@ -114,6 +126,7 @@ const blogPostingLd = {
margin-bottom: var(--space-10); margin-bottom: var(--space-10);
padding-bottom: var(--space-6); padding-bottom: var(--space-6);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
text-align: center;
} }
.post-category { .post-category {
@@ -136,6 +149,7 @@ const blogPostingLd = {
.post-meta-row { .post-meta-row {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: var(--space-2); gap: var(--space-2);
font-size: var(--text-sm); font-size: var(--text-sm);
@@ -153,6 +167,7 @@ const blogPostingLd = {
.post-tags { .post-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
gap: var(--space-2); gap: var(--space-2);
margin-top: var(--space-3); margin-top: var(--space-3);
} }
@@ -165,4 +180,30 @@ const blogPostingLd = {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.reading-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
width: 0%;
background-color: var(--accent);
z-index: 200;
pointer-events: none;
}
</style> </style>
<script>
const bar = document.querySelector<HTMLElement>('.reading-progress');
const article = document.querySelector('.post-page');
if (bar && article) {
const update = () => {
const total = article.scrollHeight - window.innerHeight;
const scrolled = window.scrollY - (article as HTMLElement).offsetTop;
const pct = Math.min(100, Math.max(0, (scrolled / total) * 100));
bar.style.width = `${pct}%`;
};
window.addEventListener('scroll', update, { passive: true });
update();
}
</script>
+606
View File
@@ -0,0 +1,606 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import contribData from "@/data/contributions.json";
interface Contribution {
project: string;
projectUrl: string;
platform: string;
type: string;
kind: string;
title: string;
url: string;
date: string;
status?: string;
description?: string;
relatedIssue?: string;
}
const contributions = (contribData as Contribution[]).sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
const totalCount = contributions.length;
const projects = [...new Set(contributions.map((c) => c.project))];
const types = [...new Set(contributions.map((c) => c.type))];
const kinds = [...new Set(contributions.map((c) => c.kind))];
const platforms = [...new Set(contributions.map((c) => c.platform))];
const mergedCount = contributions.filter((c) => c.status === "merged").length;
function fmtDate(iso: string) {
const d = new Date(iso);
return `${d.getDate()} ${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`;
}
const typeLabels: Record<string, string> = {
code: "Code",
docs: "Docs",
infra: "Infra",
};
const kindLabels: Record<string, string> = {
pr: "PR",
issue: "Issue",
commit: "Commit",
patch: "Patch",
};
const statusColors: Record<string, string> = {
merged: "badge-merged",
open: "badge-open",
closed: "badge-closed",
};
const platformIcons: Record<string, string> = {
github: `<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>`,
gitlab: `<path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/>`,
sourceware: `<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>`,
gerrit: `<path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/>`,
other: `<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>`,
};
function platformIcon(p: string): string {
return platformIcons[p] ?? platformIcons.other;
}
const collectionLd = {
"@context": "https://schema.org",
"@type": "CollectionPage",
name: "Open Source Contributions",
description: "Open source contributions across GitHub, GitLab, Sourceware, and other platforms.",
url: "https://avinal.space/contributions",
author: {
"@type": "Person",
name: "Avinal Kumar",
url: "https://avinal.space",
},
};
---
<BaseLayout
title="Contributions"
description="Open source contributions across GitHub, GitLab, Sourceware, and other platforms"
jsonLd={collectionLd}
>
<div class="contrib-page">
<header class="contrib-header">
<h1>Contributions</h1>
<p class="contrib-desc">
A curated selection of my open source contributions across projects and platforms. This list represents only a portion of my overall work — PRs, patches, commits, and issues that I consider meaningful.
</p>
</header>
<div class="stats-bar" id="stats-bar">
<div class="stat stat-contributions">
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/></svg>
<span class="stat-value" id="stat-showing">{totalCount}</span>
<span class="stat-label">contributions</span>
</div>
<div class="stat stat-projects">
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span class="stat-value">{projects.length}</span>
<span class="stat-label">projects</span>
</div>
<div class="stat stat-merged">
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></svg>
<span class="stat-value">{mergedCount}</span>
<span class="stat-label">merged</span>
</div>
<div class="stat stat-platforms">
<svg class="stat-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<span class="stat-value">{platforms.length}</span>
<span class="stat-label">platforms</span>
</div>
</div>
<div class="filter-bar" id="filter-bar">
<select class="filter-select" data-filter="project" aria-label="Filter by project">
<option value="">All projects</option>
{projects.sort().map((p) => (
<option value={p}>{p}</option>
))}
</select>
<select class="filter-select" data-filter="type" aria-label="Filter by type">
<option value="">All types</option>
{types.map((t) => (
<option value={t}>{typeLabels[t] ?? t}</option>
))}
</select>
<select class="filter-select" data-filter="kind" aria-label="Filter by kind">
<option value="">All kinds</option>
{kinds.map((k) => (
<option value={k}>{kindLabels[k] ?? k}</option>
))}
</select>
<select class="filter-select" data-filter="platform" aria-label="Filter by platform">
<option value="">All platforms</option>
{platforms.map((p) => (
<option value={p}>{p}</option>
))}
</select>
<button class="filter-clear" id="clear-filters" style="display:none;">Clear</button>
</div>
<div class="timeline" id="timeline">
{contributions.map((c) => (
<div
class={`tl-entry ct-${c.type}`}
data-project={c.project}
data-type={c.type}
data-kind={c.kind}
data-platform={c.platform}
>
<div class="tl-label-cell">
<span class="tl-type-label">{typeLabels[c.type] ?? c.type}</span>
</div>
<div class="tl-rail-cell">
<div class="tl-dot" />
</div>
<div class="tl-card">
<div class="tl-card-header">
<div class="tl-dates">
{fmtDate(c.date)}
<span class="tl-platform">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><Fragment set:html={platformIcon(c.platform)} /></svg>
<a href={c.projectUrl} class="project-link" target="_blank" rel="noopener noreferrer">{c.project}</a>
</span>
</div>
<h3 class="tl-title">
<a href={c.url} class="contrib-link" target="_blank" rel="noopener noreferrer">{c.title}</a>
</h3>
</div>
<div class="badge-row">
{c.status && (
<span class={`badge ${statusColors[c.status] ?? ""}`}>{c.status}</span>
)}
<span class="badge badge-kind">{kindLabels[c.kind] ?? c.kind}</span>
</div>
{c.description && <p class="contrib-description">{c.description}</p>}
{c.relatedIssue && (
<a href={c.relatedIssue} class="related-issue-link" target="_blank" rel="noopener noreferrer">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
Related issue
</a>
)}
</div>
</div>
))}
</div>
<p class="empty-state" id="empty-state" style="display:none;">
No contributions match the current filters.
</p>
</div>
</BaseLayout>
<style>
.contrib-page {
max-width: var(--max-w-page);
margin-inline: auto;
}
.contrib-header {
margin-bottom: var(--space-8);
}
.contrib-header h1 {
margin-bottom: var(--space-3);
}
.contrib-desc {
font-size: var(--text-lg);
color: var(--text-secondary);
line-height: var(--leading-relaxed);
max-width: var(--max-w-prose);
}
/* ---- Stats ---- */
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
margin-bottom: var(--space-6);
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-3);
border: 1px solid var(--border);
background: var(--bg-surface);
transition: border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.stat:hover {
border-color: var(--stat-color);
box-shadow: var(--shadow);
}
.stat-icon {
opacity: 0.4;
transition: opacity var(--duration-fast) var(--ease-out),
color var(--duration-fast) var(--ease-out);
}
.stat:hover .stat-icon {
opacity: 1;
color: var(--stat-color);
}
.stat-contributions { --stat-color: var(--accent); }
.stat-projects { --stat-color: #8b5cf6; }
.stat-merged { --stat-color: #10b981; }
.stat-platforms { --stat-color: #f59e0b; }
.stat-value {
font-size: var(--text-2xl);
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--text);
line-height: 1;
transition: color var(--duration-fast) var(--ease-out);
}
.stat:hover .stat-value {
color: var(--stat-color);
}
.stat-label {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ---- Filters ---- */
.filter-bar {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
margin-bottom: var(--space-8);
}
.filter-select {
font-family: inherit;
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-secondary);
background: var(--bg-surface);
border: 1px solid var(--border);
padding: var(--space-2) var(--space-3);
padding-right: var(--space-6);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23737373' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
transition: border-color var(--duration-fast) var(--ease-out);
}
.filter-select:hover,
.filter-select:focus-visible {
border-color: var(--accent);
outline: none;
}
.filter-select.has-value {
color: var(--text);
border-color: var(--accent);
}
.filter-clear {
font-family: inherit;
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
background: transparent;
border: 1px solid var(--border);
padding: var(--space-2) var(--space-3);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-out);
}
.filter-clear:hover {
color: var(--text);
border-color: var(--text-muted);
}
/* ---- Timeline ---- */
.timeline {
display: flex;
flex-direction: column;
gap: var(--space-1);
position: relative;
}
.tl-entry {
display: grid;
grid-template-columns: 64px 24px 1fr;
gap: 0 var(--space-2);
position: relative;
}
.tl-entry[data-hidden] {
display: none;
}
.tl-label-cell {
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding-top: var(--space-5);
padding-right: var(--space-2);
}
.tl-type-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
white-space: nowrap;
}
.ct-code .tl-type-label { color: var(--accent); }
.ct-docs .tl-type-label { color: #10b981; }
.ct-infra .tl-type-label { color: #f59e0b; }
.tl-rail-cell {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.tl-dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--accent);
background: var(--bg);
position: relative;
z-index: 2;
margin-top: var(--space-5);
flex-shrink: 0;
}
.ct-code .tl-dot { border-color: var(--accent); }
.ct-docs .tl-dot { border-color: #10b981; }
.ct-infra .tl-dot { border-color: #f59e0b; }
.tl-rail-cell::after {
content: "";
position: absolute;
top: calc(var(--space-5) + 12px);
bottom: 0;
left: 50%;
width: 2px;
background: var(--border);
transform: translateX(-50%);
z-index: 1;
}
.tl-entry:last-child .tl-rail-cell::after { display: none; }
.tl-card {
padding: var(--space-4) var(--space-5);
border: 1px solid var(--border);
background: var(--bg-surface);
transition: border-color var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.tl-card:hover {
border-color: var(--border-strong);
box-shadow: var(--shadow);
}
.tl-dates {
display: flex;
align-items: center;
gap: var(--space-3);
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
margin-bottom: var(--space-1);
font-variant-numeric: tabular-nums;
flex-wrap: wrap;
}
.tl-platform {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--text-xs);
color: var(--text-muted);
}
.project-link {
color: var(--text-muted);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
.project-link:hover {
color: var(--accent);
}
.tl-title {
font-size: var(--text-base);
font-weight: 600;
margin-bottom: var(--space-2);
}
.contrib-link {
color: var(--text);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
.contrib-link:hover {
color: var(--accent);
}
.badge-row {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-2);
}
.badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 2px var(--space-2);
border: 1px solid var(--border);
color: var(--text-muted);
}
.badge-merged {
color: #10b981;
border-color: #10b981;
background: color-mix(in srgb, #10b981 10%, transparent);
}
.badge-open {
color: #f59e0b;
border-color: #f59e0b;
background: color-mix(in srgb, #f59e0b 10%, transparent);
}
.badge-closed {
color: var(--text-muted);
border-color: var(--border);
background: var(--bg-surface);
}
.badge-kind {
color: var(--text-secondary);
}
.contrib-description {
font-size: var(--text-sm);
color: var(--text-secondary);
line-height: var(--leading-relaxed);
}
.related-issue-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-muted);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
.related-issue-link:hover {
color: var(--accent);
}
.empty-state {
text-align: center;
padding: var(--space-10) 0;
font-size: var(--text-lg);
color: var(--text-muted);
}
/* ---- Responsive ---- */
@media (max-width: 768px) {
.stats-bar {
grid-template-columns: repeat(2, 1fr);
}
.tl-entry {
grid-template-columns: 48px 16px 1fr;
}
.tl-type-label { font-size: 8px; }
.tl-dot {
width: 10px;
height: 10px;
}
.tl-dates {
flex-direction: column;
align-items: flex-start;
gap: var(--space-1);
}
}
</style>
<script>
const selects = document.querySelectorAll<HTMLSelectElement>(".filter-select");
const entries = document.querySelectorAll<HTMLElement>(".tl-entry[data-project]");
const clearBtn = document.getElementById("clear-filters") as HTMLButtonElement;
const showingStat = document.getElementById("stat-showing");
const emptyState = document.getElementById("empty-state");
const timeline = document.getElementById("timeline");
function applyFilters() {
const filters: Record<string, string> = {};
selects.forEach((s) => {
if (s.value) filters[s.dataset.filter!] = s.value;
s.classList.toggle("has-value", !!s.value);
});
let visible = 0;
entries.forEach((el) => {
const match = Object.entries(filters).every(
([dim, val]) => el.dataset[dim] === val,
);
if (match) {
el.removeAttribute("data-hidden");
visible++;
} else {
el.setAttribute("data-hidden", "");
}
});
if (showingStat) showingStat.textContent = String(visible);
if (emptyState) emptyState.style.display = visible === 0 ? "" : "none";
if (timeline) timeline.style.display = visible === 0 ? "none" : "";
if (clearBtn) clearBtn.style.display = Object.keys(filters).length > 0 ? "" : "none";
}
selects.forEach((s) => s.addEventListener("change", applyFilters));
if (clearBtn) {
clearBtn.addEventListener("click", () => {
selects.forEach((s) => { s.value = ""; s.classList.remove("has-value"); });
applyFilters();
});
}
</script>
+1 -1
View File
@@ -50,7 +50,7 @@ const personLd = {
<HeroCard <HeroCard
name="Avinal Kumar" name="Avinal Kumar"
role="Software Engineer II at Red Hat" role="Software Engineer II at Red Hat"
bio="I build things for hybrid cloud, contribute to open source, and self-host everything I can. GNU/Linux and free software are two of my favorite things." bio="Leading OpenShift Builds at Red Hat by day, contributing to the Linux kernel and GNU toolchain by night. Free software advocate, self-hoster, and GSoC mentor."
avatarUrl={user?.avatar_url} avatarUrl={user?.avatar_url}
/> />
</div> </div>
+18 -1
View File
@@ -11,11 +11,26 @@ export async function getStaticPaths() {
} }
const { post } = Astro.props; const { post } = Astro.props;
const { Content } = await render(post); const { Content, headings } = await render(post);
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
const wordCount = post.body?.split(/\s+/).length ?? 0; const wordCount = post.body?.split(/\s+/).length ?? 0;
const minutes = Math.max(1, Math.round(wordCount / 220)); const minutes = Math.max(1, Math.round(wordCount / 220));
const readingTime = `${minutes} min read`; const readingTime = `${minutes} min read`;
const allPosts = await getCollection("posts", ({ data }) => !data.draft);
const relatedPosts = allPosts
.filter((p) => p.id !== post.id)
.map((p) => {
let score = 0;
if (p.data.category === post.data.category) score += 10;
score += p.data.tags.filter((t) => post.data.tags.includes(t)).length * 3;
return { post: p, score };
})
.filter((s) => s.score > 0)
.sort((a, b) => b.score - a.score || b.post.data.date.getTime() - a.post.data.date.getTime())
.slice(0, 3)
.map((s) => s.post);
--- ---
<PostLayout <PostLayout
@@ -27,6 +42,8 @@ const readingTime = `${minutes} min read`;
tags={post.data.tags} tags={post.data.tags}
image={post.data.image} image={post.data.image}
readingTime={readingTime} readingTime={readingTime}
headings={tocHeadings}
relatedPosts={relatedPosts}
> >
<Content /> <Content />
</PostLayout> </PostLayout>