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]]
|
||||
name = "tetratto-core"
|
||||
version = "12.0.0"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2446a581866039e72a9203a472b97390991cb2ef51c5c29dad3aa69dd41edc2d"
|
||||
checksum = "2c1b01499f7471ee6f05299c06ebb18760440452714c6f9a6c0c9e0cf9a663bd"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"base16ct",
|
||||
|
|
|
@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later"
|
|||
homepage = "https://tetratto.com"
|
||||
|
||||
[dependencies]
|
||||
tetratto-core = "12.0.0"
|
||||
tetratto-core = "12.0.1"
|
||||
tetratto-shared = "12.0.5"
|
||||
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] }
|
||||
pathbufd = "0.1.4"
|
||||
|
|
|
@ -144,6 +144,14 @@ globalThis.tab_editor = () => {
|
|||
};
|
||||
|
||||
globalThis.tab_preview = async () => {
|
||||
if (
|
||||
!document
|
||||
.getElementById("preview_tab_button")
|
||||
.classList.contains("camo")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// render
|
||||
const res = await (
|
||||
await fetch("/api/v1/render", {
|
||||
|
@ -151,6 +159,7 @@ globalThis.tab_preview = async () => {
|
|||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
content: globalThis.editor.getValue(),
|
||||
metadata: globalThis.metadata_editor.getValue(),
|
||||
}),
|
||||
})
|
||||
).text();
|
||||
|
|
|
@ -111,7 +111,7 @@ article {
|
|||
}
|
||||
}
|
||||
|
||||
.container:not(#preview_tab) {
|
||||
.container:not(#preview_tab):not(#tabs_group) {
|
||||
margin: 10px auto 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -375,7 +375,7 @@ h6 {
|
|||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
margin: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,13 +21,14 @@
|
|||
("onclick" "tab_metadata()")
|
||||
(text "Metadata")))
|
||||
(div
|
||||
("class" "card tab tabs")
|
||||
("class" "card tab tabs container")
|
||||
("id" "tabs_group")
|
||||
(div
|
||||
("id" "editor_tab")
|
||||
("class" "tab fadein"))
|
||||
(div
|
||||
("id" "preview_tab")
|
||||
("class" "tab fadein hidden container"))
|
||||
("class" "tab fadein hidden"))
|
||||
(div
|
||||
("id" "metadata_tab")
|
||||
("class" "tab fadein hidden")))
|
||||
|
|
|
@ -21,13 +21,14 @@
|
|||
("onclick" "tab_metadata()")
|
||||
(text "Metadata")))
|
||||
(div
|
||||
("class" "card tab tabs")
|
||||
("class" "card tab tabs container")
|
||||
("id" "tabs_group")
|
||||
(div
|
||||
("id" "editor_tab")
|
||||
("class" "tab fadein"))
|
||||
(div
|
||||
("id" "preview_tab")
|
||||
("class" "tab fadein hidden container"))
|
||||
("class" "tab fadein hidden"))
|
||||
(div
|
||||
("id" "metadata_tab")
|
||||
("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 {
|
||||
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();
|
||||
|
@ -16,7 +18,13 @@ pub fn render_markdown(input: &str) -> String {
|
|||
allowed_attributes.insert("src");
|
||||
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) {
|
||||
|
@ -258,6 +266,11 @@ fn parse_comment_line(output: &mut String, _: &mut String, line: &str) {
|
|||
return;
|
||||
}
|
||||
|
||||
if line == "[..]" {
|
||||
output.push_str(" ");
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
// end
|
||||
output.push_str(&format!(
|
||||
"<span style=\"width: {}; height: {}\" 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")
|
||||
}
|
||||
"<span style=\"width: {size_lhs}; height: {size_rhs}\" class=\"img_sizer\">![{buffer}</span>"
|
||||
));
|
||||
|
||||
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.
|
||||
macro_rules! parser_ignores_pre {
|
||||
($body:ident, $input:ident) => {{
|
||||
|
@ -427,3 +598,11 @@ pub fn parse_comment(input: &str) -> String {
|
|||
pub fn parse_image_size(input: &str) -> String {
|
||||
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.gstatic.com\" crossorigin>
|
||||
<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 {
|
||||
if element == "*" {
|
||||
output.push_str(&format!(
|
||||
".container, .container * {{ font-size: {}; }}\n",
|
||||
EntryMetadata::css_escape(&size)
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push_str(&format!(
|
||||
".container {} {{ font-size: {}; }}\n",
|
||||
element,
|
||||
|
@ -426,7 +435,7 @@ impl EntryMetadata {
|
|||
if !self.content_font.is_empty() {
|
||||
output.push_str(&format!(
|
||||
".container {{ font-family: \"{}\", system-ui; }}",
|
||||
self.content_font
|
||||
self.content_font.replace("_", " ")
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -208,10 +208,20 @@ async fn editor_request(
|
|||
#[derive(Deserialize)]
|
||||
struct RenderMarkdown {
|
||||
content: String,
|
||||
metadata: String,
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue