mirror of
https://github.com/avinal/avinal.github.io.git
synced 2026-07-03 23:30:09 +05:30
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d9e1a696 | |||
| 5df0a73c08 | |||
| a8c7e06330 | |||
| 96ea6019ae | |||
|
63ab0e12b2
|
|||
|
df4f2e3863
|
|||
|
f03f57f064
|
|||
|
4f942563c1
|
|||
|
f5e739494a
|
|||
|
5f467665bc
|
|||
|
99f3fb5ec8
|
|||
|
5fa9a10203
|
|||
|
f613005a23
|
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
site: "https://avinal.space",
|
||||
output: "static",
|
||||
integrations: [sitemap()],
|
||||
prefetch: true,
|
||||
markdown: {
|
||||
shikiConfig: {
|
||||
theme: "github-dark-default",
|
||||
|
||||
@@ -14,3 +14,8 @@
|
||||
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
|
||||
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;"
|
||||
|
||||
[[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';"
|
||||
|
||||
Generated
+468
-1088
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -21,9 +21,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/avinal/avinal.github.io#readme",
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.15",
|
||||
"@astrojs/sitemap": "^3.7.0",
|
||||
"astro": "^5.17.3",
|
||||
"@astrojs/rss": "^4.0.18",
|
||||
"@astrojs/sitemap": "^3.7.2",
|
||||
"@fontsource/iosevka": "^5.2.5",
|
||||
"@fontsource/iosevka-aile": "^5.2.5",
|
||||
"astro": "^6.3.5",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
|
||||
@@ -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>
|
||||
@@ -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+0000–U+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 (0–127)
|
||||
- 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+0000–U+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>
|
||||
|
||||
- 1–4 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: 44 76 6F <span class="red" style="font-weight:700;">C5 99</span> C3 A1 6B <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 <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 0–127; 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
|
||||
@@ -27,17 +27,16 @@ interface Props {
|
||||
const { name, role, bio, avatarUrl } = Astro.props;
|
||||
|
||||
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: "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: "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: "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: "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: "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 alumnus & mentor, Campus Expert", svgPath: '<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 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[] = [
|
||||
{ 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: "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: "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: "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"/>' },
|
||||
{ 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: "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: "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"/>' },
|
||||
];
|
||||
|
||||
const links: SocialLink[] = [
|
||||
|
||||
@@ -4,8 +4,8 @@ const navLinks: { href: string; label: string; external?: boolean }[] = [
|
||||
{ href: "/posts", label: "Posts" },
|
||||
{ href: "/resume", label: "Resume" },
|
||||
{ href: "/events", label: "Events" },
|
||||
{ href: "/contributions", label: "Contributions" },
|
||||
{ href: "/meeting", label: "Meet" },
|
||||
{ href: "https://todo.avinal.space/explore", label: "Memos", external: true },
|
||||
];
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -107,8 +107,8 @@ const theme: ThemeConfig = {
|
||||
},
|
||||
|
||||
fonts: {
|
||||
sans: '"Inter", "Segoe UI", system-ui, -apple-system, sans-serif',
|
||||
mono: '"JetBrains Mono", "Fira Code", ui-monospace, monospace',
|
||||
sans: '"Iosevka Aile", system-ui, sans-serif',
|
||||
mono: '"Iosevka", ui-monospace, monospace',
|
||||
},
|
||||
|
||||
colors: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+42
-16
@@ -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",
|
||||
"date": "2024-08-25",
|
||||
@@ -7,8 +25,14 @@
|
||||
"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.",
|
||||
"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",
|
||||
"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": [
|
||||
{ "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",
|
||||
"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": [
|
||||
{ "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/" }
|
||||
{
|
||||
"label": "Campus Expert Profile",
|
||||
"url": "https://githubcampus.expert/avinal/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -50,7 +73,10 @@
|
||||
"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.",
|
||||
"links": [
|
||||
{ "label": "Campus Expert Profile", "url": "https://githubcampus.expert/avinal/" }
|
||||
{
|
||||
"label": "Campus Expert Profile",
|
||||
"url": "https://githubcampus.expert/avinal/"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+4
-5
@@ -35,15 +35,14 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "memodav",
|
||||
"name": "sciezka",
|
||||
"owner": "avinal",
|
||||
"description": "Bidirectional task sync between Memos and CalDAV. Write tasks in Memos, check them off in any CalDAV client.",
|
||||
"url": "https://github.com/avinal/memodav",
|
||||
"description": "A browser extension for fuzzy searching tabs, history and bookmarks.",
|
||||
"url": "https://github.com/avinal/sciezka",
|
||||
"stars": 0,
|
||||
"forks": 0,
|
||||
"languages": [
|
||||
{ "name": "Go", "color": "#00ADD8" },
|
||||
{ "name": "Shell", "color": "#89e051" }
|
||||
{ "name": "TypeScript", "color": "#3178C6" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+28
-11
@@ -142,8 +142,9 @@
|
||||
"Produced step-by-step tutorials with annotated screenshots, making the project accessible to new contributors and end users alike"
|
||||
],
|
||||
"links": [
|
||||
{ "label": "GSoD Case Study", "url": "https://developers.google.com/season-of-docs" },
|
||||
{ "label": "VideoLAN", "url": "https://www.videolan.org" }
|
||||
{ "label": "Docs Website", "url": "https://docs.videolan.me/vlc-user/android/3.X/en/index.html" },
|
||||
{ "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",
|
||||
"url": "https://sourceware.org/glibc/",
|
||||
"url": "https://gcc.gnu.org/",
|
||||
"startDate": "2024-05-01",
|
||||
"summary": "GNU Project",
|
||||
"summary": "GNU Project — GCC & GNU C Library",
|
||||
"location": "Remote",
|
||||
"highlights": [
|
||||
"Contributing patches to glibc — one 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"
|
||||
"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 compiler internals (tree-ssa-strlen optimizations) and C library bug fixes submitted via Sourceware and reviewed by core maintainers"
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"name": "Xeus-BASIC",
|
||||
"url": "https://github.com/avinal/xeus-basic",
|
||||
"description": "Jupyter Kernel for the BASIC language built using the Xeus Framework, C, and C++. Executes BASIC programs line by line in Jupyter Notebook."
|
||||
"name": "Sciezka",
|
||||
"url": "https://github.com/avinal/sciezka",
|
||||
"description": "Browser extension for fuzzy searching tabs, history and bookmarks. Built with TypeScript."
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
|
||||
@@ -3,6 +3,13 @@ import Nav from "@/components/Nav.astro";
|
||||
import Footer from "@/components/Footer.astro";
|
||||
import theme, { generateThemeCSS } from "@/config/theme";
|
||||
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 {
|
||||
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="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 -->
|
||||
<style set:html={themeCSS} />
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
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 {
|
||||
title: string;
|
||||
@@ -10,6 +13,8 @@ interface Props {
|
||||
tags?: string[];
|
||||
image?: string;
|
||||
readingTime: string;
|
||||
headings?: { depth: number; slug: string; text: string }[];
|
||||
relatedPosts?: CollectionEntry<"posts">[];
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -21,6 +26,8 @@ const {
|
||||
tags = [],
|
||||
image,
|
||||
readingTime,
|
||||
headings = [],
|
||||
relatedPosts = [],
|
||||
} = Astro.props;
|
||||
|
||||
const fmtDate = (d: Date) =>
|
||||
@@ -49,6 +56,7 @@ const blogPostingLd = {
|
||||
---
|
||||
|
||||
<BaseLayout title={title} description={description} ogImage={image || undefined} jsonLd={blogPostingLd}>
|
||||
<div class="reading-progress" aria-hidden="true"></div>
|
||||
<article class="post-page">
|
||||
{image && (
|
||||
<div class="post-hero-img">
|
||||
@@ -83,9 +91,13 @@ const blogPostingLd = {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{headings.length > 0 && <TableOfContents headings={headings} />}
|
||||
|
||||
<div class="prose">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{relatedPosts.length > 0 && <RelatedPosts posts={relatedPosts} />}
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -114,6 +126,7 @@ const blogPostingLd = {
|
||||
margin-bottom: var(--space-10);
|
||||
padding-bottom: var(--space-6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-category {
|
||||
@@ -136,6 +149,7 @@ const blogPostingLd = {
|
||||
.post-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
@@ -153,6 +167,7 @@ const blogPostingLd = {
|
||||
.post-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
@@ -165,4 +180,30 @@ const blogPostingLd = {
|
||||
border-radius: var(--radius-sm);
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -50,7 +50,7 @@ const personLd = {
|
||||
<HeroCard
|
||||
name="Avinal Kumar"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,26 @@ export async function getStaticPaths() {
|
||||
}
|
||||
|
||||
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 minutes = Math.max(1, Math.round(wordCount / 220));
|
||||
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
|
||||
@@ -27,6 +42,8 @@ const readingTime = `${minutes} min read`;
|
||||
tags={post.data.tags}
|
||||
image={post.data.image}
|
||||
readingTime={readingTime}
|
||||
headings={tocHeadings}
|
||||
relatedPosts={relatedPosts}
|
||||
>
|
||||
<Content />
|
||||
</PostLayout>
|
||||
|
||||
Reference in New Issue
Block a user