Commit Diff


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 @@
+<!doctype html>
+<title>404</title>
+<h1>404 - pagina nao encontrada</h1>
+<p><a href="/">voltar ao inicio</a></p>
blob - /dev/null
blob + 34191e91346d7700c1d919645395593b8aaa9d02 (mode 644)
--- /dev/null
+++ example/templates/base.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<html lang="pt">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="description" content="{{description}}">
+  <title>{{title}}</title>
+  <link rel="stylesheet" href="/static/style.css">
+</head>
+<body>
+<nav>
+  <a href="/">inicio</a> |
+  <a href="/about.html">sobre</a> |
+  <a href="/tags/">tags</a> |
+  <a href="/rss.xml">rss</a>
+</nav>
+<main>
+  {{content}}
+  {{posts}}
+</main>
+<footer>
+  <hr>
+  <p>gerado com kssg</p>
+</footer>
+</body>
+</html>
blob - b0996c1df54f66f569fe8cf420ca1d655e388c47
blob + cc4374c8751ee18007023660d5f1d3679f57ddef
--- kssg.rs
+++ kssg.rs
@@ -254,6 +254,7 @@ struct Metadata {
     updated: String,
     description: String,
     draft: bool,
+    toc: bool,
     series: Option<String>,
     part: Option<u32>,
     tags: Vec<String>,
@@ -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("</p>\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("<h2").map(|i| (2u8, pos + i));
+        let tag3 = html[pos..].find("<h3").map(|i| (3u8, pos + i));
+        let (level, start) = match (tag, tag3) {
+            (Some(a), Some(b)) => {
+                if a.1 <= b.1 {
+                    a
+                } else {
+                    b
+                }
+            }
+            (Some(a), None) => a,
+            (None, Some(b)) => b,
+            (None, None) => break,
+        };
+        let close = format!("</h{}>", 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("<code>", "").replace("</code>", "");
+        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("<nav class=\"toc\">\n<details>\n");
+    toc.push_str("<summary>Sumário</summary>\n<ul>\n");
+    let mut in_sub = false;
+    for (level, id, text) in &entries {
+        if *level == 3 {
+            if !in_sub {
+                toc.push_str("<ul>\n");
+                in_sub = true;
+            }
+            toc.push_str(&format!(
+                "<li><a href=\"#{}\">{}</a></li>\n",
+                id, text
+            ));
+        } else {
+            if in_sub {
+                toc.push_str("</ul>\n");
+                in_sub = false;
+            }
+            toc.push_str(&format!(
+                "<li><a href=\"#{}\">{}</a></li>\n",
+                id, text
+            ));
+        }
+    }
+    if in_sub {
+        toc.push_str("</ul>\n");
+    }
+    toc.push_str("</ul>\n</details>\n</nav>\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("<h2>");
+        let h3 = html[pos..].find("<h3>");
+        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 "<hN>"
+        let close = format!("</h{}>", 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("<code>", "")
+            .replace("</code>", "");
+        let id = slugify(&text);
+        result.push_str(&html[pos..start]);
+        result.push_str(&format!("<h{} id=\"{}\">", 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,