add: parse_image_line, parse_link_line
This commit is contained in:
parent
d8167aa06f
commit
9fe5735127
9 changed files with 234 additions and 25 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2551,9 +2551,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "12.0.0"
|
version = "12.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2446a581866039e72a9203a472b97390991cb2ef51c5c29dad3aa69dd41edc2d"
|
checksum = "2c1b01499f7471ee6f05299c06ebb18760440452714c6f9a6c0c9e0cf9a663bd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
|
|
@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later"
|
||||||
homepage = "https://tetratto.com"
|
homepage = "https://tetratto.com"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tetratto-core = "12.0.0"
|
tetratto-core = "12.0.1"
|
||||||
tetratto-shared = "12.0.5"
|
tetratto-shared = "12.0.5"
|
||||||
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
||||||
pathbufd = "0.1.4"
|
pathbufd = "0.1.4"
|
||||||
|
|
|
@ -144,6 +144,14 @@ globalThis.tab_editor = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
globalThis.tab_preview = async () => {
|
globalThis.tab_preview = async () => {
|
||||||
|
if (
|
||||||
|
!document
|
||||||
|
.getElementById("preview_tab_button")
|
||||||
|
.classList.contains("camo")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// render
|
// render
|
||||||
const res = await (
|
const res = await (
|
||||||
await fetch("/api/v1/render", {
|
await fetch("/api/v1/render", {
|
||||||
|
@ -151,6 +159,7 @@ globalThis.tab_preview = async () => {
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: globalThis.editor.getValue(),
|
content: globalThis.editor.getValue(),
|
||||||
|
metadata: globalThis.metadata_editor.getValue(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
).text();
|
).text();
|
||||||
|
|
|
@ -111,7 +111,7 @@ article {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container:not(#preview_tab) {
|
.container:not(#preview_tab):not(#tabs_group) {
|
||||||
margin: 10px auto 0;
|
margin: 10px auto 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -375,7 +375,7 @@ h6 {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2rem;
|
margin: 2rem 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,14 @@
|
||||||
("onclick" "tab_metadata()")
|
("onclick" "tab_metadata()")
|
||||||
(text "Metadata")))
|
(text "Metadata")))
|
||||||
(div
|
(div
|
||||||
("class" "card tab tabs")
|
("class" "card tab tabs container")
|
||||||
|
("id" "tabs_group")
|
||||||
(div
|
(div
|
||||||
("id" "editor_tab")
|
("id" "editor_tab")
|
||||||
("class" "tab fadein"))
|
("class" "tab fadein"))
|
||||||
(div
|
(div
|
||||||
("id" "preview_tab")
|
("id" "preview_tab")
|
||||||
("class" "tab fadein hidden container"))
|
("class" "tab fadein hidden"))
|
||||||
(div
|
(div
|
||||||
("id" "metadata_tab")
|
("id" "metadata_tab")
|
||||||
("class" "tab fadein hidden")))
|
("class" "tab fadein hidden")))
|
||||||
|
|
|
@ -21,13 +21,14 @@
|
||||||
("onclick" "tab_metadata()")
|
("onclick" "tab_metadata()")
|
||||||
(text "Metadata")))
|
(text "Metadata")))
|
||||||
(div
|
(div
|
||||||
("class" "card tab tabs")
|
("class" "card tab tabs container")
|
||||||
|
("id" "tabs_group")
|
||||||
(div
|
(div
|
||||||
("id" "editor_tab")
|
("id" "editor_tab")
|
||||||
("class" "tab fadein"))
|
("class" "tab fadein"))
|
||||||
(div
|
(div
|
||||||
("id" "preview_tab")
|
("id" "preview_tab")
|
||||||
("class" "tab fadein hidden container"))
|
("class" "tab fadein hidden"))
|
||||||
(div
|
(div
|
||||||
("id" "metadata_tab")
|
("id" "metadata_tab")
|
||||||
("class" "tab fadein hidden")))
|
("class" "tab fadein hidden")))
|
||||||
|
|
205
src/markdown.rs
205
src/markdown.rs
|
@ -2,7 +2,9 @@ use std::collections::HashSet;
|
||||||
|
|
||||||
pub fn render_markdown(input: &str) -> String {
|
pub fn render_markdown(input: &str) -> String {
|
||||||
let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
|
let html = tetratto_shared::markdown::render_markdown_dirty(&parse_text_color(
|
||||||
&parse_highlight(&parse_underline(&parse_comment(&parse_image_size(input)))),
|
&parse_highlight(&parse_link(&parse_image(&parse_image_size(
|
||||||
|
&parse_underline(&parse_comment(input)),
|
||||||
|
)))),
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut allowed_attributes = HashSet::new();
|
let mut allowed_attributes = HashSet::new();
|
||||||
|
@ -16,7 +18,13 @@ pub fn render_markdown(input: &str) -> String {
|
||||||
allowed_attributes.insert("src");
|
allowed_attributes.insert("src");
|
||||||
allowed_attributes.insert("style");
|
allowed_attributes.insert("style");
|
||||||
|
|
||||||
tetratto_shared::markdown::clean_html(html, allowed_attributes)
|
tetratto_shared::markdown::clean_html(
|
||||||
|
html.replace("<style>", "<span>:temp_style")
|
||||||
|
.replace("</style>", "</span>:temp_style"),
|
||||||
|
allowed_attributes,
|
||||||
|
)
|
||||||
|
.replace("<span>:temp_style", "<style>")
|
||||||
|
.replace("</span>:temp_style", "</style>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
|
fn parse_text_color_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
|
@ -258,6 +266,11 @@ fn parse_comment_line(output: &mut String, _: &mut String, line: &str) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if line == "[..]" {
|
||||||
|
output.push_str(" ");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
output.push_str(line);
|
output.push_str(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,17 +336,7 @@ fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
if in_size && in_size_rhs {
|
if in_size && in_size_rhs {
|
||||||
// end
|
// end
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
"<span style=\"width: {}; height: {}\" class=\"img_sizer\">![{buffer}</span>",
|
"<span style=\"width: {size_lhs}; height: {size_rhs}\" class=\"img_sizer\">![{buffer}</span>"
|
||||||
if size_lhs == "auto" {
|
|
||||||
size_lhs
|
|
||||||
} else {
|
|
||||||
format!("{size_lhs}px")
|
|
||||||
},
|
|
||||||
if size_rhs == "auto" {
|
|
||||||
size_rhs
|
|
||||||
} else {
|
|
||||||
format!("{size_rhs}px")
|
|
||||||
}
|
|
||||||
));
|
));
|
||||||
|
|
||||||
size_lhs = String::new();
|
size_lhs = String::new();
|
||||||
|
@ -380,6 +383,174 @@ fn parse_image_size_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_image_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
|
let mut image_possible = false;
|
||||||
|
let mut in_image = false;
|
||||||
|
let mut in_alt = false;
|
||||||
|
let mut in_src = false;
|
||||||
|
let mut alt = String::new();
|
||||||
|
|
||||||
|
for char in line.chars() {
|
||||||
|
if image_possible && char != '[' {
|
||||||
|
image_possible = false;
|
||||||
|
output.push('!');
|
||||||
|
}
|
||||||
|
|
||||||
|
match char {
|
||||||
|
'[' => {
|
||||||
|
if image_possible {
|
||||||
|
in_image = true;
|
||||||
|
image_possible = false;
|
||||||
|
in_alt = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_image {
|
||||||
|
buffer.push(char);
|
||||||
|
} else {
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
']' => {
|
||||||
|
if in_alt {
|
||||||
|
in_alt = false;
|
||||||
|
in_src = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
'(' => {
|
||||||
|
if in_src {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_image {
|
||||||
|
buffer.push(char);
|
||||||
|
} else {
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
if in_image {
|
||||||
|
// end
|
||||||
|
output.push_str(&format!(
|
||||||
|
"<img loading=\"lazy\" alt=\"{alt}\" src=\"{buffer}\" />"
|
||||||
|
));
|
||||||
|
|
||||||
|
alt = String::new();
|
||||||
|
in_alt = false;
|
||||||
|
in_src = false;
|
||||||
|
in_image = false;
|
||||||
|
image_possible = false;
|
||||||
|
|
||||||
|
buffer.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
'!' => {
|
||||||
|
// flush buffer
|
||||||
|
output.push_str(&buffer);
|
||||||
|
buffer.clear();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
image_possible = true;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if in_image {
|
||||||
|
if in_alt {
|
||||||
|
alt.push(char)
|
||||||
|
} else {
|
||||||
|
buffer.push(char);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.push(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_link_line(output: &mut String, buffer: &mut String, line: &str) {
|
||||||
|
let mut in_link = false;
|
||||||
|
let mut in_text = false;
|
||||||
|
let mut in_src = false;
|
||||||
|
let mut text = String::new();
|
||||||
|
|
||||||
|
for (i, char) in line.chars().enumerate() {
|
||||||
|
match char {
|
||||||
|
'[' => {
|
||||||
|
// flush buffer
|
||||||
|
output.push_str(&buffer);
|
||||||
|
buffer.clear();
|
||||||
|
|
||||||
|
// scan for closing, otherwise quit
|
||||||
|
let haystack = &line[i..];
|
||||||
|
|
||||||
|
if !haystack.contains("]") {
|
||||||
|
output.push('[');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
in_link = true;
|
||||||
|
in_text = true;
|
||||||
|
}
|
||||||
|
']' => {
|
||||||
|
if in_text {
|
||||||
|
in_text = false;
|
||||||
|
in_src = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
'(' => {
|
||||||
|
if in_src {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_link {
|
||||||
|
buffer.push(char);
|
||||||
|
} else {
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
if in_link {
|
||||||
|
// end
|
||||||
|
output.push_str(&format!(
|
||||||
|
"<a href=\"{buffer}\" rel=\"noopener noreferrer\">{text}</a>"
|
||||||
|
));
|
||||||
|
|
||||||
|
text = String::new();
|
||||||
|
in_text = false;
|
||||||
|
in_src = false;
|
||||||
|
in_link = false;
|
||||||
|
|
||||||
|
buffer.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(char);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if in_link {
|
||||||
|
if in_text {
|
||||||
|
text.push(char)
|
||||||
|
} else {
|
||||||
|
buffer.push(char);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.push(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper macro to quickly allow parsers to ignore fenced code blocks.
|
/// Helper macro to quickly allow parsers to ignore fenced code blocks.
|
||||||
macro_rules! parser_ignores_pre {
|
macro_rules! parser_ignores_pre {
|
||||||
($body:ident, $input:ident) => {{
|
($body:ident, $input:ident) => {{
|
||||||
|
@ -427,3 +598,11 @@ pub fn parse_comment(input: &str) -> String {
|
||||||
pub fn parse_image_size(input: &str) -> String {
|
pub fn parse_image_size(input: &str) -> String {
|
||||||
parser_ignores_pre!(parse_image_size_line, input)
|
parser_ignores_pre!(parse_image_size_line, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_image(input: &str) -> String {
|
||||||
|
parser_ignores_pre!(parse_image_line, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_link(input: &str) -> String {
|
||||||
|
parser_ignores_pre!(parse_link_line, input)
|
||||||
|
}
|
||||||
|
|
13
src/model.rs
13
src/model.rs
|
@ -364,7 +364,7 @@ impl EntryMetadata {
|
||||||
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
|
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
|
||||||
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
|
<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
|
||||||
<link href=\"https://fonts.googleapis.com/css2?family={}&display=swap\" rel=\"stylesheet\">",
|
<link href=\"https://fonts.googleapis.com/css2?family={}&display=swap\" rel=\"stylesheet\">",
|
||||||
self.content_font.replace(" ", "+").replace(" ", "_"),
|
self.content_font.replace(" ", "+").replace("_", "+"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +416,15 @@ impl EntryMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (element, size) in &self.content_text_size {
|
for (element, size) in &self.content_text_size {
|
||||||
|
if element == "*" {
|
||||||
|
output.push_str(&format!(
|
||||||
|
".container, .container * {{ font-size: {}; }}\n",
|
||||||
|
EntryMetadata::css_escape(&size)
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
".container {} {{ font-size: {}; }}\n",
|
".container {} {{ font-size: {}; }}\n",
|
||||||
element,
|
element,
|
||||||
|
@ -426,7 +435,7 @@ impl EntryMetadata {
|
||||||
if !self.content_font.is_empty() {
|
if !self.content_font.is_empty() {
|
||||||
output.push_str(&format!(
|
output.push_str(&format!(
|
||||||
".container {{ font-family: \"{}\", system-ui; }}",
|
".container {{ font-family: \"{}\", system-ui; }}",
|
||||||
self.content_font
|
self.content_font.replace("_", " ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -208,10 +208,20 @@ async fn editor_request(
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct RenderMarkdown {
|
struct RenderMarkdown {
|
||||||
content: String,
|
content: String,
|
||||||
|
metadata: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_request(Json(req): Json<RenderMarkdown>) -> impl IntoResponse {
|
async fn render_request(Json(req): Json<RenderMarkdown>) -> impl IntoResponse {
|
||||||
crate::markdown::render_markdown(&req.content)
|
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Html(e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = metadata.validate() {
|
||||||
|
return Html(e.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Html(crate::markdown::render_markdown(&req.content) + &metadata.css())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exists_request(
|
async fn exists_request(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue