Render markdown contained in bio and profile metadata values

This commit is contained in:
silverpill 2022-12-18 19:20:57 +00:00
parent 43a70fb93f
commit ee7a61833d
2 changed files with 113 additions and 15 deletions

View file

@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::errors::ValidationError;
use crate::errors::{HttpError, ValidationError};
use crate::identity::did::Did;
use crate::mastodon_api::pagination::PageSize;
use crate::mastodon_api::uploads::{UploadError, save_validated_b64_file};
@ -21,7 +21,10 @@ use crate::models::users::types::{
validate_local_username,
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/
#[derive(Serialize)]
@ -242,7 +245,14 @@ impl AccountUpdateData {
current_identity_proofs: &[IdentityProof],
current_payment_options: &[PaymentOption],
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(
self.avatar, current_avatar.clone(), media_dir,
)?;
@ -251,10 +261,16 @@ impl AccountUpdateData {
)?;
let identity_proofs = current_identity_proofs.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 {
display_name: self.display_name,
bio: self.note,
bio: maybe_bio,
bio_source: self.note_source,
avatar,
banner,

View file

@ -59,7 +59,29 @@ fn node_to_markdown<'a>(
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
/// - links and autolinks
/// - inline code and code blocks
@ -173,15 +195,61 @@ pub fn markdown_lite_to_html(text: &str) -> Result<String, MarkdownError> {
Ok(())
})?;
let mut output = vec![];
format_html(root, &options, &mut output)?;
let html = String::from_utf8(output)?
// Fix hardbreaks
.replace("<br />\n", "<br>")
// Remove extra soft breaks
.replace(">\n<", "><")
.trim_end_matches('\n')
.to_string();
let html = document_to_html(root, &options)?;
let html = fix_linebreaks(&html);
Ok(html)
}
/// Markdown Basic
/// Supported features: links, linebreaks
pub fn markdown_basic_to_html(text: &str) -> Result<String, MarkdownError> {
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)
}
@ -235,6 +303,20 @@ mod tests {
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`, &lt;span&gt;html&lt;/span&gt;",
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]
fn test_markdown_to_html() {
let text = "# heading\n\ntest";