sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git

src/init.rs (4824B)

      1 //! `sqzass init` — 새 사이트 스캐폴드.
      2 //!
      3 //! 파일 셋만 만든다. 문서의 "첫 사이트 만들기"가 손으로 만들라고 적어 둔 바로 그
      4 //! 셋이고, 그래야 튜토리얼과 도구가 같은 것을 말한다.
      5 //!
      6 //! 예제 콘텐츠를 잔뜩 깔지 않는 이유는, 스캐폴드가 크면 처음 하는 일이 "만드는 것"이
      7 //! 아니라 "지우는 것"이 되기 때문이다.
      8 
      9 use anyhow::{Context, Result, bail};
     10 use std::path::Path;
     11 
     12 /// 스캐폴드가 만드는 파일들. `(경로, 내용)` — `{title}`만 치환한다.
     13 const FILES: &[(&str, &str)] = &[
     14     (
     15         "sqzass.toml",
     16         r#"title    = "{title}"
     17 base_url = "https://example.com"
     18 
     19 # 나머지는 전부 기본값이 있다. `sqzass.toml`에 없는 키는 없는 대로 동작한다.
     20 "#,
     21     ),
     22     (
     23         "content/_index.md",
     24         r#"+++
     25 title = "{title}"
     26 +++
     27 
     28 이 파일이 사이트의 첫 페이지입니다. `content/` 아래에 마크다운을 더하면 페이지가
     29 늘어납니다. `about.md`는 `/about/`이 되고, `guide/_index.md`가 있는 디렉터리는
     30 섹션이 됩니다.
     31 
     32 ```bash
     33 sqzass serve
     34 ```
     35 "#,
     36     ),
     37     (
     38         "templates/page.html",
     39         r#"<!doctype html>
     40 <html lang="{{ page.language }}">
     41 <head>
     42 <meta charset="utf-8">
     43 <meta name="viewport" content="width=device-width, initial-scale=1">
     44 <title>{{ page.title }}</title>
     45 {# 코드 강조 스타일시트. 빌드가 테마에서 만들어 준다. #}
     46 {%- if site.highlight_css %}
     47 <link rel="stylesheet" href="{{ site.highlight_css }}">
     48 {%- endif %}
     49 </head>
     50 <body>
     51 {# page.content 는 이미 HTML이라 `| safe`가 필요하다. 나머지 값은 전부
     52    자동으로 이스케이프된다. #}
     53 {{ page.content | safe }}
     54 </body>
     55 </html>
     56 "#,
     57     ),
     58 ];
     59 
     60 /// `dir`에 새 사이트를 만든다.
     61 ///
     62 /// 이미 `sqzass.toml`이 있으면 아무것도 하지 않는다 — 남의 사이트를 반쯤 덮어쓴
     63 /// 상태로 만드는 것보다 아무 일도 안 하는 게 낫다.
     64 pub fn init(dir: &Path) -> Result<Vec<String>> {
     65     let config = dir.join("sqzass.toml");
     66     if config.exists() {
     67         bail!(
     68             "{}에 이미 사이트가 있습니다 (sqzass.toml).\n\
     69              비어 있는 디렉터리를 지정하거나, 새 디렉터리 이름을 주세요.",
     70             dir.display()
     71         );
     72     }
     73 
     74     // 디렉터리 이름을 제목으로 쓴다. `.`처럼 이름이랄 게 없으면 현재 디렉터리
     75     // 이름으로 떨어지고, 그것도 없으면 마지막 폴백을 쓴다.
     76     let title = dir
     77         .canonicalize()
     78         .ok()
     79         .as_deref()
     80         .and_then(Path::file_name)
     81         .or_else(|| dir.file_name())
     82         .map(|n| n.to_string_lossy().into_owned())
     83         .unwrap_or_else(|| "My site".into());
     84 
     85     let mut written = Vec::new();
     86     for (rel, body) in FILES {
     87         let path = dir.join(rel);
     88         if let Some(parent) = path.parent() {
     89             std::fs::create_dir_all(parent)
     90                 .with_context(|| format!("{}을(를) 만들 수 없습니다", parent.display()))?;
     91         }
     92         std::fs::write(&path, body.replace("{title}", &title))
     93             .with_context(|| format!("{}을(를) 쓸 수 없습니다", path.display()))?;
     94         written.push((*rel).to_string());
     95     }
     96 
     97     Ok(written)
     98 }
     99 
    100 #[cfg(test)]
    101 mod tests {
    102     use super::*;
    103 
    104     fn tmp(name: &str) -> std::path::PathBuf {
    105         let p = std::env::temp_dir().join(format!("sqzass-init-{}-{name}", std::process::id()));
    106         let _ = std::fs::remove_dir_all(&p);
    107         p
    108     }
    109 
    110     #[test]
    111     fn scaffolds_a_buildable_site() {
    112         let dir = tmp("build");
    113         let written = init(&dir).unwrap();
    114         assert_eq!(written.len(), 3, "파일 셋: {written:?}");
    115 
    116         // 스캐폴드가 곧바로 빌드되지 않으면 스캐폴드가 아니다.
    117         let out = crate::build(&crate::BuildOptions {
    118             input: dir.clone(),
    119             output: None,
    120             drafts: false,
    121             base_url: None,
    122             profile: false,
    123         })
    124         .unwrap();
    125         assert_eq!(out.pages_written, 1);
    126         assert!(dir.join("public/index.html").is_file());
    127 
    128         let _ = std::fs::remove_dir_all(&dir);
    129     }
    130 
    131     #[test]
    132     fn refuses_to_overwrite_an_existing_site() {
    133         let dir = tmp("existing");
    134         init(&dir).unwrap();
    135         let err = init(&dir).unwrap_err().to_string();
    136         assert!(err.contains("이미 사이트가 있습니다"), "실제: {err}");
    137         let _ = std::fs::remove_dir_all(&dir);
    138     }
    139 
    140     #[test]
    141     fn title_comes_from_the_directory_name() {
    142         let dir = tmp("mysite");
    143         init(&dir).unwrap();
    144         let cfg = std::fs::read_to_string(dir.join("sqzass.toml")).unwrap();
    145         assert!(cfg.contains("mysite"), "실제: {cfg}");
    146         let _ = std::fs::remove_dir_all(&dir);
    147     }
    148 }