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 }