styles: semantic html rather than all being pre-wrapped to generalise for others

This commit is contained in:
Lewis Wynne 2026-04-10 19:25:53 +01:00
parent 9f0e3aae6d
commit a7c74241a0
7 changed files with 52 additions and 38 deletions

View file

@ -51,16 +51,13 @@
# Maximum length for website URLs. 0 for unlimited. # Maximum length for website URLs. 0 for unlimited.
# BOOK_MAX_WEBSITE_LENGTH=0 # BOOK_MAX_WEBSITE_LENGTH=0
# Separator between guestbook entries.
# BOOK_SEPARATOR=------------------------------------------------------------
# Path to a CSS file. Takes precedence over BOOK_STYLE. # Path to a CSS file. Takes precedence over BOOK_STYLE.
# BOOK_STYLE_FILE=./templates/default.css # BOOK_STYLE_FILE=./templates/default.css
# Custom CSS injected into a style tag. # Custom CSS injected into a style tag.
# Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, # Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input,
# .guestbook-textarea, .guestbook-button, .entry-header, .entry-date, .entry-name, # .guestbook-textarea, .guestbook-button, .entry, .entry-header, .entry-date,
# .entry-website, .entry-body, .entry-separator # .entry-name, .entry-website, .entry-body
# BOOK_STYLE= # BOOK_STYLE=
# Text shown above the form. # Text shown above the form.

View file

@ -208,7 +208,7 @@ in
css = mkOption { css = mkOption {
type = types.str; type = types.str;
default = ""; default = "";
description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .guestbook-canvas, .entry-header, .entry-date, .entry-name, .entry-website, .entry-body, .entry-drawing, .entry-separator"; description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .guestbook-canvas, .entry, .entry-header, .entry-date, .entry-name, .entry-website, .entry-body, .entry-drawing";
}; };
cssFile = mkOption { cssFile = mkOption {
@ -229,12 +229,6 @@ in
description = "Custom success page template with {{title}} and {{style}} placeholders. Uses built-in default if null."; description = "Custom success page template with {{title}} and {{style}} placeholders. Uses built-in default if null.";
}; };
separator = mkOption {
type = types.str;
default = "------------------------------------------------------------";
description = "Separator between guestbook entries.";
};
greeting = mkOption { greeting = mkOption {
type = types.str; type = types.str;
default = "Thanks for visiting. Sign the guestbook!"; default = "Thanks for visiting. Sign the guestbook!";
@ -308,7 +302,6 @@ in
BOOK_MAX_NAME_LENGTH = toString cfg.limits.name; BOOK_MAX_NAME_LENGTH = toString cfg.limits.name;
BOOK_MAX_MESSAGE_LENGTH = toString cfg.limits.message; BOOK_MAX_MESSAGE_LENGTH = toString cfg.limits.message;
BOOK_MAX_WEBSITE_LENGTH = toString cfg.limits.website; BOOK_MAX_WEBSITE_LENGTH = toString cfg.limits.website;
BOOK_SEPARATOR = cfg.styles.separator;
BOOK_STYLE = cfg.styles.css; BOOK_STYLE = cfg.styles.css;
BOOK_FORM_PROMPT = cfg.styles.greeting; BOOK_FORM_PROMPT = cfg.styles.greeting;
BOOK_BUTTON_TEXT = cfg.styles.labels.submit; BOOK_BUTTON_TEXT = cfg.styles.labels.submit;

View file

@ -30,7 +30,6 @@ pub struct Config {
pub voice_note_max_duration: u32, pub voice_note_max_duration: u32,
pub template: Option<String>, pub template: Option<String>,
pub success_template: Option<String>, pub success_template: Option<String>,
pub separator: String,
pub style: String, pub style: String,
pub form_prompt: String, pub form_prompt: String,
pub button_text: String, pub button_text: String,
@ -131,8 +130,6 @@ impl Config {
.unwrap_or_else(|_| "20".into()) .unwrap_or_else(|_| "20".into())
.parse() .parse()
.map_err(|_| "BOOK_VOICE_NOTE_MAX_DURATION must be a number")?, .map_err(|_| "BOOK_VOICE_NOTE_MAX_DURATION must be a number")?,
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}"))

View file

@ -238,9 +238,8 @@ pub fn render_error_page(config: &Config, error: &str) -> String {
</head> </head>
<body> <body>
<div class="page-container"> <div class="page-container">
{error} <p>{error}</p>
<p><a href="/">&#8592; back</a></p>
<a href="/">&#8592; back</a>
</div> </div>
</body> </body>
</html>"#, </html>"#,
@ -271,7 +270,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
escape_html(&entry.meta.name) escape_html(&entry.meta.name)
}; };
let mut header = format!( let mut header = format!(
"<span class=\"entry-header\"><span class=\"entry-date\">{}</span> - <span class=\"entry-name\">{}</span>", "<header class=\"entry-header\"><span class=\"entry-date\">{}</span> - <span class=\"entry-name\">{}</span>",
&entry.meta.date[..10], name &entry.meta.date[..10], name
); );
if config.enable_website_links && !entry.meta.website.is_empty() { if config.enable_website_links && !entry.meta.website.is_empty() {
@ -281,7 +280,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
website, website website, website
)); ));
} }
header.push_str("</span>"); header.push_str("</header>");
let body = if config.enable_html_injection { let body = if config.enable_html_injection {
entry.body.clone() entry.body.clone()
} else { } else {
@ -305,8 +304,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
String::new() String::new()
}; };
format!( format!(
"\n{header}\n{drawing_html}{voice_note_html}\n<span class=\"entry-body\">{body}</span>\n\n<span class=\"entry-separator\">{}</span>\n", "<article class=\"entry\">{header}{drawing_html}{voice_note_html}<div class=\"entry-body\">{body}</div></article>"
config.separator
) )
} }
@ -345,7 +343,6 @@ mod tests {
voice_note_max_duration: 20, voice_note_max_duration: 20,
template: None, template: None,
success_template: None, success_template: None,
separator: "---".into(),
style: String::new(), style: String::new(),
form_prompt: "Thanks for visiting. Sign the guestbook!".into(), form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
button_text: "sign".into(), button_text: "sign".into(),
@ -400,7 +397,7 @@ mod tests {
assert!(html.contains("entry-header")); assert!(html.contains("entry-header"));
assert!(html.contains("entry-name")); assert!(html.contains("entry-name"));
assert!(html.contains("entry-body")); assert!(html.contains("entry-body"));
assert!(html.contains("entry-separator")); assert!(html.contains("<article class=\"entry\">"));
} }
#[test] #[test]

View file

@ -368,7 +368,6 @@ mod tests {
voice_note_max_duration: 20, voice_note_max_duration: 20,
template: None, template: None,
success_template: None, success_template: None,
separator: "---".into(),
style: String::new(), style: String::new(),
form_prompt: "Thanks for visiting. Sign the guestbook!".into(), form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
button_text: "sign".into(), button_text: "sign".into(),

View file

@ -3,22 +3,32 @@
max-width: 70ch; max-width: 70ch;
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 1rem;
white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
/* Form */ /* Form */
.guestbook-prompt {} .guestbook-prompt {
display: block;
margin-bottom: 1em;
}
.guestbook-form {} .guestbook-form {}
.guestbook-label {} .guestbook-label {
.guestbook-input {} display: block;
}
.guestbook-input {
display: block;
margin-bottom: 0.5em;
}
.guestbook-textarea { .guestbook-textarea {
display: block;
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
margin-bottom: 0.5em;
} }
.guestbook-button { .guestbook-button {
display: block; display: block;
margin-top: 1em; margin-top: 1em;
margin-bottom: 1.5em;
} }
/* Drawings */ /* Drawings */
@ -35,12 +45,19 @@
.guestbook-canvas-tools a { .guestbook-canvas-tools a {
cursor: pointer; cursor: pointer;
} }
.guestbook-drawing-wrap {
display: block;
margin-bottom: 0.5em;
}
.guestbook-drawing-inline a { .guestbook-drawing-inline a {
cursor: pointer; cursor: pointer;
} }
.guestbook-drawing-content:empty {
display: none;
}
.guestbook-drawing-content { .guestbook-drawing-content {
display: block; display: block;
margin-bottom: 1em; margin-bottom: 0.5em;
} }
.guestbook-swatch { .guestbook-swatch {
display: inline-block; display: inline-block;
@ -65,6 +82,10 @@
} }
/* Voice notes */ /* Voice notes */
.guestbook-voice-wrap {
display: block;
margin-bottom: 0.5em;
}
.guestbook-voice-record.recording { .guestbook-voice-record.recording {
color: red; color: red;
} }
@ -80,14 +101,25 @@
} }
audio { audio {
display: block; display: block;
margin-top: 0.6em; margin-bottom: 0.3em;
height: 2em; height: 2em;
} }
/* Entries */ /* Entries */
.entry-header {} .entry {
margin: 0.5em 0;
padding-bottom: 0.5em;
border-bottom: 1px solid #ccc;
}
.entry:last-child {
border-bottom: none;
}
.entry-header {
margin-bottom: 0.2em;
}
.entry-date {} .entry-date {}
.entry-name {} .entry-name {}
.entry-website {} .entry-website {}
.entry-body {} .entry-body {
.entry-separator {} white-space: pre-wrap;
}

View file

@ -20,9 +20,8 @@
</head> </head>
<body> <body>
<div class="page-container"> <div class="page-container">
Thanks! Your message is pending approval. <p>Thanks! Your message is pending approval.</p>
<p><a href="/">&#8592; back</a></p>
<a href="/">&#8592; back</a>
</div> </div>
</body> </body>
</html> </html>