diff --git a/Cargo.lock b/Cargo.lock
index 9bde7fd..b4f4e6a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -84,7 +84,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "attobin"
-version = "0.2.0"
+version = "0.2.1"
dependencies = [
"axum",
"axum-extra",
@@ -2586,9 +2586,9 @@ dependencies = [
[[package]]
name = "tetratto-shared"
-version = "12.0.4"
+version = "12.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a7a1ea669f94f12da3c007012c6fbac263cfc74c964c460e19da0d9c7f6abf1"
+checksum = "11c2ba2be9c92a4ac566b9c3b615d7311b3d0e98b175ad84e81b44644b34dd8f"
dependencies = [
"ammonia",
"chrono",
diff --git a/Cargo.toml b/Cargo.toml
index 2b70eb9..1e0be95 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "attobin"
-version = "0.2.0"
+version = "0.2.1"
edition = "2024"
authors = ["trisuaso"]
repository = "https://trisua.com/t/tetratto"
@@ -9,7 +9,7 @@ homepage = "https://tetratto.com"
[dependencies]
tetratto-core = "12.0.0"
-tetratto-shared = "12.0.4"
+tetratto-shared = "12.0.5"
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4"
serde = { version = "1.0.219", features = ["derive"] }
diff --git a/app/public/app.js b/app/public/app.js
index ba63155..987273b 100644
--- a/app/public/app.js
+++ b/app/public/app.js
@@ -69,7 +69,7 @@ function check_message() {
if (message) {
element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem";
- element.innerHTML += `
${message.replaceAll('"', "")}`;
+ element.innerHTML = `${message.replaceAll('"', "")}`;
}
// clear cookies
@@ -83,7 +83,7 @@ globalThis.show_message = (message, message_good = true) => {
const element = document.getElementById("messages");
element.style.marginBottom = "1rem";
element.style.paddingLeft = "1rem";
- element.innerHTML += `${message.replaceAll('"', "")}`;
+ element.innerHTML = `${message.replaceAll('"', "")}`;
};
check_message();
diff --git a/app/public/style.css b/app/public/style.css
index d8b6360..486357b 100644
--- a/app/public/style.css
+++ b/app/public/style.css
@@ -111,6 +111,11 @@ article {
}
}
+.container {
+ margin: 10px auto 0;
+ width: 100%;
+}
+
.content_container {
margin: var(--pad-2) auto;
width: 100%;
@@ -163,7 +168,7 @@ video {
justify-content: center;
align-items: center;
gap: var(--gap-2);
- padding: var(--pad-2) var(--pad-4);
+ padding: var(--pad-2) calc(var(--pad-3) * 1.5);
cursor: pointer;
background: var(--color-raised);
color: var(--color-text);
@@ -198,7 +203,7 @@ video {
/* input */
input {
--h: 36px;
- padding: var(--pad-2) var(--pad-4);
+ padding: var(--pad-2) calc(var(--pad-3) * 1.5);
background: var(--color-raised);
color: var(--color-text);
outline: none;
diff --git a/app/templates_src/view.lisp b/app/templates_src/view.lisp
index b6cb949..e3c252e 100644
--- a/app/templates_src/view.lisp
+++ b/app/templates_src/view.lisp
@@ -3,7 +3,11 @@
(title
(text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}")
+
+(text "{% if metadata.page_icon|length == 0 -%}")
(link ("rel" "icon") ("href" "/public/favicon.svg"))
+(text "{%- endif %}")
+
(text "{% endblock %} {% block body %}")
(div
("class" "flex flex-col gap-2")
diff --git a/src/markdown.rs b/src/markdown.rs
index 6f0cfc1..24b1bd5 100644
--- a/src/markdown.rs
+++ b/src/markdown.rs
@@ -1,3 +1,188 @@
+use std::collections::HashSet;
+
pub fn render_markdown(input: &str) -> String {
- tetratto_shared::markdown::render_markdown(input, false)
+ let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
+ &parse_highlight(input),
+ ));
+
+ let mut allowed_attributes = HashSet::new();
+ allowed_attributes.insert("id");
+ allowed_attributes.insert("class");
+ allowed_attributes.insert("ref");
+ allowed_attributes.insert("aria-label");
+ allowed_attributes.insert("lang");
+ allowed_attributes.insert("title");
+ allowed_attributes.insert("align");
+ allowed_attributes.insert("src");
+ allowed_attributes.insert("style");
+
+ tetratto_shared::markdown::clean_html(html, allowed_attributes)
+}
+
+fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
+ let mut in_color_buffer = false;
+ let mut in_main_buffer = false;
+ let mut color_buffer = String::new();
+ let mut close_1 = false;
+
+ for char in line.chars() {
+ if close_1 && char != '%' {
+ // we expected to see another percentage to close the main buffer,
+ // not getting that means this wasn't meant to be a color
+ buffer.push('%');
+ in_main_buffer = false;
+ close_1 = false;
+ }
+
+ match char {
+ '%' => {
+ if in_color_buffer {
+ in_color_buffer = false;
+ in_main_buffer = true;
+ continue;
+ }
+
+ if in_main_buffer {
+ // ending
+ if !close_1 {
+ close_1 = true;
+ continue;
+ }
+
+ // by this point, we have: !
+ // %color_buffer%main_buffer%%
+ output.push_str(&format!(
+ "{buffer}\n"
+ ));
+
+ color_buffer.clear();
+ buffer.clear();
+
+ // ...
+ in_main_buffer = false;
+ close_1 = false;
+ continue;
+ }
+
+ // start
+ // flush buffer
+ output.push_str(&buffer);
+ buffer.clear();
+
+ // toggle open
+ in_color_buffer = true;
+ }
+ ' ' => {
+ if in_color_buffer == true {
+ buffer.push_str(&color_buffer);
+ color_buffer.clear();
+ }
+
+ buffer.push(char);
+ }
+ _ => {
+ if in_color_buffer {
+ color_buffer.push(char)
+ } else {
+ buffer.push(char)
+ }
+ }
+ }
+ }
+}
+
+fn parse_highlight_line(output: &mut String, buffer: &mut String, line: &str) {
+ let mut open_1 = false;
+ let mut open_2 = false;
+ let mut close_1 = false;
+ let mut is_open = false;
+
+ for char in line.chars() {
+ if close_1 && char != '=' {
+ buffer.push('=');
+ close_1 = false;
+ }
+
+ match char {
+ '=' => {
+ if !is_open {
+ // flush buffer
+ output.push_str(&buffer);
+ buffer.clear();
+
+ // toggle open
+ open_1 = true;
+ is_open = true;
+ } else {
+ if open_1 {
+ // this is the second open we've recieved
+ open_2 = true;
+ open_1 = false;
+ continue;
+ }
+
+ if close_1 {
+ // this is the second close we've received
+ output.push_str(&format!("{buffer}\n"));
+ buffer.clear();
+ open_1 = false;
+ open_2 = false;
+ close_1 = false;
+ is_open = false;
+ continue;
+ }
+
+ close_1 = true;
+ }
+ }
+ _ => {
+ if open_1 {
+ open_1 = false;
+ buffer.push('=');
+ }
+
+ if open_2 && is_open {
+ open_2 = false;
+ }
+
+ buffer.push(char);
+ }
+ }
+ }
+}
+
+/// Helper macro to quickly allow parsers to ignore fenced code blocks.
+macro_rules! parser_ignores_pre {
+ ($body:ident, $input:ident) => {{
+ let mut in_pre_block = false;
+ let mut output = String::new();
+ let mut buffer = String::new();
+
+ for line in $input.split("\n") {
+ if line.starts_with("```") {
+ in_pre_block = !in_pre_block;
+ output.push_str(&format!("{line}\n"));
+ continue;
+ }
+
+ if in_pre_block {
+ output.push_str(&format!("{line}\n"));
+ continue;
+ }
+
+ $body(&mut output, &mut buffer, line);
+ output.push_str(&format!("{buffer}\n"));
+ buffer.clear();
+ }
+
+ output
+ }};
+}
+
+pub fn parse_text_color(input: &str) -> String {
+ parser_ignores_pre!(parse_text_color_line, input)
+}
+
+pub fn parse_highlight(input: &str) -> String {
+ parser_ignores_pre!(parse_highlight_line, input)
}
diff --git a/src/model.rs b/src/model.rs
index 2318e6d..5deb76c 100644
--- a/src/model.rs
+++ b/src/model.rs
@@ -10,6 +10,7 @@ pub struct Entry {
pub created: usize,
pub edited: usize,
pub content: String,
+ #[serde(default)]
pub metadata: String,
}
@@ -69,7 +70,7 @@ pub struct EntryMetadata {
#[validate(max_length = 256)]
pub page_description: String,
/// The favicon of the page.
- #[serde(default, alias = "PAGE_icon")]
+ #[serde(default, alias = "PAGE_ICON")]
#[validate(max_length = 128)]
pub page_icon: String,
/// The title of the page shown in external embeds.
@@ -99,38 +100,171 @@ pub struct EntryMetadata {
#[serde(default, alias = "ACCESS_EASY_READ")]
#[validate(max_length = 32)]
pub access_easy_read: String,
- /// The color of the text in the inner container.
- #[serde(default, alias = "CONTAINER_INNER_FOREGROUND")]
+ /// The padding of the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_PADDING")]
#[validate(max_length = 32)]
- pub container_inner_foreground: String,
+ pub container_padding: String,
+ /// The maximum width of the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_MAX_WIDTH")]
+ #[validate(max_length = 16)]
+ pub container_max_width: String,
+ /// The padding of the container.
+ /// The color of the text in the inner container.
+ #[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
+ #[validate(max_length = 32)]
+ pub container_inner_foreground_color: String,
/// The background of the inner container.
///
/// Syntax:
#[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
#[validate(max_length = 256)]
pub container_inner_background: String,
- /// The color of the text in the outer container.
- #[serde(default, alias = "CONTAINER_OUTER_FOREGROUND")]
+ /// The background of the inner container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_COLOR")]
#[validate(max_length = 32)]
- pub container_outer_foreground: String,
+ pub container_inner_background_color: String,
+ /// The background of the inner container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE")]
+ #[validate(max_length = 128)]
+ pub container_inner_background_image: String,
+ /// The background of the inner container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_REPEAT")]
+ #[validate(max_length = 16)]
+ pub container_inner_background_image_repeat: String,
+ /// The background of the inner container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_POSITION")]
+ #[validate(max_length = 16)]
+ pub container_inner_background_image_position: String,
+ /// The background of the inner container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_SIZE")]
+ #[validate(max_length = 16)]
+ pub container_inner_background_image_size: String,
+ /// The color of the text in the outer container.
+ #[serde(default, alias = "CONTAINER_OUTER_FOREGROUND_COLOR")]
+ #[validate(max_length = 32)]
+ pub container_outer_foreground_color: String,
/// The background of the outer container.
///
/// Syntax:
#[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
#[validate(max_length = 256)]
pub container_outer_background: String,
+ /// The background of the outer container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_COLOR")]
+ #[validate(max_length = 32)]
+ pub container_outer_background_color: String,
+ /// The background of the outer container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE")]
+ #[validate(max_length = 128)]
+ pub container_outer_background_image: String,
+ /// The background of the outer container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_REPEAT")]
+ #[validate(max_length = 16)]
+ pub container_outer_background_image_repeat: String,
+ /// The background of the outer container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_POSITION")]
+ #[validate(max_length = 16)]
+ pub container_outer_background_image_position: String,
+ /// The background of the outer container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_SIZE")]
+ #[validate(max_length = 16)]
+ pub container_outer_background_image_size: String,
/// The border around the container.
///
/// Syntax:
#[serde(default, alias = "CONTAINER_BORDER")]
#[validate(max_length = 256)]
pub container_border: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_IMAGE")]
+ #[validate(max_length = 128)]
+ pub container_border_image: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_IMAGE_SLICE")]
+ #[validate(max_length = 16)]
+ pub container_border_image_slice: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_IMAGE_WIDTH")]
+ #[validate(max_length = 16)]
+ pub container_border_image_width: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_IMAGE_OUTSET")]
+ #[validate(max_length = 32)]
+ pub container_border_image_outset: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_IMAGE_REPEAT")]
+ #[validate(max_length = 16)]
+ pub container_border_image_repeat: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_COLOR")]
+ #[validate(max_length = 32)]
+ pub container_border_color: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
+ #[validate(max_length = 16)]
+ pub container_border_width: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_STYLE")]
+ #[validate(max_length = 16)]
+ pub container_border_style: String,
+ /// The border around the container.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
+ #[validate(max_length = 16)]
+ pub container_border_radius: String,
/// The shadow around the container.
///
/// Syntax:
#[serde(default, alias = "CONTAINER_SHADOW")]
#[validate(max_length = 32)]
pub container_shadow: String,
+ /// The shadow under text.
+ ///
+ /// Syntax:
+ #[serde(default, alias = "CONTENT_TEXT_SHADOW")]
+ #[validate(max_length = 32)]
+ pub content_text_shadow: String,
/// The name of a font from Google Fonts to use.
#[serde(default, alias = "CONTENT_FONT")]
#[validate(max_length = 32)]
@@ -166,6 +300,17 @@ macro_rules! metadata_css {
));
}
};
+
+ ($selector:literal, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
+ if !$self.$field.is_empty() {
+ $output.push_str(&format!(
+ "{} {{ {}: {}; }}\n",
+ $selector,
+ $property,
+ format!($format, EntryMetadata::css_escape(&$self.$field))
+ ));
+ }
+ };
}
impl EntryMetadata {
@@ -233,12 +378,34 @@ impl EntryMetadata {
pub fn css(&self) -> String {
let mut output = ""
}
+
+ pub fn ini_to_toml(input: &str) -> String {
+ let mut output = String::new();
+
+ for line in input.split("\n") {
+ if !line.contains("=") {
+ // no equal sign, skip line (some other toml function)
+ continue;
+ }
+
+ let mut key = String::new();
+ let mut value = String::new();
+
+ let mut chars = line.chars();
+ let mut in_value = false;
+
+ while let Some(char) = chars.next() {
+ if !in_value {
+ key.push(char);
+
+ if char == '=' {
+ in_value = true;
+ }
+
+ continue;
+ } else {
+ value.push(char);
+ continue;
+ }
+ }
+
+ value = value.trim().to_string();
+
+ // determine if we need to stringify
+ let mut is_numeric = false;
+
+ for char in value.chars() {
+ is_numeric = char.is_numeric();
+ }
+
+ if !is_numeric && !value.starts_with("[") && !value.starts_with("\"") {
+ value = format!("\"{value}\"");
+ }
+
+ // push line
+ output.push_str(&format!("{key} {value}\n"));
+ }
+
+ output
+ }
}
diff --git a/src/routes.rs b/src/routes.rs
index 18f674f..135a23b 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -104,7 +104,8 @@ async fn view_request(
};
// check metadata
- let metadata: EntryMetadata = match toml::from_str(&entry.metadata) {
+ let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&entry.metadata))
+ {
Ok(x) => x,
Err(e) => {
let mut ctx = default_context(&data, &build_code);
@@ -269,7 +270,7 @@ async fn create_request(
}
// check metadata
- let metadata: EntryMetadata = match toml::from_str(&req.metadata) {
+ let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
@@ -359,7 +360,7 @@ async fn edit_request(
}
// check metadata
- let metadata: EntryMetadata = match toml::from_str(&req.metadata) {
+ let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};