Render markdown contained in bio and profile metadata values
This commit is contained in:
parent
43a70fb93f
commit
ee7a61833d
2 changed files with 113 additions and 15 deletions
|
@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::{HttpError, ValidationError};
|
||||||
use crate::identity::did::Did;
|
use crate::identity::did::Did;
|
||||||
use crate::mastodon_api::pagination::PageSize;
|
use crate::mastodon_api::pagination::PageSize;
|
||||||
use crate::mastodon_api::uploads::{UploadError, save_validated_b64_file};
|
use crate::mastodon_api::uploads::{UploadError, save_validated_b64_file};
|
||||||
|
@ -21,7 +21,10 @@ use crate::models::users::types::{
|
||||||
validate_local_username,
|
validate_local_username,
|
||||||
User,
|
User,
|
||||||
};
|
};
|
||||||
use crate::utils::files::get_file_url;
|
use crate::utils::{
|
||||||
|
files::get_file_url,
|
||||||
|
markdown::markdown_basic_to_html,
|
||||||
|
};
|
||||||
|
|
||||||
/// https://docs.joinmastodon.org/entities/field/
|
/// https://docs.joinmastodon.org/entities/field/
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -242,7 +245,14 @@ impl AccountUpdateData {
|
||||||
current_identity_proofs: &[IdentityProof],
|
current_identity_proofs: &[IdentityProof],
|
||||||
current_payment_options: &[PaymentOption],
|
current_payment_options: &[PaymentOption],
|
||||||
media_dir: &Path,
|
media_dir: &Path,
|
||||||
) -> Result<ProfileUpdateData, UploadError> {
|
) -> Result<ProfileUpdateData, HttpError> {
|
||||||
|
let maybe_bio = if let Some(ref note_source) = self.note_source {
|
||||||
|
let bio = markdown_basic_to_html(note_source)
|
||||||
|
.map_err(|_| ValidationError("invalid markdown"))?;
|
||||||
|
Some(bio)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let avatar = process_b64_image_field_value(
|
let avatar = process_b64_image_field_value(
|
||||||
self.avatar, current_avatar.clone(), media_dir,
|
self.avatar, current_avatar.clone(), media_dir,
|
||||||
)?;
|
)?;
|
||||||
|
@ -251,10 +261,16 @@ impl AccountUpdateData {
|
||||||
)?;
|
)?;
|
||||||
let identity_proofs = current_identity_proofs.to_vec();
|
let identity_proofs = current_identity_proofs.to_vec();
|
||||||
let payment_options = current_payment_options.to_vec();
|
let payment_options = current_payment_options.to_vec();
|
||||||
let extra_fields = self.fields_attributes.unwrap_or(vec![]);
|
let mut extra_fields = self.fields_attributes.unwrap_or(vec![]);
|
||||||
|
for extra_field in extra_fields.iter_mut() {
|
||||||
|
let value_source = extra_field.value_source.as_ref()
|
||||||
|
.ok_or(ValidationError("missing value source"))?;
|
||||||
|
extra_field.value = markdown_basic_to_html(value_source)
|
||||||
|
.map_err(|_| ValidationError("invalid markdown"))?;
|
||||||
|
};
|
||||||
let profile_data = ProfileUpdateData {
|
let profile_data = ProfileUpdateData {
|
||||||
display_name: self.display_name,
|
display_name: self.display_name,
|
||||||
bio: self.note,
|
bio: maybe_bio,
|
||||||
bio_source: self.note_source,
|
bio_source: self.note_source,
|
||||||
avatar,
|
avatar,
|
||||||
banner,
|
banner,
|
||||||
|
|
|
@ -59,7 +59,29 @@ fn node_to_markdown<'a>(
|
||||||
Ok(markdown)
|
Ok(markdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Supported markdown features:
|
fn document_to_html<'a>(
|
||||||
|
document: &'a AstNode<'a>,
|
||||||
|
options: &ComrakOptions,
|
||||||
|
) -> Result<String, MarkdownError> {
|
||||||
|
let mut output = vec![];
|
||||||
|
format_html(document, options, &mut output)?;
|
||||||
|
let html = String::from_utf8(output)?;
|
||||||
|
Ok(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes extra soft breaks from a HTML document generated by comrak
|
||||||
|
fn fix_linebreaks(html: &str) -> String {
|
||||||
|
html
|
||||||
|
// Fix hardbreaks
|
||||||
|
.replace("<br />\n", "<br>")
|
||||||
|
// Remove extra soft breaks
|
||||||
|
.replace(">\n<", "><")
|
||||||
|
.trim_end_matches('\n')
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Markdown Lite
|
||||||
|
/// Supported features:
|
||||||
/// - bold and italic
|
/// - bold and italic
|
||||||
/// - links and autolinks
|
/// - links and autolinks
|
||||||
/// - inline code and code blocks
|
/// - inline code and code blocks
|
||||||
|
@ -173,15 +195,61 @@ pub fn markdown_lite_to_html(text: &str) -> Result<String, MarkdownError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut output = vec![];
|
let html = document_to_html(root, &options)?;
|
||||||
format_html(root, &options, &mut output)?;
|
let html = fix_linebreaks(&html);
|
||||||
let html = String::from_utf8(output)?
|
Ok(html)
|
||||||
// Fix hardbreaks
|
}
|
||||||
.replace("<br />\n", "<br>")
|
|
||||||
// Remove extra soft breaks
|
/// Markdown Basic
|
||||||
.replace(">\n<", "><")
|
/// Supported features: links, linebreaks
|
||||||
.trim_end_matches('\n')
|
pub fn markdown_basic_to_html(text: &str) -> Result<String, MarkdownError> {
|
||||||
.to_string();
|
let options = build_comrak_options();
|
||||||
|
let arena = Arena::new();
|
||||||
|
let root = parse_document(
|
||||||
|
&arena,
|
||||||
|
text,
|
||||||
|
&options,
|
||||||
|
);
|
||||||
|
|
||||||
|
iter_nodes(root, &|node| {
|
||||||
|
let node_value = node.data.borrow().value.clone();
|
||||||
|
match node_value {
|
||||||
|
NodeValue::Document |
|
||||||
|
NodeValue::Text(_) |
|
||||||
|
NodeValue::Link(_) |
|
||||||
|
NodeValue::SoftBreak |
|
||||||
|
NodeValue::LineBreak
|
||||||
|
=> (),
|
||||||
|
NodeValue::Paragraph => {
|
||||||
|
if node.next_sibling().is_some() {
|
||||||
|
// If this is not the last paragraph,
|
||||||
|
// insert a line break, otherwise line break will not
|
||||||
|
// be preserved during HTML cleaning.
|
||||||
|
if let Some(last_child) = node.last_child() {
|
||||||
|
let last_child_value = &last_child.data.borrow().value;
|
||||||
|
if !matches!(last_child_value, NodeValue::LineBreak) {
|
||||||
|
let line_break = AstNode::from(NodeValue::LineBreak);
|
||||||
|
node.append(arena.alloc(line_break));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// Replace node with text node containing markdown
|
||||||
|
let markdown = node_to_markdown(node, &options)?;
|
||||||
|
for child in node.children() {
|
||||||
|
child.detach();
|
||||||
|
};
|
||||||
|
let text = NodeValue::Text(markdown.as_bytes().to_vec());
|
||||||
|
let mut borrowed_node = node.data.borrow_mut();
|
||||||
|
*borrowed_node = Ast::new(text);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let html = document_to_html(root, &options)?;
|
||||||
|
let html = fix_linebreaks(&html);
|
||||||
Ok(html)
|
Ok(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +303,20 @@ mod tests {
|
||||||
assert_eq!(html, format!("<p>{}</p>", text));
|
assert_eq!(html, format!("<p>{}</p>", text));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_markdown_basic_to_html() {
|
||||||
|
let text = "test **bold** test *italic* test ~~strike~~ with `code`, <span>html</span> and https://example.com\nnew line\n\nanother line";
|
||||||
|
let html = markdown_basic_to_html(text).unwrap();
|
||||||
|
let expected_html = concat!(
|
||||||
|
"<p>",
|
||||||
|
"test **bold** test *italic* test ~~strike~~ with `code`, <span>html</span>",
|
||||||
|
r#" and <a href="https://example.com">https://example.com</a>"#,
|
||||||
|
"<br>new line<br></p>",
|
||||||
|
"<p>another line</p>",
|
||||||
|
);
|
||||||
|
assert_eq!(html, expected_html);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_markdown_to_html() {
|
fn test_markdown_to_html() {
|
||||||
let text = "# heading\n\ntest";
|
let text = "# heading\n\ntest";
|
||||||
|
|
Loading…
Reference in a new issue