///|
priv struct BlockMatchData {
context : Context
masq : Array[String]
terminator : String
}
///|
fn delimited_block_info(tip : String) -> (Context, Array[String])? {
match tip {
"--" =>
Some(
(
Open,
[
"comment", "example", "literal", "listing", "pass", "quote", "sidebar",
"source", "verse", "admonition", "abstract", "partintro",
],
),
)
"----" => Some((Listing, ["literal", "source"]))
"...." => Some((Literal, ["listing", "source"]))
"====" => Some((Example, ["admonition"]))
"****" => Some((Sidebar, []))
"____" => Some((Quote, ["verse"]))
"++++" => Some((Pass, ["stem", "latexmath", "asciimath"]))
"|===" | ",===" | ":===" | "!===" => Some((Table, []))
"~~~~" => Some((Open, ["abstract", "partintro"]))
"////" => Some((Comment, []))
"```" => Some((FencedCode, []))
_ => None
}
}
///|
fn is_delimited_block_head(head : String) -> Bool {
match head {
"--"
| ".."
| "=="
| "**"
| "__"
| "++"
| "|="
| ",="
| ":="
| "!="
| "~~"
| "//"
| "``" => true
_ => false
}
}
///|
/// Checks whether the line is a delimited block delimiter (Ruby `is_delimited_block?`).
fn is_delimited_block(line0 : String) -> BlockMatchData? {
let mut line = line0
let mut line_len = line.length()
if !(line_len > 1 && is_delimited_block_head(@rb.slice(line, 0, 2))) {
return None
}
let mut tip = ""
let mut tip_len = 0
if line_len == 2 {
tip = line
tip_len = 2
} else {
if line_len < 5 {
tip = line
tip_len = line_len
} else {
tip = @rb.slice(line, 0, 4)
tip_len = 4
}
if compliance.markdown_syntax && tip.has_prefix("`") {
if tip_len == 4 {
if tip == "````" {
return None
}
tip = @rb.chop(tip)
if tip != "```" {
return None
}
line = tip
line_len = 3
tip_len = 3
} else if tip != "```" {
return None
}
} else if tip_len == 3 {
return None
}
}
match delimited_block_info(tip) {
Some((context, masq)) => {
let tail = @rb.from(tip, tip.length() - 1)
if line_len == tip_len ||
(tip_len == 4 && is_uniform(@rb.from(line, 1), tail, line_len - 1)) {
Some({ context, masq, terminator: line, })
} else {
None
}
}
None => None
}
}
///|
fn start_of_block(l : String) -> Bool {
(l.has_prefix("[") && block_attribute_line_rx.matches(l)) ||
is_delimited_block(l) is Some(_)
}
///|
fn start_of_list(l : String) -> Bool {
any_list_rx.matches(l)
}
///|
fn start_of_block_or_list(l : String) -> Bool {
is_delimited_block(l) is Some(_) ||
(l.has_prefix("[") && block_attribute_line_rx.matches(l)) ||
any_list_rx.matches(l)
}
///|
fn read_paragraph_lines(
reader : Reader,
break_at_list : Bool,
skip_line_comments? : Bool = false,
skip_processing? : Bool = false,
) -> Array[String] {
reader.read_lines_until(
break_on_blank_lines=true,
break_on_list_continuation=true,
preserve_last_line=true,
skip_line_comments~,
skip_processing~,
break_if?=if compliance.block_terminates_paragraph {
Some(if break_at_list { start_of_block_or_list } else { start_of_block })
} else if break_at_list {
Some(start_of_list)
} else {
None
},
)
}
///|
fn layout_break_context(ch0 : String) -> Context? {
match ch0 {
"'" | "-" | "*" | "_" => Some(ThematicBreak)
"<" => Some(PageBreak)
_ => None
}
}
///|
let admonition_styles : Array[String] = [
"NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION",
]
///|
let paragraph_styles : Array[String] = [
"comment", "example", "literal", "listing", "normal", "open", "pass", "quote",
"sidebar", "source", "verse", "abstract", "partintro",
]
///|
let verbatim_styles : Array[String] = ["literal", "listing", "source", "verse"]
///|
/// Parses the next block (Ruby `Parser.next_block`).
fn next_block(
reader : Reader,
parent : Node,
attributes : Attributes,
text_only? : Bool = false,
parse_metadata? : Bool = true,
list_type? : Context,
) -> Node? {
guard reader.skip_blank_lines() is Some(skipped) else { return None }
let mut text_only = text_only
if text_only && skipped > 0 {
text_only = false
}
let document = parent.document()
if parse_metadata {
while parse_block_metadata_line(reader, document, attributes, text_only~) {
reader.shift() |> ignore
if reader.skip_blank_lines() is None {
return None
}
}
}
let extensions = document.extensions()
let block_extensions = match extensions {
Some(e) => e.has_blocks()
None => false
}
let block_macro_extensions = match extensions {
Some(e) => e.has_block_macros()
None => false
}
reader.mark() |> ignore
let this_line = reader.read_line().unwrap_or("")
let doc_attrs = document.attributes
let mut style = attributes.pos_str(1)
let mut block : Node? = None
let mut block_context : Context = Paragraph
let mut cloaked_context : Context = Paragraph
let mut terminator = ""
let delimited_block = is_delimited_block(this_line)
match delimited_block {
Some(db) => {
block_context = db.context
cloaked_context = db.context
terminator = db.terminator
match style {
Some(s) =>
if s != block_context.name() {
if db.masq.contains(s) {
block_context = Context::from_name(s)
} else if db.masq.contains("admonition") &&
admonition_styles.contains(s) {
block_context = Admonition
} else if block_extensions &&
extensions.unwrap().registered_for_block(s, block_context)
is Some(_) {
block_context = Context::from_name(s)
} else {
if logger().is_debug() {
log_debug(
"unknown style for \{block_context.name()} block: \{s}",
source_location=reader.cursor_at_mark(),
)
}
style = Some(block_context.name())
}
}
None => {
style = Some(block_context.name())
attributes.set_str("style", block_context.name())
}
}
}
None => ()
}
if delimited_block is None {
// loop that executes once (Ruby `while true ... break`)
let mut ch0 = ""
let mut indented = false
let mut done = false
if style is Some(s) &&
compliance.strict_verbatim_paragraphs &&
verbatim_styles.contains(s) {
block_context = Context::from_name(s)
cloaked_context = Paragraph
reader.unshift_line(this_line)
done = true
}
if !done {
if text_only {
indented = this_line.has_prefix(" ") || this_line.has_prefix("\t")
} else if this_line.has_prefix(" ") {
indented = true
ch0 = " "
let stripped = @rb.lstrip(this_line)
if compliance.markdown_syntax &&
(
stripped.has_prefix("-") ||
stripped.has_prefix("*") ||
stripped.has_prefix("_")
) &&
markdown_thematic_break_rx.matches(this_line) {
block = Some(
Node::new_block(parent, ThematicBreak, content_model=Empty),
)
done = true
}
} else if this_line.has_prefix("\t") {
indented = true
ch0 = "\t"
} else {
indented = false
ch0 = first_char(this_line)
let lb = if compliance.markdown_syntax {
layout_break_context(ch0)
} else if ch0 == "'" || ch0 == "<" {
layout_break_context(ch0)
} else {
None
}
match lb {
Some(lbc) if (if compliance.markdown_syntax {
ext_layout_break_rx.matches(this_line)
} else {
is_uniform(this_line, ch0, this_line.length()) &&
this_line.length() > 2
}) => {
block = Some(Node::new_block(parent, lbc, content_model=Empty))
done = true
}
_ =>
if this_line.has_suffix("]") && this_line.contains("::") {
if (
ch0 == "i" ||
this_line.has_prefix("video:") ||
this_line.has_prefix("audio:")
) &&
block_media_macro_rx.find(this_line) is Some(m) {
let blk_ctx = Context::from_name(m.at(1))
let mut target = m.at(2)
let b = Node::new_block(parent, blk_ctx, content_model=Empty)
match m.group(3) {
Some(blk_attrs) => {
let posattrs : Array[String?] = match blk_ctx {
Video => [Some("poster"), Some("width"), Some("height")]
Audio => []
_ => [Some("alt"), Some("width"), Some("height")]
}
b.parse_attributes(
blk_attrs,
posattrs,
sub_input=true,
into=attributes,
)
|> ignore
}
None => ()
}
if attributes.contains("style") {
attributes.remove("style") |> ignore
}
if target.contains("{") {
let expanded = b.sub_attributes(target)
if expanded == "" &&
doc_attrs
.str("attribute-missing")
.unwrap_or(compliance.attribute_missing) ==
"drop-line" &&
b.sub_attributes(
target + " ",
attribute_missing="drop-line",
drop_line_ignore=true,
) ==
"" {
attributes.clear()
return None
} else {
target = expanded
}
}
if blk_ctx == Image {
document.register_image(target)
if !attributes.truthy("imagesdir") {
attributes.set(
"imagesdir",
doc_attrs.get("imagesdir").unwrap_or(Nil),
)
}
if !attributes.truthy("alt") {
match style {
Some(s) => attributes.set_str("alt", s)
None => {
let default_alt = basename(target, drop_ext=true)
.replace_all(old="_", new=" ")
.replace_all(old="-", new=" ")
attributes.set_str("default-alt", default_alt)
attributes.set_str("alt", default_alt)
}
}
}
match attributes.remove_str("scaledwidth") {
Some(sw) if sw != "" =>
attributes.set_str(
"scaledwidth",
if trailing_digits_rx.matches(sw) {
"\{sw}%"
} else {
sw
},
)
_ => ()
}
match attributes.remove_str("title") {
Some(t) => {
b.set_title(Some(t))
b.assign_caption(
attributes.remove_str("caption"),
caption_context="figure",
)
}
None => ()
}
}
attributes.set_str("target", target)
block = Some(b)
done = true
} else if ch0 == "t" &&
this_line.has_prefix("toc:") &&
block_toc_macro_rx.find(this_line) is Some(m) {
let b = Node::new_block(parent, Toc, content_model=Empty)
match m.group(1) {
Some(a) =>
b.parse_attributes(a, [], sub_input=true, into=attributes)
|> ignore
None => ()
}
block = Some(b)
done = true
} else if block_macro_extensions {
match custom_block_macro_rx.find(this_line) {
Some(m) =>
match
extensions.unwrap().registered_for_block_macro(m.at(1)) {
Some(extension) => {
let content = m.group(3)
let mut target = m.at(2)
if target.contains("{") {
let expanded = parent.sub_attributes(target)
if expanded == "" &&
doc_attrs
.str("attribute-missing")
.unwrap_or(compliance.attribute_missing) ==
"drop-line" &&
parent.sub_attributes(
target + " ",
attribute_missing="drop-line",
drop_line_ignore=true,
) ==
"" {
attributes.clear()
return None
} else {
target = expanded
}
}
if extension.config.content_model == Some(Attributes) {
match content {
Some(c) =>
document.parse_attributes(
c,
extension.config.positional_attrs.map(x => {
Some(x)
}),
sub_input=true,
into=attributes,
)
|> ignore
None => ()
}
} else {
attributes.set_str("text", content.unwrap_or(""))
}
for d in extension.config.default_attrs {
if !attributes.contains(d.0) {
attributes.set_str(d.0, d.1)
}
}
match
((extension.process)(parent, target, attributes) catch {
e => {
parent.abort_processing(e)
None
}
}) {
Some(b) if !physical_equal(b, parent) => {
attributes.replace(b.attributes)
block = Some(b)
done = true
}
_ => {
attributes.clear()
return None
}
}
}
None =>
if logger().is_debug() {
log_debug(
"unknown name for block macro: \{m.at(1)}",
source_location=reader.cursor_at_mark(),
)
}
}
None => ()
}
} else if logger().is_debug() {
match custom_block_macro_rx.find(this_line) {
Some(m) =>
log_debug(
"unknown name for block macro: \{m.at(1)}",
source_location=reader.cursor_at_mark(),
)
None => ()
}
}
}
}
}
}
if !done {
if ch0 == "" {
ch0 = first_char(this_line)
}
if !indented && ch0 == "<" && callout_list_rx.find(this_line) is Some(m) {
reader.unshift_line(this_line)
block = Some(
parse_callout_list(reader, Some(m), parent, document.callouts()),
)
attributes.set_str("style", "arabic")
done = true
} else if unordered_list_rx.matches(this_line) {
reader.unshift_line(this_line)
if style is None &&
parent.context == Section &&
parent.sectname == Some("bibliography") {
attributes.set_str("style", "bibliography")
style = Some("bibliography")
}
block = Some(parse_list(reader, Ulist, parent, style))
done = true
} else if ordered_list_rx.matches(this_line) {
reader.unshift_line(this_line)
let b = parse_list(
reader,
Olist,
parent,
style,
start=attributes.remove_str("start"),
)
match b.style {
Some(s) => attributes.set_str("style", s)
None => ()
}
block = Some(b)
done = true
} else if (this_line.contains("::") || this_line.contains(";;")) &&
description_list_rx.find(this_line) is Some(m) {
reader.unshift_line(this_line)
block = Some(parse_description_list(reader, m, parent))
done = true
} else if (style == Some("float") || style == Some("discrete")) &&
(if compliance.underline_style_section_titles {
is_section_title(this_line, reader.peek_line()) is Some(_)
} else {
!indented && atx_section_title(this_line) is Some(_)
}) {
reader.unshift_line(this_line)
let (float_id, float_reftext, block_title, float_level, _) = parse_section_title(
reader,
document,
attributes.str("id"),
)
match float_reftext {
Some(r) => attributes.set_str("reftext", r)
None => ()
}
let b = Node::new_block(parent, FloatingTitle, content_model=Empty)
b.set_title(Some(block_title))
attributes.remove("title") |> ignore
b.id = match float_id {
Some(i) => Some(i)
None =>
if doc_attrs.contains("sectids") {
Some(generate_section_id(b.title().unwrap_or(""), document))
} else {
None
}
}
b.level = float_level
block = Some(b)
done = true
} else if style is Some(s) && s != "normal" {
if paragraph_styles.contains(s) {
block_context = Context::from_name(s)
cloaked_context = Paragraph
reader.unshift_line(this_line)
done = true
} else if admonition_styles.contains(s) {
block_context = Admonition
cloaked_context = Paragraph
reader.unshift_line(this_line)
done = true
} else if block_extensions &&
extensions.unwrap().registered_for_block(s, Paragraph) is Some(_) {
block_context = Context::from_name(s)
cloaked_context = Paragraph
reader.unshift_line(this_line)
done = true
} else {
if logger().is_debug() {
log_debug(
"unknown style for paragraph: \{s}",
source_location=reader.cursor_at_mark(),
)
}
style = None
}
}
}
if !done {
reader.unshift_line(this_line)
if indented && style is None {
let content_adjacent = if skipped == 0 { list_type } else { None }
let lines = read_paragraph_lines(
reader,
content_adjacent is Some(_),
skip_line_comments=text_only,
)
adjust_indentation(lines)
if text_only || content_adjacent == Some(Dlist) {
block = Some(
Node::new_block(
parent,
Paragraph,
content_model=Simple,
source=lines,
attributes~,
),
)
} else {
block = Some(
Node::new_block(
parent,
Literal,
content_model=Verbatim,
source=lines,
attributes~,
),
)
}
} else {
let lines = read_paragraph_lines(
reader,
skipped == 0 && list_type is Some(_),
skip_line_comments=true,
)
if text_only {
if indented && style == Some("normal") {
adjust_indentation(lines)
}
block = Some(
Node::new_block(
parent,
Paragraph,
content_model=Simple,
source=lines,
attributes~,
),
)
} else if admonition_styles.iter().any(s => first_char(s) == ch0) &&
this_line.contains(":") &&
admonition_paragraph_rx.find(this_line) is Some(m) {
lines[0] = m.post_match()
let admonition_name = @rb.downcase(m.at(1))
attributes.set_str("style", m.at(1))
attributes.set_str("name", admonition_name)
attributes.set(
"textlabel",
match attributes.remove("caption") {
Some(c) if c.truthy() => c
_ => doc_attrs.get("\{admonition_name}-caption").unwrap_or(Nil)
},
)
block = Some(
Node::new_block(
parent,
Admonition,
content_model=Simple,
source=lines,
attributes~,
),
)
} else if compliance.markdown_syntax &&
ch0 == ">" &&
this_line.has_prefix("> ") {
for i, line in lines {
lines[i] = if line == ">" {
@rb.from(line, 1)
} else if line.has_prefix("> ") {
@rb.from(line, 2)
} else {
line
}
}
let mut credit_line : String? = None
if lines[lines.length() - 1].has_prefix("-- ") {
credit_line = Some(@rb.from(lines.pop().unwrap(), 3))
if !lines.is_empty() {
while !lines.is_empty() && lines[lines.length() - 1] == "" {
lines.pop() |> ignore
}
}
}
attributes.set_str("style", "quote")
let b = build_block(
Quote,
Compound,
None,
parent,
Reader::new(lines),
attributes,
reader_only=true,
)
match (credit_line, b) {
(Some(cl), Some(b)) => {
let parts = @rb.split(
b.apply_subs(cl, normal_subs),
", ",
limit=2,
)
match parts.get(0) {
Some(a) => attributes.set_str("attribution", a)
None => ()
}
match parts.get(1) {
Some(c) => attributes.set_str("citetitle", c)
None => ()
}
}
_ => ()
}
block = b
} else if ch0 == "\"" &&
lines.length() > 1 &&
lines[lines.length() - 1].has_prefix("-- ") &&
lines[lines.length() - 2].has_suffix("\"") {
lines[0] = @rb.from(this_line, 1)
let credit_line = @rb.from(lines.pop().unwrap(), 3)
while !lines.is_empty() && lines[lines.length() - 1] == "" {
lines.pop() |> ignore
}
let last = lines.pop().unwrap()
lines.push(@rb.chop(last))
attributes.set_str("style", "quote")
let b = Node::new_block(
parent,
Quote,
content_model=Simple,
source=lines,
attributes~,
)
let parts = @rb.split(
b.apply_subs(credit_line, normal_subs),
", ",
limit=2,
)
match parts.get(0) {
Some(a) => attributes.set_str("attribution", a)
None => ()
}
match parts.get(1) {
Some(c) => attributes.set_str("citetitle", c)
None => ()
}
block = Some(b)
} else {
if indented && style == Some("normal") {
adjust_indentation(lines)
}
block = Some(
Node::new_block(
parent,
Paragraph,
content_model=Simple,
source=lines,
attributes~,
),
)
}
match block {
Some(b) =>
catalog_inline_anchors(lines.join("\n"), b, document, reader)
None => ()
}
}
}
}
if block is None {
match block_context {
Listing | Source => {
let mut language : String? = None
let is_source = block_context == Source ||
({
language = if attributes.pos_str(1) is Some(_) {
None
} else {
match attributes.pos_str(2) {
Some(l) => Some(l)
None => doc_attrs.str("source-language")
}
}
language is Some(_)
})
if is_source {
match language {
Some(lang) => {
attributes.set_str("style", "source")
attributes.set_str("language", lang)
rekey_attributes(attributes, [None, None, Some("linenums")])
|> ignore
}
None => {
rekey_attributes(attributes, [
None,
Some("language"),
Some("linenums"),
])
|> ignore
if !attributes.contains("language") &&
doc_attrs.contains("source-language") {
attributes.set(
"language",
doc_attrs.get("source-language").unwrap(),
)
}
if cloaked_context != Listing {
attributes.set_str("cloaked-context", cloaked_context.name())
}
}
}
if !attributes.contains("linenums-option") &&
(
attributes.contains("linenums") ||
doc_attrs.contains("source-linenums-option")
) {
attributes.set_str("linenums-option", "")
}
if !attributes.contains("indent") &&
doc_attrs.contains("source-indent") {
attributes.set("indent", doc_attrs.get("source-indent").unwrap())
}
}
block = build_block(
Listing,
Verbatim,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
}
FencedCode => {
attributes.set_str("style", "source")
let ll = this_line.length()
let mut language = ""
if ll > 3 {
language = @rb.from(this_line, 3)
match language.find(",") {
Some(comma_idx) =>
if comma_idx > 0 {
language = @rb.strip(@rb.slice(language, 0, comma_idx))
if comma_idx < ll - 4 {
attributes.set_str("linenums", "")
}
} else {
if ll > 4 {
attributes.set_str("linenums", "")
}
language = @rb.slice(language, 0, comma_idx)
}
None => language = @rb.lstrip(language)
}
}
if language == "" {
if doc_attrs.contains("source-language") {
attributes.set(
"language",
doc_attrs.get("source-language").unwrap(),
)
}
} else {
attributes.set_str("language", language)
}
attributes.set_str("cloaked-context", cloaked_context.name())
if !attributes.contains("linenums-option") &&
(
attributes.contains("linenums") ||
doc_attrs.contains("source-linenums-option")
) {
attributes.set_str("linenums-option", "")
}
if !attributes.contains("indent") && doc_attrs.contains("source-indent") {
attributes.set("indent", doc_attrs.get("source-indent").unwrap())
}
block = build_block(
Listing,
Verbatim,
Some(@rb.slice(terminator, 0, 3)),
parent,
reader,
attributes,
)
}
Table => {
let block_cursor = reader.cursor()
let block_reader = Reader::new(
reader.read_lines_until(
terminator~,
skip_line_comments=true,
context=Some("table"),
cursor_at_mark=true,
),
cursor=block_cursor,
)
if !(terminator.has_prefix("|") || terminator.has_prefix("!")) {
if !attributes.truthy("format") {
attributes.set_str(
"format",
if terminator.has_prefix(",") {
"csv"
} else {
"dsv"
},
)
}
}
block = Some(parse_table(block_reader, parent, attributes))
}
Sidebar =>
block = build_block(
block_context,
Compound,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
Admonition => {
let admonition_name = @rb.downcase(style.unwrap_or(""))
attributes.set_str("name", admonition_name)
attributes.set(
"textlabel",
match attributes.remove("caption") {
Some(c) if c.truthy() => c
_ => doc_attrs.get("\{admonition_name}-caption").unwrap_or(Nil)
},
)
block = build_block(
block_context,
Compound,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
}
Open | Abstract | PartIntro =>
block = build_block(
Open,
Compound,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
Literal =>
block = build_block(
block_context,
Verbatim,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
Example => {
if attributes.truthy("collapsible-option") {
attributes.set_str("caption", "")
}
block = build_block(
block_context,
Compound,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
}
Quote | Verse => {
rekey_attributes(attributes, [
None,
Some("attribution"),
Some("citetitle"),
])
|> ignore
block = build_block(
block_context,
if block_context == Verse {
Verbatim
} else {
Compound
},
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
}
Stem | LatexMath | AsciiMath => {
if block_context == Stem {
attributes.set_str(
"style",
stem_type_alias(
match attributes.pos_str(2) {
Some(s) => Some(s)
None => doc_attrs.str("stem")
},
),
)
}
block = build_block(
Stem,
Raw,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
}
Pass =>
block = build_block(
block_context,
Raw,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
Comment => {
build_block(
block_context,
Skip,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
)
|> ignore
attributes.clear()
return None
}
_ => {
let extension = if block_extensions {
extensions
.unwrap()
.registered_for_block(block_context.name(), cloaked_context)
} else {
None
}
guard extension is Some(extension) else {
abort(
"Unsupported block type \{block_context.name()} at \{reader.cursor().line_info()}",
)
}
let content_model = extension.config.content_model.unwrap_or(Compound)
if content_model != Skip {
let pos = extension.config.positional_attrs
if !pos.is_empty() {
rekey_attributes(attributes, [None] + pos.map(x => Some(x)))
|> ignore
}
for d in extension.config.default_attrs {
if !attributes.truthy(d.0) {
attributes.set_str(d.0, d.1)
}
}
attributes.set_str("cloaked-context", cloaked_context.name())
}
match
build_block(
block_context,
content_model,
opt_terminator(delimited_block, terminator),
parent,
reader,
attributes,
extension~,
) {
Some(b) => block = Some(b)
None => {
attributes.clear()
return None
}
}
}
}
}
guard block is Some(block) else { return None }
if document.sourcemap() {
block.source_location = Some(reader.cursor_at_mark())
}
let mut block_title : String? = None
match attributes.remove_str("title") {
Some(t) => {
block.set_title(Some(t))
block_title = Some(t)
if caption_attribute_name(block.context.name()) is Some(_) {
block.assign_caption(attributes.remove_str("caption"))
}
}
None => ()
}
block.style = attributes.str("style")
let block_id = match block.id {
Some(i) => Some(i)
None => {
block.id = attributes.str("id")
block.id
}
}
match block_id {
Some(i) => {
let should_sub_title = match block_title {
Some(t) => t.contains("{")
None => block.has_title()
}
if should_sub_title {
block.title() |> ignore
}
if !document.register_ref(i, block) {
log_warn(
"id assigned to block already in use: \{i}",
source_location=reader.cursor_at_mark(),
)
}
}
None => ()
}
if !attributes.is_empty() {
block.update_attributes(attributes)
}
block.commit_subs()
if block.has_sub(Callouts) {
if !catalog_callouts(block.source(), document) {
block.remove_sub(Callouts)
}
}
Some(block)
}
///|
fn opt_terminator(delimited : BlockMatchData?, terminator : String) -> String? {
match delimited {
Some(_) => Some(terminator)
None => None
}
}
///|
/// Builds a block from lines read up to the terminator (Ruby `build_block`).
fn build_block(
block_context : Context,
content_model : ContentModel,
terminator : String?,
parent : Node,
reader : Reader,
attributes : Attributes,
reader_only? : Bool = false,
extension? : BlockProcessor,
) -> Node? {
let (skip_processing, parse_as_content_model) = match content_model {
Skip => (true, Simple)
Raw => (false, Simple)
_ => (false, content_model)
}
let mut content_model = content_model
let mut lines : Array[String]? = None
let mut block_reader : Reader? = None
if reader_only {
block_reader = Some(reader)
} else {
match terminator {
None =>
if parse_as_content_model == Verbatim {
lines = Some(
reader.read_lines_until(
break_on_blank_lines=true,
break_on_list_continuation=true,
),
)
} else {
if content_model == Compound {
content_model = Simple
}
lines = Some(
read_paragraph_lines(
reader,
false,
skip_line_comments=true,
skip_processing~,
),
)
}
Some(t) =>
if parse_as_content_model != Compound {
lines = Some(
reader.read_lines_until(
terminator=t,
skip_processing~,
context=Some(block_context.name()),
cursor_at_mark=true,
),
)
} else {
let block_cursor = reader.cursor()
block_reader = Some(
Reader::new(
reader.read_lines_until(
terminator=t,
skip_processing~,
context=Some(block_context.name()),
cursor_at_mark=true,
),
cursor=block_cursor,
),
)
}
}
}
match content_model {
Verbatim => {
let tab_size = match attributes.get("tabsize") {
Some(v) if v.truthy() => v.to_i()
_ =>
match parent.document().attributes.get("tabsize") {
Some(v) => v.to_i()
None => 0
}
}
match attributes.get("indent") {
Some(indent) if indent.truthy() =>
adjust_indentation(
lines.unwrap_or([]),
indent_size=indent.to_i(),
tab_size~,
)
_ =>
if tab_size > 0 {
adjust_indentation(lines.unwrap_or([]), indent_size=-1, tab_size~)
}
}
}
Skip => return None
_ => ()
}
let block = match extension {
Some(ext) => {
attributes.remove("style") |> ignore
let r = match block_reader {
Some(r) => r
None => Reader::new(lines.unwrap_or([]))
}
match
((ext.process)(parent, r, attributes.copy()) catch {
e => {
parent.abort_processing(e)
None
}
}) {
Some(b) if !physical_equal(b, parent) => {
attributes.replace(b.attributes)
if b.content_model == Compound && !b.lines.is_empty() {
content_model = Compound
block_reader = Some(Reader::new(b.lines))
}
b
}
_ => return None
}
}
None =>
Node::new_block(
parent,
block_context,
content_model~,
source?=lines,
attributes~,
)
}
if content_model == Compound {
match block_reader {
Some(r) => parse_blocks(r, block, None)
None => ()
}
}
Some(block)
}
///|
/// Parses blocks into `parent` until the reader is exhausted.
fn parse_blocks(
reader : Reader,
parent : Node,
attributes : Attributes?,
) -> Unit {
while true {
let attrs = match attributes {
Some(a) => a.copy()
None => Attributes::new()
}
match next_block(reader, parent, attrs) {
Some(b) => parent.blocks.push(b)
None => if !reader.has_more_lines() { break }
}
}
}
///|
/// Registers the callouts found in `text` (Ruby `catalog_callouts`).
fn catalog_callouts(text : String, document : Node) -> Bool {
let mut found = false
let mut autonum = 0
if text.contains("<") {
for m in callout_scan_rx.find_all(text) {
if !m.matched().has_prefix("\\") {
let num = if m.at(2) == "." {
autonum += 1
autonum.to_string()
} else {
m.at(2)
}
document.callouts().register(num) |> ignore
}
found = true
}
}
found
}
///|
/// Registers an inline anchor (Ruby `Parser.catalog_inline_anchor`).
fn catalog_inline_anchor(
id : String,
reftext : String?,
node : Node,
location : Cursor?,
doc : Node,
) -> Unit {
let reftext = match reftext {
Some(r) if r.contains("{") => Some(doc.sub_attributes(r))
r => r
}
if !doc.register_ref(
id,
Node::new_inline(node, Anchor, text?=reftext, type_="ref", id~),
) {
log_warn(
"id assigned to anchor already in use: \{id}",
source_location?=location,
)
}
}
///|
fn catalog_inline_anchors(
text : String,
block : Node,
document : Node,
reader : Reader,
) -> Unit {
if !(text.contains("[[") || text.contains("or:")) {
return
}
for m in inline_anchor_scan_rx.find_all(text) {
let mut id = ""
let mut reftext : String? = None
match m.group(1) {
Some(i) => {
id = i
reftext = m.group(2)
match reftext {
Some(r) if r.contains("{") => {
let r2 = document.sub_attributes(r)
if r2 == "" {
continue
}
reftext = Some(r2)
}
_ => ()
}
}
None => {
id = m.at(3)
reftext = m.group(4)
match reftext {
Some(r) =>
if r.contains("]") {
let r2 = r.replace_all(old="\\]", new="]")
reftext = Some(
if r2.contains("{") {
document.sub_attributes(r2)
} else {
r2
},
)
} else if r.contains("{") {
let r2 = document.sub_attributes(r)
reftext = if r2 == "" { None } else { Some(r2) }
}
None => ()
}
}
}
if !document.register_ref(
id,
Node::new_inline(block, Anchor, text?=reftext, type_="ref", id~),
) {
let location = reader.cursor_at_mark()
let pre = m.pre_match()
let offset = @rb.count(pre, "\n") +
(if m.matched().has_prefix("\n") { 1 } else { 0 })
if offset > 0 {
location.advance(offset)
}
log_warn(
"id assigned to anchor already in use: \{id}",
source_location=location,
)
}
}
}
///|
fn catalog_inline_biblio_anchor(
id : String,
reftext : String?,
node : Node,
reader : Reader,
) -> Unit {
let text = match reftext {
Some(r) => Some("[\{r}]")
None => None
}
if !node
.document()
.register_ref(
id,
Node::new_inline(node, Anchor, text?, type_="bibref", id~),
) {
log_warn(
"id assigned to bibliography anchor already in use: \{id}",
source_location=reader.cursor(),
)
}
}