///|
pub(all) enum MarkdownBlockKind {
Heading(Int)
Paragraph
UnorderedListItem
TaskListItem(Bool)
OrderedListItem(Int)
Blockquote
CodeBlock
HorizontalRule
Table
FootnoteDefinition(String)
HtmlBlock
FrontMatter
} derive(Eq, Debug, ToJson)
///|
pub(all) struct MarkdownBlock {
kind : MarkdownBlockKind
text : String
inlines : Array[MarkdownInline]
table_alignments : Array[@core.TextAlign?]
code_language : String?
} derive(Eq, Debug, ToJson)
///|
pub(all) struct MarkdownFrontMatterParseResult {
block : MarkdownBlock
next_index : Int
} derive(Eq, Debug)
///|
fn markdown_front_matter_block_at(
lines : Array[String],
) -> MarkdownFrontMatterParseResult? {
if lines.length() < 2 || lines[0].trim().to_owned() != "---" {
return None
}
let content : Array[String] = []
let mut index = 1
while index < lines.length() {
if lines[index].trim().to_owned() == "---" {
return Some({
block: markdown_block(FrontMatter, markdown_join_lines(content)),
next_index: index + 1,
})
}
content.push(lines[index])
index = index + 1
}
None
}
///|
pub fn parse_markdown(text : String) -> Array[MarkdownBlock] {
let blocks : Array[MarkdownBlock] = []
let definitions = markdown_reference_definitions(text)
let lines = markdown_split_lines(text)
match markdown_front_matter_block_at(lines) {
Some(front_matter) => {
blocks.push(front_matter.block)
let remaining_lines : Array[String] = []
for index in front_matter.next_index.. ()
}
let code_lines : Array[String] = []
let mut code_language : String? = None
let mut code_fence = ""
let mut in_code = false
let mut index = 0
while index < lines.length() {
let line = lines[index]
if in_code {
if markdown_closing_code_fence(line, code_fence) {
blocks.push(
markdown_code_block(markdown_join_lines(code_lines), code_language),
)
code_lines.clear()
code_fence = ""
in_code = false
} else {
code_lines.push(line)
}
} else {
match markdown_opening_code_fence(line) {
Some(fence) => {
in_code = true
code_fence = fence.marker
code_language = fence.info
}
None =>
if line.trim().to_owned() != "" {
match markdown_table_block_at(lines, index) {
Some(table) => {
blocks.push(table.block)
index = table.end_index
}
None =>
match markdown_setext_heading_level_at(lines, index) {
Some(level) => {
let text = line.trim().to_owned()
blocks.push({
kind: MarkdownBlockKind::Heading(level),
text,
inlines: parse_markdown_inlines_with_definitions(
text, definitions,
),
table_alignments: [],
code_language: None,
})
index = index + 1
}
None => {
let parsed = @md.parse(line)
for block in parsed.document.children {
append_markdown_ast_block(
blocks,
block,
definitions,
source_text=Some(line),
)
}
}
}
}
}
}
}
index = index + 1
}
if in_code {
blocks.push(
markdown_code_block(markdown_join_lines(code_lines), code_language),
)
}
blocks
}
///|
fn markdown_setext_heading_level_at(lines : Array[String], index : Int) -> Int? {
if index + 1 >= lines.length() {
return None
}
let text = lines[index].trim().to_owned()
if text == "" ||
markdown_opening_code_fence(lines[index]) is Some(_) ||
!markdown_is_setext_paragraph_candidate(lines[index]) {
return None
}
markdown_setext_underline_level(lines[index + 1])
}
///|
fn markdown_is_setext_paragraph_candidate(line : String) -> Bool {
let parsed = @md.parse(line)
match parsed.document.children {
[Paragraph(_)] => true
_ => false
}
}
///|
fn markdown_setext_underline_level(line : String) -> Int? {
let text = line.trim().to_owned()
if text == "" {
return None
}
let chars = text.to_array()
let marker = chars[0]
if marker != '=' && marker != '-' {
return None
}
for ch in chars {
if ch != marker {
return None
}
}
Some(if marker == '=' { 1 } else { 2 })
}
///|
pub(all) struct MarkdownCodeFenceInfo {
marker : String
info : String?
} derive(Eq, Debug)
///|
fn markdown_opening_code_fence(line : String) -> MarkdownCodeFenceInfo? {
let chars = line.to_array()
let indent = markdown_leading_indent_length(chars)
if chars.length() < indent + 3 {
return None
}
if chars[indent] != '`' ||
chars[indent + 1] != '`' ||
chars[indent + 2] != '`' {
return None
}
let mut fence_end = indent + 3
while fence_end < chars.length() && chars[fence_end] == '`' {
fence_end = fence_end + 1
}
for index in fence_end.. String? {
let info = String::from_array(chars[start:]).trim().to_owned()
if info == "" {
None
} else {
Some(info)
}
}
///|
fn markdown_closing_code_fence(line : String, fence : String) -> Bool {
let chars = line.to_array()
let fence_chars = fence.to_array()
let indent = markdown_leading_indent_length(chars)
if fence_chars.length() < 3 || chars.length() < indent + fence_chars.length() {
return false
}
for index in 0.. Int {
let mut index = 0
while index < chars.length() && (chars[index] == ' ' || chars[index] == '\t') {
index = index + 1
}
index
}
///|
fn append_markdown_ast_block(
blocks : Array[MarkdownBlock],
block : @md.Block,
definitions : Array[MarkdownReferenceDefinition],
source_text? : String? = None,
) -> Unit {
match block {
Heading(level~, children~, ..) => {
let text = match source_text {
Some(source_text) => markdown_heading_visible_source_text(source_text)
None => markdown_inline_text(children)
}
// A heading source is only rendered as a heading block when it has `#`
// markers, a space/tab separator, AND actual content after the separator
// (e.g. `"# text"`). Bare markers (`"#"`, `"##"`) or markers with a
// separator but no content yet (`"# "`, `"## "`) are treated as plain
// text so the user can still see the `#` they typed and the cursor stays
// at normal size until real heading content is entered.
let is_real_heading = match source_text {
Some(source_text) => markdown_heading_source_is_renderable(source_text)
None => true
}
if level >= 1 && level <= 6 && is_real_heading {
blocks.push({
kind: MarkdownBlockKind::Heading(level),
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
})
} else {
let para_text = match source_text {
Some(source_text) if !is_real_heading => source_text
_ => markdown_heading_source(level, text)
}
blocks.push({
kind: MarkdownBlockKind::Paragraph,
text: para_text,
inlines: parse_markdown_inlines_with_definitions(
para_text, definitions,
),
table_alignments: [],
code_language: None,
})
}
}
Paragraph(children~, ..) => {
let text = match source_text {
Some(source_text) => source_text
None => markdown_inline_text(children)
}
blocks.push({
kind: MarkdownBlockKind::Paragraph,
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
})
}
ThematicBreak(..) =>
blocks.push(markdown_block(MarkdownBlockKind::HorizontalRule, ""))
FencedCode(code~, info~, ..) =>
blocks.push(
markdown_code_block(
markdown_trim_single_trailing_newline(code),
markdown_normalize_code_info(Some(info)),
),
)
IndentedCode(code~, ..) =>
blocks.push(
markdown_code_block(markdown_trim_single_trailing_newline(code), None),
)
Blockquote(children~, ..) => {
let text = markdown_blocks_text(children)
blocks.push({
kind: MarkdownBlockKind::Blockquote,
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
})
}
BulletList(marker~, items~, ..) =>
if marker is Plus {
for item in items {
let text = markdown_list_item_text(item)
let plus_text = "+ " + text
blocks.push({
kind: MarkdownBlockKind::Paragraph,
text: plus_text,
inlines: parse_markdown_inlines_with_definitions(
plus_text, definitions,
),
table_alignments: [],
code_language: None,
})
}
} else {
for item in items {
let text = markdown_list_item_text(item)
match item.checked {
Some(checked) =>
blocks.push({
kind: MarkdownBlockKind::TaskListItem(checked),
text,
inlines: parse_markdown_inlines_with_definitions(
text, definitions,
),
table_alignments: [],
code_language: None,
})
None =>
blocks.push({
kind: MarkdownBlockKind::UnorderedListItem,
text,
inlines: parse_markdown_inlines_with_definitions(
text, definitions,
),
table_alignments: [],
code_language: None,
})
}
}
}
OrderedList(start~, items~, ..) => {
let mut number = start
for item in items {
let text = markdown_list_item_text(item)
blocks.push({
kind: MarkdownBlockKind::OrderedListItem(number),
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
})
number = number + 1
}
}
BlankLines(..) => ()
HtmlBlock(html~, ..) =>
blocks.push(markdown_block(MarkdownBlockKind::HtmlBlock, html))
Table(header~, rows~, ..) => {
let text = markdown_table_text(header, rows)
blocks.push(markdown_table_block(text, []))
}
FootnoteDefinition(label~, children~, ..) => {
let text = markdown_blocks_text(children)
blocks.push({
kind: MarkdownBlockKind::FootnoteDefinition(label),
text,
inlines: parse_markdown_inlines_with_definitions(text, definitions),
table_alignments: [],
code_language: None,
})
}
}
}
///|
fn markdown_trim_single_trailing_newline(text : String) -> String {
let chars = text.to_array()
if chars.length() > 0 && chars[chars.length() - 1] == '\n' {
String::from_array(chars[:chars.length() - 1])
} else {
text
}
}
///|
fn markdown_block(kind : MarkdownBlockKind, text : String) -> MarkdownBlock {
let inlines = match kind {
CodeBlock | HorizontalRule | Table | FrontMatter => []
_ => parse_markdown_inlines(text)
}
{ kind, text, inlines, table_alignments: [], code_language: None }
}
///|
fn markdown_code_block(text : String, code_language : String?) -> MarkdownBlock {
{
kind: MarkdownBlockKind::CodeBlock,
text,
inlines: [],
table_alignments: [],
code_language,
}
}
///|
fn markdown_normalize_code_info(info : String?) -> String? {
match info {
Some(value) => {
let trimmed = value.trim().to_owned()
if trimmed == "" {
None
} else {
Some(trimmed)
}
}
None => None
}
}
///|
pub fn markdown_split_lines(text : String) -> Array[String] {
let lines : Array[String] = []
let current : Array[Char] = []
for ch in text {
if ch == '\n' {
lines.push(String::from_array(current))
current.clear()
} else {
current.push(ch)
}
}
lines.push(String::from_array(current))
lines
}
///|
pub fn markdown_join_lines(lines : Array[String]) -> String {
let chars : Array[Char] = []
for index, line in lines {
if index > 0 {
chars.push('\n')
}
chars.append(line.to_array())
}
String::from_array(chars)
}