commit 9703368d1f6b4ee0d0d6e5e89fb2c31892c4a616 from: Murilo Ijanc date: Fri Apr 10 21:53:18 2026 UTC add optional toc generation, example site commit - de29d8401122b1fa37504de6b7f496c99187b2b3 commit + 9703368d1f6b4ee0d0d6e5e89fb2c31892c4a616 blob - /dev/null blob + 301d067fce13beb1fe42c352ac18f3921088ebc7 (mode 644) --- /dev/null +++ example/content/about.md @@ -0,0 +1,9 @@ +--- +title: sobre +description: sobre este site +--- + +# sobre + +site gerado com [kssg](https://got.ijanc.org/?action=summary&path=kssg.git), +um gerador de sites estaticos escrito em rust. blob - /dev/null blob + aa479542f0d7fdeb406d7b48ec1e56ff1140c149 (mode 644) --- /dev/null +++ example/content/index.md @@ -0,0 +1,8 @@ +--- +title: inicio +description: site de exemplo do kssg +--- + +# bem-vindo + +este e um site de exemplo para testar o **kssg**. blob - /dev/null blob + 6a0c37c6a95be774971a298ee4888391dc9822e7 (mode 644) --- /dev/null +++ example/content/posts/hello-world.md @@ -0,0 +1,40 @@ +--- +title: hello world +date: 2026-04-01 +description: primeiro post de exemplo +tags: exemplo, kssg +toc: true +--- + +# hello world + +este e o primeiro post de exemplo. + +## instalacao + +compile com `make` e instale com `make install`: + +``` +make +make install PREFIX=~/.local +``` + +## uso + +gere o site com `kssg build` e visualize com `kssg serve`. + +### build + +``` +kssg build +``` + +### serve + +``` +kssg serve +``` + +## conclusao + +pronto, o site esta no ar. blob - /dev/null blob + f2a9a570f47d9fb8875c28a79d51bfc36c947d5d (mode 644) --- /dev/null +++ example/content/posts/rascunho.md @@ -0,0 +1,11 @@ +--- +title: post em rascunho +date: 2026-04-10 +description: este post nao deve aparecer no build +tags: exemplo +draft: true +--- + +# rascunho + +este post nao deve aparecer no site. blob - /dev/null blob + 828e63410fc8ceaca61b6b8377de471fa42d9a1f (mode 644) --- /dev/null +++ example/content/posts/serie-rust-parte-1.md @@ -0,0 +1,33 @@ +--- +title: "rust basico: variaveis" +date: 2026-04-07 +description: variaveis e mutabilidade em rust +tags: rust, serie +series: rust-basico +part: 1 +toc: true +--- + +# rust basico: variaveis + +primeira parte da serie sobre rust. + +## let e mut + +por padrao variaveis sao imutaveis: + +```rust +let x = 5; +// x = 6; // erro! +let mut y = 5; +y = 6; // ok +``` + +## shadowing + +voce pode redeclarar uma variavel com o mesmo nome: + +```rust +let x = 5; +let x = x + 1; +``` blob - /dev/null blob + 8c396359b22a2af3071887b657e2a8c95381978b (mode 644) --- /dev/null +++ example/content/posts/serie-rust-parte-2.md @@ -0,0 +1,36 @@ +--- +title: "rust basico: structs" +date: 2026-04-08 +description: structs e metodos em rust +tags: rust, serie +series: rust-basico +part: 2 +toc: true +--- + +# rust basico: structs + +segunda parte da serie sobre rust. + +## definindo structs + +```rust +struct Point { + x: f64, + y: f64, +} +``` + +## metodos + +use `impl` para adicionar metodos: + +```rust +impl Point { + fn distance(&self, other: &Point) -> f64 { + ((self.x - other.x).powi(2) + + (self.y - other.y).powi(2)) + .sqrt() + } +} +``` blob - /dev/null blob + cf780200d5e0f6b24ba0a14239a5f074dd10b3b1 (mode 644) --- /dev/null +++ example/content/posts/til-make.md @@ -0,0 +1,14 @@ +--- +title: "TIL: variaveis no make" +date: 2026-04-05 +description: como usar variaveis condicionais no make +tags: til, make +--- + +# TIL: variaveis no make + +`?=` so atribui se a variavel nao estiver definida: + +``` +CC ?= cc +``` blob - /dev/null blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644) blob - /dev/null blob + 1cdfa2ed3891b5124297ab4db9e9f74dd76191f1 (mode 644) --- /dev/null +++ example/static/style.css @@ -0,0 +1,21 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + font-family: monospace; + max-width: 42em; + margin: 0 auto; + padding: 1em; + line-height: 1.6; +} +nav { margin-bottom: 1em; } +main ul { margin-left: 1.5em; } +footer { margin-top: 2em; color: #666; } +time { color: #666; } +.dates { color: #666; font-size: 0.9em; } +.toc { margin: 1em 0; } +.toc summary { cursor: pointer; font-weight: bold; } +.toc > details > ul { margin: 0.5em 0 0 1.5em; } +.toc ul ul { margin: 0 0 0 1.5em; } +.series-nav { margin-top: 2em; display: flex; justify-content: space-between; } +pre { background: #f5f5f5; padding: 1em; overflow-x: auto; } +code { background: #f5f5f5; padding: 0.1em 0.3em; } +pre code { background: none; padding: 0; } blob - /dev/null blob + 7eb413fae53581b1ba3f489f1d8eed050a378fc8 (mode 644) --- /dev/null +++ example/templates/404.html @@ -0,0 +1,4 @@ + +404 +

404 - pagina nao encontrada

+

voltar ao inicio

blob - /dev/null blob + 34191e91346d7700c1d919645395593b8aaa9d02 (mode 644) --- /dev/null +++ example/templates/base.html @@ -0,0 +1,26 @@ + + + + + + + {{title}} + + + + +
+ {{content}} + {{posts}} +
+ + + blob - b0996c1df54f66f569fe8cf420ca1d655e388c47 blob + cc4374c8751ee18007023660d5f1d3679f57ddef --- kssg.rs +++ kssg.rs @@ -254,6 +254,7 @@ struct Metadata { updated: String, description: String, draft: bool, + toc: bool, series: Option, part: Option, tags: Vec, @@ -297,6 +298,7 @@ fn parse_frontmatter(content: &str) -> Metadata { updated: map.remove("updated").unwrap_or_default(), description: map.remove("description").unwrap_or_default(), draft: map.get("draft").map(|v| v == "true").unwrap_or(false), + toc: map.get("toc").map(|v| v == "true").unwrap_or(false), series: map.remove("series"), part: map.get("part").and_then(|v| v.parse().ok()), tags: map @@ -548,7 +550,17 @@ fn build() { content.push_str("

\n"); } - content.push_str(&posts[i].html_content); + if posts[i].meta.toc { + let toc = generate_toc(&posts[i].html_content); + content.push_str(&toc); + } + + let html = if posts[i].meta.toc { + add_heading_ids(&posts[i].html_content) + } else { + posts[i].html_content.clone() + }; + content.push_str(&html); if !nav.is_empty() { content.push_str(&nav); } @@ -769,6 +781,117 @@ fn generate_tag_pages(posts: &[Post], template: &str) log!("wrote public/tags/index.html"); } +fn generate_toc(html: &str) -> String { + let mut entries: Vec<(u8, String, String)> = Vec::new(); + let mut pos = 0; + while pos < html.len() { + let tag = html[pos..].find(" { + if a.1 <= b.1 { + a + } else { + b + } + } + (Some(a), None) => a, + (None, Some(b)) => b, + (None, None) => break, + }; + let close = format!("", level); + let gt = match html[start..].find('>') { + Some(i) => start + i + 1, + None => break, + }; + let end = match html[gt..].find(&close) { + Some(i) => gt + i, + None => { + pos = gt; + continue; + } + }; + let text = html[gt..end].replace("", "").replace("", ""); + let id = slugify(&text); + entries.push((level, id, text)); + pos = end + close.len(); + } + + if entries.is_empty() { + return String::new(); + } + + let mut toc = String::from("\n"); + toc +} + +fn add_heading_ids(html: &str) -> String { + let mut result = String::with_capacity(html.len()); + let mut pos = 0; + while pos < html.len() { + let h2 = html[pos..].find("

"); + let h3 = html[pos..].find("

"); + let (level, start) = match (h2, h3) { + (Some(a), Some(b)) => { + if a <= b { + (2, pos + a) + } else { + (3, pos + b) + } + } + (Some(a), None) => (2, pos + a), + (None, Some(b)) => (3, pos + b), + (None, None) => break, + }; + let tag_end = start + 4; // len of "" + let close = format!("", level); + let end = match html[tag_end..].find(&close) { + Some(i) => tag_end + i, + None => { + pos = tag_end; + continue; + } + }; + let text = html[tag_end..end] + .replace("", "") + .replace("", ""); + let id = slugify(&text); + result.push_str(&html[pos..start]); + result.push_str(&format!("", level, id)); + result.push_str(&html[tag_end..end + close.len()]); + pos = end + close.len(); + } + result.push_str(&html[pos..]); + result +} + fn series_nav(posts: &[Post], current: usize) -> String { let series = match &posts[current].meta.series { Some(s) => s,