element here so other apps could recognize the included language info.
if let BufferBlockStyle::CodeBlock { code_block_type } = &text_block.style {
let language_name = QualName::new(None, ns!(), "class".into());
let language_value = format!(
"language-{}",
code_block_type
.to_markdown_representation(MarkdownStyle::Export {
app_context: Some(self.context),
should_not_escape_markdown_punctuation: false,
})
.to_lowercase()
);
let attrs = [(&language_name, language_value.as_str())];
let code_block_name = QualName::new(None, ns!(html), "code".into());
serializer.start_elem(code_block_name.clone(), attrs.into_iter())?;
}
} else if let BufferBlockStyle::TaskList { complete, .. } = text_block.style {
// For tasklists, the formatting is similar to unordered lists but with an additional
// before the text fragments.
let type_name = QualName::new(None, ns!(), "type".into());
let checked_name = QualName::new(None, ns!(), "checked".into());
let mut checkbox_attrs = vec![(&type_name, "checkbox")];
if complete {
checkbox_attrs.push((&checked_name, ""));
}
let tag_name = QualName::new(None, ns!(html), "input".into());
serializer.start_elem(tag_name.clone(), checkbox_attrs.into_iter())?;
serializer.end_elem(tag_name)?;
}
for (idx, runs) in text_block.block.iter().enumerate() {
let mut text = runs.run.as_str();
// Strip trailing newline since or already includes a linebreak.
if idx == text_block.block.len() - 1
&& let Some(removed_trailing_newline) = runs.run.strip_suffix('\n')
{
text = removed_trailing_newline;
}
if let Some(link) = runs.text_styles.link_content() {
let tag_name = QualName::new(None, ns!(html), "a".into());
let attribute_name = QualName::new(None, ns!(), "href".into());
serializer.start_elem(
tag_name,
vec![(&attribute_name, link.as_str())].into_iter(),
)?;
}
if runs.text_styles.is_at_least_bold() {
let tag_name = QualName::new(None, ns!(html), "strong".into());
serializer.start_elem(tag_name, iter::empty())?;
}
if runs.text_styles.is_italic() {
let tag_name = QualName::new(None, ns!(html), "em".into());
serializer.start_elem(tag_name, iter::empty())?;
}
if runs.text_styles.is_strikethrough() {
let tag_name = QualName::new(None, ns!(html), "s".into());
serializer.start_elem(tag_name, iter::empty())?;
}
if runs.text_styles.is_underlined() {
let tag_name = QualName::new(None, ns!(html), "u".into());
serializer.start_elem(tag_name, iter::empty())?;
}
if runs.text_styles.is_inline_code() {
let tag_name = QualName::new(None, ns!(html), "code".into());
serializer.start_elem(tag_name, iter::empty())?;
}
serializer.write_text(text)?;
if runs.text_styles.is_inline_code() {
serializer.end_elem(QualName::new(None, ns!(html), "code".into()))?;
}
if runs.text_styles.is_underlined() {
serializer.end_elem(QualName::new(None, ns!(html), "u".into()))?;
}
if runs.text_styles.is_strikethrough() {
serializer.end_elem(QualName::new(None, ns!(html), "s".into()))?;
}
if runs.text_styles.is_italic() {
serializer.end_elem(QualName::new(None, ns!(html), "em".into()))?;
}
if runs.text_styles.is_at_least_bold() {
serializer.end_elem(QualName::new(None, ns!(html), "strong".into()))?;
}
if runs.text_styles.is_link() {
serializer.end_elem(QualName::new(None, ns!(html), "a".into()))?;
}
}
if let Some(tag_name) = tag_name {
if matches!(text_block.style, BufferBlockStyle::CodeBlock { .. }) {
let code_block_name = QualName::new(None, ns!(html), "code".into());
serializer.end_elem(code_block_name)?;
}
serializer.end_elem(tag_name)?;
}
}
}
}
// If the copied region ended on a list element, close the parent list.
if let Some(end_container) = previous_block_type.and_then(|style| container_element(&style))
{
end_list_with_indent(serializer, end_container, active_indent_level)?
}
Ok(())
}
}
fn start_list_with_indent(
serializer: &mut S,
tag: &str,
indent_level: usize,
start: Option,
) -> io::Result<()>
where
S: Serializer,
{
let start_attr = start.map(|start| {
(
QualName::new(None, ns![], "start".into()),
start.to_string(),
)
});
for i in 0..indent_level {
// If an ordered list item had a custom number, we make it the start of the immediate parent
// list. As a result, custom numbers on items within a list are ignored, matching the
// behavior for Markdown.
let list_attrs = start_attr.as_ref().and_then(|(attr, value)| {
if i == indent_level - 1 {
Some((attr, value.as_str()))
} else {
None
}
});
serializer.start_elem(
QualName::new(None, ns!(html), tag.into()),
list_attrs.into_iter(),
)?;
serializer.start_elem(QualName::new(None, ns!(html), "li".into()), iter::empty())?;
}
Ok(())
}
fn end_list_with_indent(serializer: &mut S, tag: &str, indent_level: usize) -> io::Result<()>
where
S: Serializer,
{
for _ in 0..indent_level {
serializer.end_elem(QualName::new(None, ns!(html), "li".into()))?;
serializer.end_elem(QualName::new(None, ns!(html), tag.into()))?;
}
Ok(())
}
/// The parent element wrapping consecutive blocks of a given style.
///
/// This only applies to list blocks.
fn container_element(style: &BufferBlockStyle) -> Option<&'static str> {
match style {
BufferBlockStyle::OrderedList { .. } => Some("ol"),
BufferBlockStyle::UnorderedList { .. } | BufferBlockStyle::TaskList { .. } => Some("ul"),
_ => None,
}
}
fn formatted_text_line_to_block_style(line: &FormattedTextLine) -> Option {
match line {
FormattedTextLine::Heading(header) => Some(BufferBlockStyle::Header {
header_size: header
.heading_size
.try_into()
.unwrap_or(BlockHeaderSize::Header1),
}),
FormattedTextLine::CodeBlock(text) => Some(BufferBlockStyle::CodeBlock {
code_block_type: text.into(),
}),
FormattedTextLine::Table(table) => Some(BufferBlockStyle::table(table.alignments.clone())),
FormattedTextLine::UnorderedList(list) => Some(BufferBlockStyle::UnorderedList {
indent_level: ListIndentLevel::from_usize(list.indent_level),
}),
FormattedTextLine::TaskList(list) => Some(BufferBlockStyle::TaskList {
indent_level: ListIndentLevel::from_usize(list.indent_level),
complete: list.complete,
}),
FormattedTextLine::OrderedList(list) => Some(BufferBlockStyle::OrderedList {
indent_level: ListIndentLevel::from_usize(list.indented_text.indent_level),
number: list.number,
}),
FormattedTextLine::Line(_) => Some(BufferBlockStyle::PlainText),
FormattedTextLine::LineBreak => None,
FormattedTextLine::HorizontalRule => Some(BufferBlockStyle::PlainText),
// TODO(kevin): handle embedded objects in buffer.
FormattedTextLine::Embedded(_) => None,
// Images are block items, not block styles
FormattedTextLine::Image(_) => None,
}
}
fn alignment_to_html_value(alignment: &FormattedTableAlignment) -> &'static str {
match alignment {
FormattedTableAlignment::Left => "left",
FormattedTableAlignment::Center => "center",
FormattedTableAlignment::Right => "right",
}
}
fn serialize_table_cell_inline(
serializer: &mut S,
inline: &[FormattedTextFragment],
) -> io::Result<()>
where
S: Serializer,
{
for fragment in inline {
let styles = TextStylesWithMetadata::from(fragment.styles.clone());
if let Some(link) = styles.link_content() {
let tag = QualName::new(None, ns!(html), "a".into());
let href = QualName::new(None, ns!(), "href".into());
serializer.start_elem(tag, vec![(&href, link.as_str())].into_iter())?;
}
if styles.is_at_least_bold() {
serializer.start_elem(
QualName::new(None, ns!(html), "strong".into()),
iter::empty(),
)?;
}
if styles.is_italic() {
serializer.start_elem(QualName::new(None, ns!(html), "em".into()), iter::empty())?;
}
if styles.is_strikethrough() {
serializer.start_elem(QualName::new(None, ns!(html), "s".into()), iter::empty())?;
}
if styles.is_underlined() {
serializer.start_elem(QualName::new(None, ns!(html), "u".into()), iter::empty())?;
}
if styles.is_inline_code() {
serializer.start_elem(QualName::new(None, ns!(html), "code".into()), iter::empty())?;
}
serializer.write_text(&fragment.text)?;
if styles.is_inline_code() {
serializer.end_elem(QualName::new(None, ns!(html), "code".into()))?;
}
if styles.is_underlined() {
serializer.end_elem(QualName::new(None, ns!(html), "u".into()))?;
}
if styles.is_strikethrough() {
serializer.end_elem(QualName::new(None, ns!(html), "s".into()))?;
}
if styles.is_italic() {
serializer.end_elem(QualName::new(None, ns!(html), "em".into()))?;
}
if styles.is_at_least_bold() {
serializer.end_elem(QualName::new(None, ns!(html), "strong".into()))?;
}
if styles.link_content().is_some() {
serializer.end_elem(QualName::new(None, ns!(html), "a".into()))?;
}
}
Ok(())
}
fn serialize_table_to_html(serializer: &mut S, table: &FormattedTable) -> io::Result<()>
where
S: Serializer,
{
let table_tag = QualName::new(None, ns!(html), "table".into());
serializer.start_elem(table_tag.clone(), iter::empty())?;
let thead_tag = QualName::new(None, ns!(html), "thead".into());
serializer.start_elem(thead_tag.clone(), iter::empty())?;
let tr_tag = QualName::new(None, ns!(html), "tr".into());
serializer.start_elem(tr_tag.clone(), iter::empty())?;
let align_attr = QualName::new(None, ns!(), "align".into());
for (i, header) in table.headers.iter().enumerate() {
let alignment = table
.alignments
.get(i)
.unwrap_or(&FormattedTableAlignment::Left);
let align_value = alignment_to_html_value(alignment);
let th_tag = QualName::new(None, ns!(html), "th".into());
serializer.start_elem(th_tag.clone(), vec![(&align_attr, align_value)].into_iter())?;
serialize_table_cell_inline(serializer, header)?;
serializer.end_elem(th_tag)?;
}
serializer.end_elem(tr_tag)?;
serializer.end_elem(thead_tag)?;
let tbody_tag = QualName::new(None, ns!(html), "tbody".into());
serializer.start_elem(tbody_tag.clone(), iter::empty())?;
for row in &table.rows {
let tr_tag = QualName::new(None, ns!(html), "tr".into());
serializer.start_elem(tr_tag.clone(), iter::empty())?;
for (i, cell) in row.iter().enumerate() {
let alignment = table
.alignments
.get(i)
.unwrap_or(&FormattedTableAlignment::Left);
let align_value = alignment_to_html_value(alignment);
let td_tag = QualName::new(None, ns!(html), "td".into());
serializer.start_elem(td_tag.clone(), vec![(&align_attr, align_value)].into_iter())?;
serialize_table_cell_inline(serializer, cell)?;
serializer.end_elem(td_tag)?;
}
serializer.end_elem(tr_tag)?;
}
serializer.end_elem(tbody_tag)?;
serializer.end_elem(table_tag)?;
Ok(())
}
fn inline_to_markdown(inline: &FormattedTextInline) -> String {
let mut markdown = String::new();
let mut previous_styles = TextStylesWithMetadata::default();
for fragment in inline {
let next_styles = TextStylesWithMetadata::from(fragment.styles.clone());
let content = BufferMarkdownParser::append_formatting(
&previous_styles,
&next_styles,
fragment.text.as_str(),
&mut markdown,
);
previous_styles = next_styles;
BufferMarkdownParser::append_content(content, true, &mut markdown);
}
BufferMarkdownParser::append_formatting(
&previous_styles,
&TextStylesWithMetadata::default(),
"",
&mut markdown,
);
markdown
}
fn append_gfm_table_row(cells: &[String], buf: &mut String) {
buf.push('|');
for cell in cells {
buf.push(' ');
buf.push_str(&escape_gfm_table_cell(cell));
buf.push(' ');
buf.push('|');
}
buf.push('\n');
}
fn alignment_to_gfm_separator(alignment: FormattedTableAlignment) -> String {
match alignment {
FormattedTableAlignment::Left => "---".to_string(),
FormattedTableAlignment::Center => ":---:".to_string(),
FormattedTableAlignment::Right => "---:".to_string(),
}
}
fn escape_gfm_table_cell(cell: &str) -> String {
cell.replace('|', "\\|")
}
#[cfg(test)]
#[path = "markdown_tests.rs"]
mod tests;