///|
/// A passthrough extracted before substitutions and restored afterwards.
priv struct Passthrough {
text : String
subs : Array[Sub]?
type_ : String?
attributes : Attributes?
}
///|
pub let basic_subs : Array[Sub] = [SpecialCharacters]
///|
pub let header_subs : Array[Sub] = [SpecialCharacters, Attributes]
///|
pub let no_subs : Array[Sub] = []
///|
pub let normal_subs : Array[Sub] = [
SpecialCharacters,
Quotes,
Attributes,
Replacements,
Macros,
PostReplacements,
]
///|
pub let reftext_subs : Array[Sub] = [SpecialCharacters, Quotes, Replacements]
///|
pub let verbatim_subs : Array[Sub] = [SpecialCharacters, Callouts]
///|
let pass_start : String = "\u{96}"
///|
let pass_end : String = "\u{97}"
///|
let can_char : String = "\u{18}"
///|
let del_char : String = "\u{7f}"
///|
/// Escapes `<`, `>` and `&` (Ruby `sub_specialchars`).
pub fn sub_specialchars(text : String) -> String {
if !(text.contains(">") || text.contains("&") || text.contains("<")) {
return text
}
let sb = StringBuilder(size_hint=text.length() + 16)
for c in text {
match c {
'<' => sb.write_string("<")
'>' => sb.write_string(">")
'&' => sb.write_string("&")
_ => sb.write_char(c)
}
}
sb.to_string()
}
///|
/// Ruby `sprintf(format, arg)` for a format with one `%s`.
pub fn sub_placeholder(format : String, arg : String) -> String {
match format.find("%s") {
Some(i) => @rb.slice(format, 0, i) + arg + @rb.from(format, i + 2)
None => format
}
}
///|
/// Applies substitutions to text (Ruby `apply_subs`).
pub fn Node::apply_subs(
self : Node,
text : String,
subs : Array[Sub],
) -> String {
if text == "" || subs.is_empty() {
return text
}
let mut text = text
let mut passthrus = false
let mut clear_passthrus = false
if subs.contains(Macros) {
text = self.extract_passthroughs(text)
if !self.passthroughs.is_empty() {
passthrus = true
if !self.passthroughs_locked {
self.passthroughs_locked = true
clear_passthrus = true
}
}
}
for type_ in subs {
match type_ {
SpecialCharacters => text = sub_specialchars(text)
Quotes => text = self.sub_quotes(text)
Attributes => if text.contains("{") { text = self.sub_attributes(text) }
Replacements => text = self.sub_replacements(text)
Macros => text = self.sub_macros(text)
Highlight => text = self.highlight_source(text, subs.contains(Callouts))
Callouts =>
if !subs.contains(Highlight) {
text = self.sub_callouts(text)
}
PostReplacements => text = self.sub_post_replacements(text)
}
}
if passthrus {
text = self.restore_passthroughs(text)
if clear_passthrus {
self.passthroughs.clear()
self.passthroughs_locked = false
}
}
text
}
///|
/// Applies substitutions to lines (Ruby `apply_subs` with an Array).
pub fn Node::apply_subs_lines(
self : Node,
lines : Array[String],
subs : Array[Sub],
) -> Array[String] {
if lines.is_empty() || subs.is_empty() {
return lines.copy()
}
let text = if lines.length() > 1 { lines.join("\n") } else { lines[0] }
if text == "" {
return [text]
}
@rb.split(self.apply_subs(text, subs), "\n", limit=-1)
}
///|
pub fn Node::apply_normal_subs(self : Node, text : String) -> String {
self.apply_subs(text, normal_subs)
}
///|
pub fn Node::apply_header_subs(self : Node, text : String) -> String {
self.apply_subs(text, header_subs)
}
///|
pub fn Node::apply_title_subs(self : Node, text : String) -> String {
self.apply_subs(text, normal_subs)
}
///|
pub fn Node::apply_reftext_subs(self : Node, text : String) -> String {
self.apply_subs(text, reftext_subs)
}
///|
/// Applies inline quote substitutions.
pub fn Node::sub_quotes(self : Node, text : String) -> String {
let compat = self.document().compat_mode()
let sniff = quoted_text_sniff_rx
.get(if compat { "true" } else { "false" })
.unwrap()
if !sniff.matches(text) {
return text
}
let mut text = text
for q in (if compat { quote_subs_compat } else { quote_subs_normal }) {
let (type_, scope, pattern) = q
text = pattern.replace(text, m => self.convert_quoted_text(m, type_, scope))
}
text
}
///|
/// Replaces attribute references (Ruby `sub_attributes`).
pub fn Node::sub_attributes(
self : Node,
text : String,
attribute_missing? : String,
drop_line_ignore? : Bool = false,
) -> String {
let doc = self.document()
let doc_attrs = doc.attributes
let mut drop = false
let mut drop_line = false
let mut drop_empty_line = false
let mut attribute_undefined : String? = None
let mut attribute_missing_val : String? = attribute_missing
let text = attribute_reference_rx.replace(text, m => {
if m.group(1) == Some("\\") || m.group(4) == Some("\\") {
return "{\{m.at(2)}}"
}
if m.has(3) {
let args = @rb.split(m.at(2), ":", limit=3)
let directive = args.remove(0)
match directive {
"set" => {
let (_, value) = store_attribute(
args.get(0).unwrap_or(""),
Some(args.get(1).unwrap_or("")),
doc=Some(doc),
attrs=None,
)
let undefined = match attribute_undefined {
Some(u) => u
None => {
let u = doc_attrs
.str("attribute-undefined")
.unwrap_or(compliance.attribute_undefined)
attribute_undefined = Some(u)
u
}
}
if value is Some(_) || undefined != "drop-line" {
drop = true
drop_empty_line = true
return del_char
} else {
drop = true
drop_line = true
return can_char
}
}
"counter2" => {
doc.counter(args.get(0).unwrap_or(""), seed?=args.get(1)) |> ignore
drop = true
drop_empty_line = true
return del_char
}
_ => return doc.counter(args.get(0).unwrap_or(""), seed?=args.get(1))
}
}
let key = @rb.downcase(m.at(2))
if doc_attrs.contains(key) {
return doc_attrs.get(key).unwrap().to_s()
}
match intrinsic_attribute(key) {
Some(v) => return v
None => ()
}
let missing = match attribute_missing_val {
Some(v) => v
None => {
let v = doc_attrs
.str("attribute-missing")
.unwrap_or(compliance.attribute_missing)
attribute_missing_val = Some(v)
v
}
}
match missing {
"drop" => {
drop = true
drop_empty_line = true
del_char
}
"drop-line" => {
if !drop_line_ignore {
log_info(
"dropping line containing reference to missing attribute: \{key}",
)
}
drop = true
drop_line = true
can_char
}
"warn" => {
log_warn("skipping reference to missing attribute: \{key}")
m.matched()
}
_ => m.matched()
}
})
if !drop {
return text
}
if drop_empty_line {
let lines = @rb.split(@rb.squeeze(text, chars=del_char), "\n", limit=-1)
let kept = if drop_line {
lines.filter(line => !(line == del_char || line.contains(can_char)))
} else {
lines.filter(line => line != del_char)
}
@rb.delete_chars(kept.join("\n"), del_char)
} else if text.contains("\n") {
@rb.split(text, "\n", limit=-1)
.filter(line => !line.contains(can_char))
.join("\n")
} else {
""
}
}
///|
fn intrinsic_attribute(key : String) -> String? {
match key {
"startsb" => Some("[")
"endsb" => Some("]")
"vbar" => Some("|")
"caret" => Some("^")
"asterisk" => Some("*")
"tilde" => Some("~")
"plus" => Some("+")
"backslash" => Some("\\")
"backtick" => Some("`")
"blank" => Some("")
"empty" => Some("")
"sp" => Some(" ")
"two-colons" => Some("::")
"two-semicolons" => Some(";;")
"nbsp" => Some(" ")
"deg" => Some("°")
"zwsp" => Some("")
"quot" => Some(""")
"apos" => Some("'")
"lsquo" => Some("‘")
"rsquo" => Some("’")
"ldquo" => Some("“")
"rdquo" => Some("”")
"wj" => Some("")
"brvbar" => Some("¦")
"pp" => Some("++")
"cpp" => Some("C++")
"cxx" => Some("C++")
"amp" => Some("&")
"lt" => Some("<")
"gt" => Some(">")
_ => None
}
}
///|
/// Applies textual replacements (Ruby `sub_replacements`).
pub fn Node::sub_replacements(_self : Node, text : String) -> String {
if !replaceable_text_rx.matches(text) {
return text
}
let mut text = text
for r in replacements {
let (pattern, replacement, restore) = r
text = pattern.replace(text, m => do_replacement(m, replacement, restore))
}
text
}
///|
fn do_replacement(
m : @regex.MatchData,
replacement : String,
restore : String,
) -> String {
let captured = m.matched()
if captured.contains("\\") {
captured.replace(old="\\", new="")
} else {
match restore {
"none" => replacement
"bounding" => m.at(1) + replacement + m.at(2)
_ => m.at(1) + replacement
}
}
}
///|
fn Node::convert_quoted_text(
self : Node,
m : @regex.MatchData,
type_ : String,
scope : String,
) -> String {
let mut unescaped_attrs : String? = None
if m.matched().has_prefix("\\") {
if scope == "constrained" && m.has(2) {
unescaped_attrs = Some("[\{m.at(2)}]")
} else {
return @rb.from(m.matched(), 1)
}
}
if scope == "constrained" {
match unescaped_attrs {
Some(ua) =>
"\{ua}\{Node::new_inline(self, Quoted, text=m.at(3), type_~).convert()}"
None => {
let mut type_ = type_
let mut id : String? = None
let mut attributes : Attributes? = None
match m.group(2) {
Some(attrlist) => {
let a = self.parse_quoted_text_attributes(attrlist)
id = a.str("id")
attributes = Some(a)
if type_ == "mark" {
type_ = "unquoted"
}
}
None => ()
}
"\{m.at(1)}\{Node::new_inline(self, Quoted, text=m.at(3), type_~, id?=id, attributes?=attributes).convert()}"
}
}
} else {
let mut type_ = type_
let mut id : String? = None
let mut attributes : Attributes? = None
match m.group(1) {
Some(attrlist) => {
let a = self.parse_quoted_text_attributes(attrlist)
id = a.str("id")
attributes = Some(a)
if type_ == "mark" {
type_ = "unquoted"
}
}
None => ()
}
Node::new_inline(self, Quoted, text=m.at(2), type_~, id?, attributes?).convert()
}
}
///|
fn Node::parse_quoted_text_attributes(self : Node, str : String) -> Attributes {
let mut str = str
if str.contains("{") {
str = self.sub_attributes(str)
}
match str.find(",") {
Some(i) => str = @rb.slice(str, 0, i)
None => ()
}
str = @rb.strip(str)
let attrs = Attributes::new()
if str == "" {
attrs
} else if (str.has_prefix(".") || str.has_prefix("#")) &&
compliance.shorthand_property_syntax {
let (before, _, after) = @rb.partition(str, "#")
if after == "" {
if before.length() > 1 {
attrs.set_str("role", @rb.lstrip(before.replace_all(old=".", new=" ")))
}
} else {
let (id, _, roles) = @rb.partition(after, ".")
if id != "" {
attrs.set_str("id", id)
}
if roles == "" {
if before.length() > 1 {
attrs.set_str(
"role",
@rb.lstrip(before.replace_all(old=".", new=" ")),
)
}
} else if before.length() > 1 {
attrs.set_str(
"role",
@rb.lstrip((before + "." + roles).replace_all(old=".", new=" ")),
)
} else {
attrs.set_str("role", roles.replace_all(old=".", new=" "))
}
}
attrs
} else {
attrs.set_str("role", str)
attrs
}
}
///|
/// Ruby `normalize_text`.
fn normalize_text(
text : String,
normalize_whitespace? : Bool = false,
unescape_closing_square_brackets? : Bool = false,
) -> String {
if text == "" {
return text
}
let mut text = text
if normalize_whitespace {
text = @rb.strip(text).replace_all(old="\n", new=" ")
}
if unescape_closing_square_brackets && text.contains("]") {
text = text.replace_all(old="\\]", new="]")
}
text
}
///|
fn split_simple_csv(str : String) -> Array[String] {
if str == "" {
[]
} else if str.contains("\"") {
let values = []
let accum = StringBuilder()
let mut quote_open = false
for c in str {
match c {
',' =>
if quote_open {
accum.write_char(c)
} else {
values.push(@rb.strip(accum.to_string()))
accum.reset()
}
'"' => quote_open = !quote_open
_ => accum.write_char(c)
}
}
values.push(@rb.strip(accum.to_string()))
values
} else {
@rb.split(str, ",").map(@rb.strip)
}
}
///|
fn Node::extract_attributes_from_text(
self : Node,
text : String,
default_text? : String?,
) -> (String?, Attributes) {
let attrlist = if text.contains("\n") {
text.replace_all(old="\n", new=" ")
} else {
text
}
let attrs = parse_attribute_list(attrlist, block=self)
match attrs.pos_str(1) {
Some(resolved) =>
if resolved == attrlist {
attrs.clear()
(Some(text), attrs)
} else {
(Some(resolved), attrs)
}
None => (default_text.unwrap_or(None), attrs)
}
}
///|
/// Parses an attribute list (Ruby `parse_attributes`).
pub fn Node::parse_attributes(
self : Node,
attrlist : String,
posattrs : Array[String?],
unescape_input? : Bool = false,
sub_input? : Bool = false,
sub_result? : Bool = false,
into? : Attributes,
) -> Attributes {
if attrlist == "" {
return match into {
Some(a) => a
None => Attributes::new()
}
}
let mut attrlist = attrlist
if unescape_input {
attrlist = normalize_text(
attrlist,
normalize_whitespace=true,
unescape_closing_square_brackets=true,
)
}
if sub_input && attrlist.contains("{") {
attrlist = self.document().sub_attributes(attrlist)
}
let block = if sub_result { Some(self) } else { None }
let parsed = parse_attribute_list(attrlist, positional_attrs=posattrs, block?)
match into {
Some(a) => {
a.update(parsed)
a
}
None => parsed
}
}
///|
/// Applies post replacements (hard line breaks).
pub fn Node::sub_post_replacements(self : Node, text : String) -> String {
if self.attributes.truthy("hardbreaks-option") ||
self.document().attributes.truthy("hardbreaks-option") {
let lines = @rb.split(text, "\n", limit=-1)
if lines.length() < 2 {
return text
}
let last = lines.pop().unwrap()
let out = lines.map(line => {
Node::new_inline(
self,
Break,
text=if line.has_suffix(" +") {
@rb.slice(line, 0, line.length() - 2)
} else {
line
},
type_="line",
).convert()
})
out.push(last)
out.join("\n")
} else if text.contains("+") && text.contains(" +") {
hard_line_break_rx.replace(text, m => {
Node::new_inline(self, Break, text=m.at(1), type_="line").convert()
})
} else {
text
}
}
///|
/// Escapes special characters and optionally processes callouts.
pub fn Node::sub_source(
self : Node,
source : String,
process_callouts : Bool,
) -> String {
if process_callouts {
self.sub_callouts(sub_specialchars(source))
} else {
sub_specialchars(source)
}
}
///|
let callout_rx_cache : Map[String, @regex.Regex] = Map([])
///|
fn callout_rx_for(line_comment : String, source : Bool) -> @regex.Regex {
let key = (if source { "s:" } else { "e:" }) + line_comment
match callout_rx_cache.get(key) {
Some(r) => r
None => {
let prefix = if line_comment == "" {
""
} else {
"\{regex_escape(line_comment)} ?"
}
let rxt = if source {
"(\\\\)?<()(\\d+|\\.)>(?=(?: ?\\\\?<(?:\\d+|\\.)>)*$)"
} else {
"(\\\\)?<()(\\d+|\\.)>(?=(?: ?\\\\?<(?:\\d+|\\.)>)*$)"
}
let r = @regex.re("(\{prefix})?\{rxt}")
callout_rx_cache[key] = r
r
}
}
}
///|
/// Replaces callout markers with callout nodes.
pub fn Node::sub_callouts(self : Node, text : String) -> String {
let callout_rx = if self.has_attr("line-comment") {
callout_rx_for(self.attr("line-comment").unwrap_or(""), true)
} else {
callout_source_rx
}
let mut autonum = 0
callout_rx.replace(text, m => {
if m.has(2) {
m.matched().replace(old="\\", new="")
} else {
let num = if m.at(4) == "." {
autonum += 1
autonum.to_string()
} else {
m.at(4)
}
let guard_ : AttrVal = match m.group(1) {
Some(g) => Str(g)
None => if m.at(3) == "--" { List([""]) } else { Nil }
}
let attrs = Attributes::new()
attrs.set("guard", guard_)
Node::new_inline(
self,
Callout,
text=num,
id?=self.document().callouts().read_next_id(),
attributes=attrs,
).convert()
}
})
}
///|
/// Highlights source using the document's syntax highlighter (server-side only).
fn Node::highlight_source(
self : Node,
source : String,
process_callouts : Bool,
) -> String {
// NOTE the call to handles_highlighting is a defensive check since,
// normally, we wouldn't arrive here unless it returns true
let doc = self.document()
guard doc.syntax_highlighter() is Some(syntax_hl) &&
syntax_hl.handles_highlighting() else {
return self.sub_source(source, process_callouts)
}
let (source, callout_marks) = if process_callouts {
self.extract_callouts(source)
} else {
(source, None)
}
let doc_attrs = doc.attributes
let syntax_hl_name = syntax_hl.name()
let linenums_mode = if self.has_option("linenums") {
Some(doc_attrs.str("\{syntax_hl_name}-linenums-mode").unwrap_or("table"))
} else {
None
}
let start_line_number = if linenums_mode is Some(_) {
let start = @rb.to_i(self.attr("start", default="1").unwrap_or("1"))
Some(if start < 1 { 1 } else { start })
} else {
None
}
let highlight_lines = if self.has_attr("highlight") {
Some(
resolve_lines_to_highlight(
source,
self.attr("highlight").unwrap_or(""),
start?=start_line_number,
),
)
} else {
None
}
let (highlighted, source_offset) = syntax_hl.highlight(
self,
source,
self.attr("language"),
HighlightOptions::new(
callouts?=callout_marks,
css_mode=doc_attrs.str("\{syntax_hl_name}-css").unwrap_or("class"),
highlight_lines?,
number_lines?=linenums_mode,
start_line_number?,
style?=doc_attrs.str("\{syntax_hl_name}-style"),
),
)
// fix passthrough placeholders that got caught up in syntax highlighting
let highlighted = if self.passthroughs.is_empty() {
highlighted
} else {
highlighted_pass_slot_rx.replace(highlighted, m => {
"\{pass_start}\{m.at(1)}\{pass_end}"
})
}
// NOTE highlight method may have depleted callouts
match callout_marks {
Some(marks) if !marks.is_empty() =>
self.restore_callouts(highlighted, marks, source_offset?)
_ => highlighted
}
}
///|
/// Line numbers to highlight (Ruby `resolve_lines_to_highlight`).
pub fn resolve_lines_to_highlight(
source : String,
spec : String,
start? : Int,
) -> Array[Int] {
let mut lines : Array[Int] = []
let spec = if spec.contains(" ") { @rb.delete_chars(spec, " ") } else { spec }
let entries = if spec.contains(",") {
@rb.split(spec, ",")
} else {
@rb.split(spec, ";")
}
for entry0 in entries {
let mut entry = entry0
let mut negate = false
if entry.has_prefix("!") {
entry = @rb.from(entry, 1)
negate = true
}
let delim = if entry.contains("..") {
Some("..")
} else if entry.contains("-") {
Some("-")
} else {
None
}
match delim {
Some(d) => {
let (from, _, to) = @rb.partition(entry, d)
let to_i = if to == "" || @rb.to_i(to) < 0 {
@rb.count(source, "\n") + 1
} else {
@rb.to_i(to)
}
let range = []
for i in @rb.to_i(from)..<=to_i {
range.push(i)
}
if negate {
lines = lines.filter(l => !range.contains(l))
} else {
for r in range {
if !lines.contains(r) {
lines.push(r)
}
}
}
}
None =>
if negate {
let v = @rb.to_i(entry)
lines = lines.filter(l => l != v)
} else {
let line = @rb.to_i(entry)
if !lines.contains(line) {
lines.push(line)
}
}
}
}
let shift = match start {
Some(s) => s - 1
None => 0
}
if shift != 0 {
lines = lines.map(l => l - shift)
}
lines.sort()
lines
}
///|
fn Node::add_passthrough(self : Node, p : Passthrough) -> String {
let key = self.passthroughs.length()
self.passthroughs.push(p)
"\{pass_start}\{key}\{pass_end}"
}
///|
/// Extracts passthroughs (Ruby `extract_passthroughs`).
fn Node::extract_passthroughs(self : Node, text : String) -> String {
let compat_mode = self.document().compat_mode()
let mut text = text
if text.contains("++") || text.contains("$$") || text.contains("ss:") {
text = inline_pass_macro_rx.replace(text, m => {
let mut preceding : String? = None
match m.group(4) {
Some(boundary) => {
if compat_mode && boundary == "++" {
let head = match m.group(2) {
Some(a) => "\{m.at(1)}[\{a}]\{m.at(3)}"
None => "\{m.at(1)}\{m.at(3)}"
}
return "\{head}++\{self.extract_passthroughs(m.at(5))}++"
}
let mut attributes : Attributes? = None
let mut old_behavior = false
match m.group(2) {
Some(attrlist) => {
let escape_count = m.at(3).length()
if escape_count > 0 {
return "\{m.at(1)}[\{attrlist}]\{@rb.repeat("\\", escape_count - 1)}\{boundary}\{m.at(5)}\{boundary}"
} else if m.at(1) == "\\" {
preceding = Some("[\{attrlist}]")
} else if boundary == "++" {
if attrlist == "x-" {
old_behavior = true
attributes = Some(Attributes::new())
} else if attrlist.has_suffix(" x-") {
old_behavior = true
attributes = Some(
self.parse_quoted_text_attributes(
@rb.slice(attrlist, 0, attrlist.length() - 3),
),
)
} else {
attributes = Some(self.parse_quoted_text_attributes(attrlist))
}
} else {
attributes = Some(self.parse_quoted_text_attributes(attrlist))
}
}
None => {
let escape_count = m.at(3).length()
if escape_count > 0 {
return "\{@rb.repeat("\\", escape_count - 1)}\{boundary}\{m.at(5)}\{boundary}"
}
}
}
let subs = if boundary == "+++" { [] } else { basic_subs }
let key = match attributes {
Some(a) =>
if old_behavior {
self.add_passthrough({
text: m.at(5),
subs: Some(normal_subs),
type_: Some("monospaced"),
attributes: Some(a),
})
} else {
self.add_passthrough({
text: m.at(5),
subs: Some(subs),
type_: Some("unquoted"),
attributes: Some(a),
})
}
None =>
self.add_passthrough({
text: m.at(5),
subs: Some(subs),
type_: None,
attributes: None,
})
}
"\{preceding.unwrap_or("")}\{key}"
}
None => {
if m.group(6) == Some("\\") {
return @rb.from(m.matched(), 1)
}
let t = normalize_text(m.at(8), unescape_closing_square_brackets=true)
match m.group(7) {
Some(subs) =>
self.add_passthrough({
text: t,
subs: Some(self.resolve_pass_subs(subs)),
type_: None,
attributes: None,
})
None =>
self.add_passthrough({
text: t,
subs: None,
type_: None,
attributes: None,
})
}
}
}
})
}
let (pass_char1, pass_char2, pass_rx) = if compat_mode {
inline_pass_rx_true
} else {
inline_pass_rx_false
}
if text.contains(pass_char1) ||
(match pass_char2 {
Some(c) => text.contains(c)
None => false
}) {
text = pass_rx.replace(text, m => {
let mut preceding = m.at(1)
let attrlist = match m.group(4) {
Some(a) => Some(a)
None => m.group(3)
}
let escaped = m.has(5)
let quoted_text = m.at(6)
let format_mark = m.at(7)
let content = m.at(8)
let mut old_behavior = false
let mut old_behavior_forced = false
if compat_mode {
old_behavior = true
} else {
match attrlist {
Some(a) if a == "x-" || a.has_suffix(" x-") => {
old_behavior = true
old_behavior_forced = true
}
_ => ()
}
}
let mut attributes : Attributes? = None
match attrlist {
Some(a) =>
if escaped {
return "\{preceding}[\{a}]\{@rb.from(quoted_text, 1)}"
} else if preceding == "\\" {
if old_behavior_forced && format_mark == "`" {
return "\{preceding}[\{a}]\{quoted_text}"
}
preceding = "[\{a}]"
} else if old_behavior_forced {
attributes = Some(
if a == "x-" {
Attributes::new()
} else {
self.parse_quoted_text_attributes(
@rb.slice(a, 0, a.length() - 3),
)
},
)
} else {
attributes = Some(self.parse_quoted_text_attributes(a))
}
None =>
if escaped {
return "\{preceding}\{@rb.from(quoted_text, 1)}"
} else if compat_mode && preceding == "\\" {
return quoted_text
}
}
let key = if compat_mode {
self.add_passthrough({
text: content,
subs: Some(basic_subs),
type_: Some("monospaced"),
attributes,
})
} else {
match attributes {
Some(_) =>
if old_behavior {
self.add_passthrough({
text: content,
subs: Some(
if format_mark == "`" {
basic_subs
} else {
normal_subs
},
),
type_: Some("monospaced"),
attributes,
})
} else {
self.add_passthrough({
text: content,
subs: Some(basic_subs),
type_: Some("unquoted"),
attributes,
})
}
None =>
self.add_passthrough({
text: content,
subs: Some(basic_subs),
type_: None,
attributes: None,
})
}
}
"\{preceding}\{key}"
})
}
if text.contains(":") && (text.contains("stem:") || text.contains("math:")) {
text = inline_stem_macro_rx.replace(text, m => {
if m.matched().has_prefix("\\") {
return @rb.from(m.matched(), 1)
}
let mut type_ = m.at(1)
if type_ == "stem" {
type_ = stem_type_alias(self.document().attributes.str("stem"))
}
let mut content = normalize_text(
m.at(3),
unescape_closing_square_brackets=true,
)
if type_ == "latexmath" &&
content.has_prefix("$") &&
content.has_suffix("$") {
content = @rb.slice(content, 1, content.length() - 1)
}
let subs = match m.group(2) {
Some(s) => Some(self.resolve_pass_subs(s, subject="stem macro"))
None =>
if self.document().is_basebackend("html") {
Some(basic_subs)
} else {
None
}
}
self.add_passthrough({
text: content,
subs,
type_: Some(type_),
attributes: None,
})
})
}
text
}
///|
/// Ruby `STEM_TYPE_ALIASES[value]`.
fn stem_type_alias(v : String?) -> String {
match v {
Some("latexmath") | Some("latex") | Some("tex") => "latexmath"
_ => "asciimath"
}
}
///|
/// Restores passthroughs (Ruby `restore_passthroughs`).
fn Node::restore_passthroughs(self : Node, text : String) -> String {
pass_slot_rx.replace(text, m => {
let idx = @rb.to_i(m.at(1))
match self.passthroughs.get(idx) {
Some(pass) => {
let mut subbed = match pass.subs {
Some(s) => self.apply_subs(pass.text, s)
None => pass.text
}
match pass.type_ {
Some(t) => {
let id = match pass.attributes {
Some(a) => a.str("id")
None => None
}
subbed = Node::new_inline(
self,
Quoted,
text=subbed,
type_=t,
id?,
attributes?=pass.attributes,
).convert()
}
None => ()
}
if subbed.contains(pass_start) {
self.restore_passthroughs(subbed)
} else {
subbed
}
}
None => {
log_error("unresolved passthrough detected: \{text}")
"??pass??"
}
}
})
}
///|
fn sub_group(key : String) -> Array[Sub]? {
match key {
"none" => Some(no_subs)
"normal" => Some(normal_subs)
"verbatim" => Some(verbatim_subs)
"specialchars" => Some(basic_subs)
_ => None
}
}
///|
fn sub_hint(key : String) -> String? {
match key {
"a" => Some("attributes")
"m" => Some("macros")
"n" => Some("normal")
"p" => Some("post_replacements")
"q" => Some("quotes")
"r" => Some("replacements")
"c" => Some("specialcharacters")
"v" => Some("verbatim")
_ => None
}
}
///|
fn sub_from_name(name : String) -> Sub? {
match name {
"specialcharacters" => Some(SpecialCharacters)
"quotes" => Some(Quotes)
"attributes" => Some(Attributes)
"replacements" => Some(Replacements)
"macros" => Some(Macros)
"post_replacements" => Some(PostReplacements)
"callouts" => Some(Callouts)
_ => None
}
}
///|
/// Resolves a subs attribute value (Ruby `resolve_subs`). Returns an empty
/// array when `subs` is empty.
pub fn Node::resolve_subs(
_self : Node,
subs : String,
type_? : String = "block",
defaults? : Array[Sub],
subject? : String,
) -> Array[Sub] {
if subs == "" {
return []
}
let mut candidates : Array[String]? = None
let subs = if subs.contains(" ") { @rb.delete_chars(subs, " ") } else { subs }
let modifiers_present = sub_modifier_sniff_rx.matches(subs)
for key0 in @rb.split(subs, ",") {
let mut key = key0
let mut op : Int = 0 // 1 append, 2 remove, 3 prepend
if modifiers_present {
if key.has_prefix("+") {
op = 1
key = @rb.from(key, 1)
} else if key.has_prefix("-") {
op = 2
key = @rb.from(key, 1)
} else if key.has_suffix("+") {
op = 3
key = @rb.chop(key)
}
}
let resolved_keys : Array[String] = if type_ == "inline" &&
(key == "verbatim" || key == "v") {
["specialcharacters"]
} else {
match sub_group(key) {
Some(g) => g.map(s => s.name())
None =>
if type_ == "inline" && key.length() == 1 && sub_hint(key) is Some(rk) {
match sub_group(rk) {
Some(g) => g.map(s => s.name())
None => [rk]
}
} else {
[key]
}
}
}
if op != 0 {
let c = match candidates {
Some(c) => c
None =>
match defaults {
Some(d) => d.map(s => s.name())
None => []
}
}
candidates = Some(
match op {
1 => c + resolved_keys
3 => resolved_keys + c
_ => c.filter(k => !resolved_keys.contains(k))
},
)
} else {
candidates = Some(candidates.unwrap_or([]) + resolved_keys)
}
}
guard candidates is Some(candidates) else { return [] }
// Ruby `candidates & SUB_OPTIONS[type]` (keeps order, removes duplicates)
let valid = fn(k : String) {
match k {
"none" | "normal" | "verbatim" | "specialchars" => true
"callouts" => type_ == "block"
_ => sub_from_name(k) is Some(_)
}
}
let resolved : Array[Sub] = []
let seen : Array[String] = []
let invalid : Array[String] = []
for k in candidates {
if valid(k) {
if !seen.contains(k) {
seen.push(k)
match sub_from_name(k) {
Some(s) => resolved.push(s)
None => ()
}
}
} else {
invalid.push(k)
}
}
if !invalid.is_empty() {
let subj = match subject {
Some(s) => " for \{s}"
None => ""
}
log_warn(
"invalid substitution type\{if invalid.length() > 1 { "s" } else { "" }}\{subj}: \{invalid.join(", ")}",
)
}
resolved
}
///|
pub fn Node::resolve_block_subs(
self : Node,
subs : String,
defaults : Array[Sub]?,
subject : String,
) -> Array[Sub] {
self.resolve_subs(subs, type_="block", defaults?, subject~)
}
///|
pub fn Node::resolve_pass_subs(
self : Node,
subs : String,
subject? : String = "passthrough macro",
) -> Array[Sub] {
self.resolve_subs(subs, type_="inline", subject~)
}
///|
/// Expands a subs spec (Ruby `expand_subs`); None means no subs.
pub fn Node::expand_subs(
self : Node,
subs : String,
subject? : String,
) -> Array[Sub]? {
let r = self.resolve_subs(subs, type_="inline", subject?)
if r.is_empty() {
None
} else {
Some(r)
}
}
///|
/// Resolves and stores the subs of this block (Ruby `commit_subs`).
pub fn Node::commit_subs(self : Node) -> Unit {
let default_subs = match self.default_subs {
Some(d) => d
None =>
match self.content_model {
Simple => normal_subs
Verbatim =>
if self.context == Verse {
normal_subs
} else {
verbatim_subs
}
Raw => if self.context == Stem { basic_subs } else { no_subs }
_ => return
}
}
match self.attributes.str("subs") {
Some(custom) =>
self.subs = self.resolve_block_subs(
custom,
Some(default_subs),
self.context.name(),
)
None => self.subs = default_subs.copy()
}
if self.context == Listing && self.style == Some("source") {
match self.document().syntax_highlighter() {
Some(hl) if hl.handles_highlighting() =>
match self.subs.search(SpecialCharacters) {
Some(idx) => self.subs[idx] = Highlight
None => ()
}
_ => ()
}
}
}
///|
/// Extracts callouts from source (Ruby `extract_callouts`) before a
/// server-side syntax highlighter highlights it.
fn Node::extract_callouts(
self : Node,
source : String,
) -> (String, Map[Int, Array[(AttrVal, String)]]?) {
let marks : Map[Int, Array[(AttrVal, String)]] = Map([])
let mut autonum = 0
let mut lineno = 0
let mut last_lineno : Int? = None
let callout_rx = if self.has_attr("line-comment") {
callout_rx_for(self.attr("line-comment").unwrap_or(""), false)
} else {
callout_extract_rx
}
let lines = @rb.split(source, "\n", limit=-1).map(line => {
lineno += 1
callout_rx.replace(line, m => {
if m.has(2) {
m.matched().replace(old="\\", new="")
} else {
let guard_ : AttrVal = match m.group(1) {
Some(g) => Str(g)
None => if m.at(3) == "--" { List([""]) } else { Nil }
}
let num = if m.at(4) == "." {
autonum += 1
autonum.to_string()
} else {
m.at(4)
}
match marks.get(lineno) {
Some(a) => a.push((guard_, num))
None => marks[lineno] = [(guard_, num)]
}
last_lineno = Some(lineno)
""
}
})
})
let mut source = lines.join("\n")
match last_lineno {
Some(l) => {
if l == lineno {
source = source + "\n"
}
(source, Some(marks))
}
None => (source, None)
}
}
///|
/// Restores callouts after highlighting (Ruby `restore_callouts`).
fn Node::restore_callouts(
self : Node,
source : String,
callout_marks : Map[Int, Array[(AttrVal, String)]],
source_offset? : Int,
) -> String {
let (preamble, source) = match source_offset {
Some(off) => (@rb.slice(source, 0, off), @rb.from(source, off))
None => ("", source)
}
let mut lineno = 0
let callout_node = fn(g : AttrVal, numeral : String) {
let attrs = Attributes::new()
attrs.set("guard", g)
Node::new_inline(
self,
Callout,
text=numeral,
id?=self.document().callouts().read_next_id(),
attributes=attrs,
).convert()
}
preamble +
@rb.split(source, "\n", limit=-1)
.map(line => {
lineno += 1
match callout_marks.get(lineno) {
Some(conums) => {
callout_marks.remove(lineno)
if conums.length() == 1 {
line + callout_node(conums[0].0, conums[0].1)
} else {
line + conums.map(c => callout_node(c.0, c.1)).join(" ")
}
}
None => line
}
})
.join("\n")
}