configurable separator, .env.example
This commit is contained in:
parent
d751eb62a9
commit
ef6a190549
5 changed files with 39 additions and 14 deletions
13
.env.example
Normal file
13
.env.example
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
BOOK_PORT=8123
|
||||||
|
BOOK_DATA_DIR=./data
|
||||||
|
BOOK_SITE_TITLE=guestbook
|
||||||
|
BOOK_SITE_URL=https://example.com
|
||||||
|
BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here
|
||||||
|
BOOK_TELEGRAM_CHAT_ID=0
|
||||||
|
BOOK_HONEYPOT=true
|
||||||
|
BOOK_MAX_NAME_LENGTH=50
|
||||||
|
BOOK_MAX_MESSAGE_LENGTH=1000
|
||||||
|
BOOK_MAX_WEBSITE_LENGTH=100
|
||||||
|
BOOK_OPEN_REGISTRATION=true
|
||||||
|
BOOK_SEPARATOR=------------------------------------------------------------
|
||||||
|
# BOOK_TEMPLATE=./templates/default.html
|
||||||
|
|
@ -75,6 +75,12 @@ in
|
||||||
description = "Allow new guestbook submissions. When false, the form is hidden and submissions are rejected.";
|
description = "Allow new guestbook submissions. When false, the form is hidden and submissions are rejected.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
separator = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "------------------------------------------------------------";
|
||||||
|
description = "Separator between guestbook entries.";
|
||||||
|
};
|
||||||
|
|
||||||
templateFile = mkOption {
|
templateFile = mkOption {
|
||||||
type = types.nullOr types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
@ -121,6 +127,7 @@ in
|
||||||
BOOK_MAX_MESSAGE_LENGTH = toString cfg.maxMessageLength;
|
BOOK_MAX_MESSAGE_LENGTH = toString cfg.maxMessageLength;
|
||||||
BOOK_MAX_WEBSITE_LENGTH = toString cfg.maxWebsiteLength;
|
BOOK_MAX_WEBSITE_LENGTH = toString cfg.maxWebsiteLength;
|
||||||
BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false";
|
BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false";
|
||||||
|
BOOK_SEPARATOR = cfg.separator;
|
||||||
} // lib.optionalAttrs (cfg.templateFile != null) {
|
} // lib.optionalAttrs (cfg.templateFile != null) {
|
||||||
BOOK_TEMPLATE = cfg.templateFile;
|
BOOK_TEMPLATE = cfg.templateFile;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ pub struct Config {
|
||||||
pub max_website_length: usize,
|
pub max_website_length: usize,
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub template: Option<String>,
|
pub template: Option<String>,
|
||||||
|
pub separator: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
|
@ -57,6 +58,8 @@ impl Config {
|
||||||
open_registration: env::var("BOOK_OPEN_REGISTRATION")
|
open_registration: env::var("BOOK_OPEN_REGISTRATION")
|
||||||
.map(|v| v != "false")
|
.map(|v| v != "false")
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
|
separator: env::var("BOOK_SEPARATOR")
|
||||||
|
.unwrap_or_else(|_| "------------------------------------------------------------".into()),
|
||||||
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
|
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
|
||||||
std::fs::read_to_string(&path)
|
std::fs::read_to_string(&path)
|
||||||
.unwrap_or_else(|e| panic!("failed to read template {path}: {e}"))
|
.unwrap_or_else(|e| panic!("failed to read template {path}: {e}"))
|
||||||
|
|
|
||||||
|
|
@ -35,23 +35,23 @@ entries
|
||||||
</html>
|
</html>
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str) -> String {
|
pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str, separator: &str) -> String {
|
||||||
let entries_html = render_entries(entries);
|
let entries_html = render_entries(entries, separator);
|
||||||
template
|
template
|
||||||
.replace("{{title}}", title)
|
.replace("{{title}}", title)
|
||||||
.replace("{{form}}", form_html)
|
.replace("{{form}}", form_html)
|
||||||
.replace("{{entries}}", &entries_html)
|
.replace("{{entries}}", &entries_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_entries(entries: &[Entry]) -> String {
|
fn render_entries(entries: &[Entry], separator: &str) -> String {
|
||||||
let mut html = String::new();
|
let mut html = String::new();
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
html.push_str(&render_entry(entry));
|
html.push_str(&render_entry(entry, separator));
|
||||||
}
|
}
|
||||||
html
|
html
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_entry(entry: &Entry) -> String {
|
fn render_entry(entry: &Entry, separator: &str) -> String {
|
||||||
let mut header = format!("{} - {}", entry.meta.date, entry.meta.name);
|
let mut header = format!("{} - {}", entry.meta.date, entry.meta.name);
|
||||||
if !entry.meta.website.is_empty() {
|
if !entry.meta.website.is_empty() {
|
||||||
header.push_str(&format!(
|
header.push_str(&format!(
|
||||||
|
|
@ -60,7 +60,7 @@ fn render_entry(entry: &Entry) -> String {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
format!(
|
format!(
|
||||||
"\n{header}\n\n{}\n\n------------------------------------------------------------\n",
|
"\n{header}\n\n{}\n\n{separator}\n",
|
||||||
entry.body
|
entry.body
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -99,16 +99,16 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_default_template() {
|
fn test_render_default_template() {
|
||||||
let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML);
|
let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML, "---");
|
||||||
assert!(html.contains("<title>ily.rs</title>"));
|
assert!(html.contains("<title>ily.rs</title>"));
|
||||||
assert!(html.contains("action=\"/submit\""));
|
assert!(html.contains("action=\"/submit\""));
|
||||||
assert!(html.contains("<pre style=font:unset>"));
|
assert!(html.contains("<pre>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_custom_template() {
|
fn test_render_custom_template() {
|
||||||
let custom = "<html>{{title}} {{form}} {{entries}}</html>";
|
let custom = "<html>{{title}} {{form}} {{entries}}</html>";
|
||||||
let html = render_page(custom, "my site", &[], FORM_HTML);
|
let html = render_page(custom, "my site", &[], FORM_HTML, "---");
|
||||||
assert!(html.contains("my site"));
|
assert!(html.contains("my site"));
|
||||||
assert!(html.contains("action=\"/submit\""));
|
assert!(html.contains("action=\"/submit\""));
|
||||||
}
|
}
|
||||||
|
|
@ -116,31 +116,31 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_entry_no_website() {
|
fn test_render_entry_no_website() {
|
||||||
let entry = make_entry("alice", "2026-04-09", "Hello!");
|
let entry = make_entry("alice", "2026-04-09", "Hello!");
|
||||||
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
|
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
|
||||||
assert!(html.contains("2026-04-09 - alice"));
|
assert!(html.contains("2026-04-09 - alice"));
|
||||||
assert!(html.contains("Hello!"));
|
assert!(html.contains("Hello!"));
|
||||||
assert!(html.contains("----"));
|
assert!(html.contains("---"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_entry_with_website() {
|
fn test_render_entry_with_website() {
|
||||||
let mut entry = make_entry("bob", "2026-04-09", "Hi!");
|
let mut entry = make_entry("bob", "2026-04-09", "Hi!");
|
||||||
entry.meta.website = "https://bob.com".into();
|
entry.meta.website = "https://bob.com".into();
|
||||||
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
|
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
|
||||||
assert!(html.contains(r#"<a href="https://bob.com">"#));
|
assert!(html.contains(r#"<a href="https://bob.com">"#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_preserves_html_in_body() {
|
fn test_render_preserves_html_in_body() {
|
||||||
let entry = make_entry("carol", "2026-04-09", "<b>Bold</b> <script>alert(1)</script>");
|
let entry = make_entry("carol", "2026-04-09", "<b>Bold</b> <script>alert(1)</script>");
|
||||||
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
|
let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
|
||||||
assert!(html.contains("<b>Bold</b>"));
|
assert!(html.contains("<b>Bold</b>"));
|
||||||
assert!(html.contains("<script>alert(1)</script>"));
|
assert!(html.contains("<script>alert(1)</script>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_empty_form_when_closed() {
|
fn test_render_empty_form_when_closed() {
|
||||||
let html = render_page(DEFAULT_TEMPLATE, "test", &[], "");
|
let html = render_page(DEFAULT_TEMPLATE, "test", &[], "", "---");
|
||||||
assert!(!html.contains("action=\"/submit\""));
|
assert!(!html.contains("action=\"/submit\""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
&state.config.site_title,
|
&state.config.site_title,
|
||||||
&entries,
|
&entries,
|
||||||
form,
|
form,
|
||||||
|
&state.config.separator,
|
||||||
);
|
);
|
||||||
Html(html)
|
Html(html)
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +135,7 @@ mod tests {
|
||||||
max_website_length: 100,
|
max_website_length: 100,
|
||||||
open_registration: true,
|
open_registration: true,
|
||||||
template: None,
|
template: None,
|
||||||
|
separator: "---".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue