///|
/// Serializes HTML nodes to Markdown.
pub fn write_markdown(nodes : Array[HtmlNode]) -> String {
let writer = MarkdownWriter::{ buffer: StringBuilder() }
writer.write_nodes(nodes)
writer.buffer.to_string()
}
///|
priv struct MarkdownWriter {
buffer : StringBuilder
}
///|
fn MarkdownWriter::write_nodes(
self : MarkdownWriter,
nodes : Array[HtmlNode],
) -> Unit {
for node in nodes {
self.write_node(node)
}
}
///|
fn MarkdownWriter::write_node(self : MarkdownWriter, node : HtmlNode) -> Unit {
match node {
TextNode(value) => self.buffer.write_string(escape_markdown_text(value))
ForceWrite => ()
Element(tag~, attributes~, children~, ..) =>
match tag {
"p" => {
self.write_id_anchor(attributes)
self.write_nodes(children)
self.buffer.write_string("\n\n")
}
"br" => self.buffer.write_string(" \n")
"strong" => {
self.buffer.write_string("__")
self.write_nodes(children)
self.buffer.write_string("__")
}
"em" => {
self.buffer.write_string("*")
self.write_nodes(children)
self.buffer.write_string("*")
}
"a" => self.write_anchor(attributes, children)
"img" => self.write_image(attributes)
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" =>
self.write_heading(tag, attributes, children)
"ul" => self.write_list(children, ordered=false, indent=0, nested=false)
"ol" => self.write_list(children, ordered=true, indent=0, nested=false)
"li" =>
self.write_list_item(
attributes,
children,
ordered=false,
indent=0,
number=1,
)
_ => {
self.write_id_anchor(attributes)
self.write_nodes(children)
}
}
}
}
///|
fn MarkdownWriter::write_id_anchor(
self : MarkdownWriter,
attributes : Map[String, String],
) -> Unit {
match attributes.get("id") {
Some(id) => {
self.buffer.write_string("")
}
None => ()
}
}
///|
fn MarkdownWriter::write_anchor(
self : MarkdownWriter,
attributes : Map[String, String],
children : Array[HtmlNode],
) -> Unit {
self.write_id_anchor(attributes)
match attributes.get("href") {
Some(href) => {
self.buffer.write_string("[")
self.write_nodes(children)
self.buffer.write_string("](")
self.buffer.write_string(href)
self.buffer.write_string(")")
}
None => self.write_nodes(children)
}
}
///|
fn MarkdownWriter::write_image(
self : MarkdownWriter,
attributes : Map[String, String],
) -> Unit {
let src = attributes.get_or_default("src", "")
let alt = attributes.get_or_default("alt", "")
if src != "" || alt != "" {
self.buffer.write_string("
self.buffer.write_string(src)
self.buffer.write_string(")")
}
}
///|
fn MarkdownWriter::write_heading(
self : MarkdownWriter,
tag : String,
attributes : Map[String, String],
children : Array[HtmlNode],
) -> Unit {
let level = tag[1].to_int() - ('0' : UInt16).to_int()
for _ in 0.. Unit {
if nested {
self.buffer.write_string("\n")
}
let mut index = 1
for child in children {
match child {
Element(tag="li", attributes=li_attributes, children=li_children, ..) => {
self.write_list_item(
li_attributes,
li_children,
ordered~,
indent~,
number=index,
)
index = index + 1
}
_ => self.write_node(child)
}
}
if !nested {
self.buffer.write_string("\n")
}
}
///|
fn MarkdownWriter::write_list_item(
self : MarkdownWriter,
attributes : Map[String, String],
children : Array[HtmlNode],
ordered~ : Bool,
indent~ : Int,
number~ : Int,
) -> Unit {
for _ in 0.. Bool {
let mut ended_by_nested_list = false
for child in children {
match child {
Element(tag="ul", children=nested_children, ..) => {
self.write_list(
nested_children,
ordered=false,
indent=nested_indent,
nested=true,
)
ended_by_nested_list = true
}
Element(tag="ol", children=nested_children, ..) => {
self.write_list(
nested_children,
ordered=true,
indent=nested_indent,
nested=true,
)
ended_by_nested_list = true
}
_ => {
self.write_node(child)
ended_by_nested_list = false
}
}
}
ended_by_nested_list
}
///|
/// Escapes text for Markdown output.
pub fn escape_markdown_text(value : String) -> String {
let builder = StringBuilder()
for char in value {
match char {
'\\'
| '`'
| '*'
| '_'
| '{'
| '}'
| '['
| ']'
| '('
| ')'
| '#'
| '+'
| '-'
| '.'
| '!' => {
builder.write_string("\\")
builder.write_char(char)
}
_ => builder.write_char(char)
}
}
builder.to_string()
}