///|
priv struct MaxDepth {
abs : Int
curr : Int
rel : Int
}
///|
priv struct IncludeFrame {
lines : Array[String]
file : String?
dir : String
path : String
lineno : Int
maxdepth : MaxDepth?
process_lines : Bool
}
///|
priv struct Conditional {
name : String
target : String?
expr : String?
skip : Bool
skipping : Bool
source_location : Cursor?
}
///|
priv struct PreState {
document : Node
sourcemap : Bool
mut maxdepth : MaxDepth?
mut include_stack : Array[IncludeFrame]
includes : Map[String, Bool]
mut skipping : Bool
mut conditional_stack : Array[Conditional]
}
///|
priv struct ReaderSnapshot {
file : String?
dir : String
path : String
lineno : Int
lines : Array[String]
mark : Cursor?
look_ahead : Int
process_lines : Bool
unescape_next_line : Bool
unterminated : Bool
pre : (MaxDepth?, Array[IncludeFrame], Bool, Array[Conditional])?
}
///|
/// A line reader over AsciiDoc source (Ruby `Reader`), optionally with
/// preprocessing of conditionals and includes (Ruby `PreprocessorReader`).
pub struct Reader {
priv mut file : String?
priv mut dir : String
priv mut path : String
priv mut lineno : Int
priv mut lines : Array[String] // reversed: next line is last
source_lines : Array[String]
priv mut mark_ : Cursor?
priv mut look_ahead : Int
mut process_lines : Bool
priv mut unescape_next_line : Bool
mut unterminated : Bool
priv mut saved : ReaderSnapshot?
priv pre : PreState?
// list continuation markers carried by line (parallel to `lines`); Ruby
// tracks these through string identity (ListContinuationMarker)
priv mut marks : Array[Bool]?
priv mut last_marked : Bool
}
///|
fn reader_init(cursor : Cursor?) -> (String?, String, String, Int) {
match cursor {
None => (None, ".", "", 1)
Some(c) =>
match c.file {
Some(f) =>
(
Some(f),
c.dir.unwrap_or(dirname(f)),
c.path.unwrap_or(basename(f)),
c.lineno,
)
None =>
(None, c.dir.unwrap_or("."), c.path.unwrap_or(""), c.lineno)
}
}
}
///|
/// Creates a reader over `data` (lines without line terminators).
pub fn Reader::new(
data : Array[String],
cursor? : Cursor,
normalize? : Bool = false,
) -> Reader {
let (file, dir, path, lineno) = reader_init(cursor)
let source_lines = if normalize {
prepare_source_array(data)
} else {
data.copy()
}
let lines = source_lines.copy()
lines.rev_in_place()
{
file,
dir,
path,
lineno,
lines,
source_lines,
mark_: None,
look_ahead: 0,
process_lines: true,
unescape_next_line: false,
unterminated: false,
saved: None,
pre: None,
marks: None,
last_marked: false,
}
}
///|
/// Creates a reader whose lines carry list-continuation markers.
fn Reader::new_marked(
data : Array[String],
marks : Array[Bool],
cursor? : Cursor,
) -> Reader {
let r = Reader::new(data, cursor?)
if marks.iter().any(m => m) {
let m = marks.copy()
m.rev_in_place()
r.marks = Some(m)
}
r
}
///|
/// Whether the line most recently consumed carried a list continuation marker.
fn Reader::last_line_marked(self : Reader) -> Bool {
self.last_marked
}
///|
/// Restores a line with its marker flag.
fn Reader::unshift_marked(self : Reader, line : String, mark : Bool) -> Unit {
if mark && self.marks is None {
self.marks = Some(Array::make(self.lines.length(), false))
}
self.lineno -= 1
self.look_ahead += 1
self.lines.push(line)
match self.marks {
Some(m) => m.push(mark)
None => ()
}
}
///|
/// Creates a reader from a string (Ruby `Reader.new str`: chomp + split on LF).
pub fn Reader::from_string(data : String, cursor? : Cursor) -> Reader {
Reader::new(@rb.split(@rb.chomp(data), "\n", limit=-1), cursor?)
}
///|
/// Creates a preprocessor reader (Ruby `PreprocessorReader.new`).
pub fn Reader::new_preprocessor(
document : Node,
data : Array[String],
cursor? : Cursor,
normalize? : Bool = false,
) -> Reader {
let (file, dir, path, lineno) = reader_init(cursor)
let default_depth = match document.attributes.get("max-include-depth") {
Some(v) if v.truthy() => v.to_i()
_ => 64
}
let pre : PreState = {
document,
sourcemap: document.sourcemap(),
maxdepth: if default_depth > 0 {
Some({ abs: default_depth, curr: default_depth, rel: default_depth, })
} else {
None
},
include_stack: [],
includes: document.catalog().includes,
skipping: false,
conditional_stack: [],
}
let r : Reader = {
file,
dir,
path,
lineno,
lines: [],
source_lines: [],
mark_: None,
look_ahead: 0,
process_lines: true,
unescape_next_line: false,
unterminated: false,
saved: None,
pre: Some(pre),
marks: None,
last_marked: false,
}
let skip_front_matter = document.attributes.truthy("skip-front-matter")
let prepared = r.pre_prepare_lines(
data,
normalize_chomp=false,
normalize~,
for_include=false,
skip_front_matter~,
)
for l in prepared {
r.source_lines.push(l)
}
let lines = prepared.copy()
lines.rev_in_place()
r.lines = lines
r
}
///|
fn Reader::pre_prepare_lines(
self : Reader,
data : Array[String],
normalize_chomp~ : Bool,
normalize~ : Bool,
for_include~ : Bool,
indent? : Int,
skip_front_matter? : Bool = false,
) -> Array[String] {
let result = if normalize || normalize_chomp {
prepare_source_array(data, trim_end=!normalize_chomp)
} else {
data.copy()
}
let pre = self.pre.unwrap()
if skip_front_matter {
match self.skip_front_matter(result) {
Some(front_matter) =>
if !for_include {
pre.document.attributes.set_str(
"front-matter",
front_matter.join("\n"),
)
}
None => ()
}
}
if for_include {
match indent {
Some(i) =>
adjust_indentation(
result,
indent_size=i,
tab_size=pre.document.attributes
.get("tabsize")
.map(v => v.to_i())
.unwrap_or(0),
)
None => ()
}
} else {
while result.length() > 0 && result[result.length() - 1] == "" {
result.pop() |> ignore
}
}
result
}
///|
fn Reader::skip_front_matter(
self : Reader,
data : Array[String],
) -> Array[String]? {
guard data.get(0) is Some(delim) && (delim == "---" || delim == "+++") else {
return None
}
let mut i = 1
let front_matter = []
while i < data.length() && data[i] != delim {
front_matter.push(data[i])
i += 1
}
if i >= data.length() {
return None
}
// remove delimiter lines and front matter
for _ in 0..<=i {
data.remove(0) |> ignore
}
self.lineno += i + 1
Some(front_matter)
}
///|
pub fn Reader::file(self : Reader) -> String? {
self.file
}
///|
pub fn Reader::dir(self : Reader) -> String {
self.dir
}
///|
pub fn Reader::path(self : Reader) -> String {
self.path
}
///|
pub fn Reader::lineno(self : Reader) -> Int {
self.lineno
}
///|
/// Whether there are more lines (processing directives as needed).
pub fn Reader::has_more_lines(self : Reader) -> Bool {
if self.pre is Some(_) {
return self.peek_line() is Some(_)
}
if self.lines.is_empty() {
self.look_ahead = 0
false
} else {
true
}
}
///|
pub fn Reader::is_empty(self : Reader) -> Bool {
!self.has_more_lines()
}
///|
pub fn Reader::next_line_empty(self : Reader) -> Bool {
match self.peek_line() {
Some(l) => l == ""
None => true
}
}
///|
fn Reader::base_peek_line(self : Reader, direct : Bool) -> String? {
while true {
let n = self.lines.length()
if direct || self.look_ahead > 0 {
if n == 0 {
return None
}
let next_line = self.lines[n - 1]
return Some(
if self.unescape_next_line {
@rb.from(next_line, 1)
} else {
next_line
},
)
}
if n > 0 {
match self.process_line(self.lines[n - 1]) {
Some(line) => return Some(line)
None => continue
}
} else {
self.look_ahead = 0
return None
}
}
None
}
///|
/// The next line without consuming it.
pub fn Reader::peek_line(self : Reader, direct? : Bool = false) -> String? {
match self.base_peek_line(direct) {
Some(line) => Some(line)
None =>
match self.pre {
None => None
Some(pre) =>
if pre.include_stack.is_empty() {
let mut end_cursor : Cursor? = None
for c in pre.conditional_stack {
let loc = match c.source_location {
Some(l) => l
None =>
match end_cursor {
Some(e) => e
None => {
let e = self.cursor_at_prev_line()
end_cursor = Some(e)
e
}
}
}
log_error(
"detected unterminated preprocessor conditional directive: \{c.name}::\{c.target.unwrap_or("")}[\{c.expr.unwrap_or("")}]",
source_location=loc,
)
}
pre.conditional_stack = []
None
} else {
self.pop_include()
self.peek_line(direct~)
}
}
}
}
///|
/// Peeks up to `num` lines (processing directives unless `direct`).
pub fn Reader::peek_lines(
self : Reader,
num? : Int,
direct? : Bool = false,
) -> Array[String] {
let old_look_ahead = self.look_ahead
let result = []
let limit = num.unwrap_or(2147483647)
for _ in 0.. result.push(l)
None => {
if direct {
self.lineno -= 1
}
break
}
}
}
if !result.is_empty() {
self.unshift_all(result)
if direct {
self.look_ahead = old_look_ahead
}
}
result
}
///|
/// Reads (consumes) the next line.
pub fn Reader::read_line(self : Reader) -> String? {
if self.look_ahead > 0 || self.has_more_lines() {
self.shift()
} else {
None
}
}
///|
/// Reads all remaining lines.
pub fn Reader::read_lines(self : Reader) -> Array[String] {
let lines = []
while self.has_more_lines() {
match self.shift() {
Some(l) => lines.push(l)
None => break
}
}
lines
}
///|
/// Reads all remaining lines joined with LF.
pub fn Reader::read(self : Reader) -> String {
self.read_lines().join("\n")
}
///|
/// Advances past the next line; returns whether a line was consumed.
pub fn Reader::advance(self : Reader) -> Bool {
self.shift() is Some(_)
}
///|
pub fn Reader::unshift_line(self : Reader, line : String) -> Unit {
self.unshift(line)
}
///|
pub fn Reader::unshift_lines(self : Reader, lines : Array[String]) -> Unit {
self.unshift_all(lines)
}
///|
/// Replaces the next line (Ruby `replace_next_line`).
pub fn Reader::replace_next_line(self : Reader, replacement : String) -> Bool {
self.shift() |> ignore
self.unshift(replacement)
true
}
///|
/// Skips blank lines; returns the number skipped, or None if at end.
pub fn Reader::skip_blank_lines(self : Reader) -> Int? {
if self.is_empty() {
return None
}
let mut num_skipped = 0
while self.peek_line() is Some(next_line) {
if next_line != "" {
return Some(num_skipped)
}
self.shift() |> ignore
num_skipped += 1
}
None
}
///|
/// Skips comment lines and comment blocks.
pub fn Reader::skip_comment_lines(self : Reader) -> Unit {
if self.is_empty() {
return
}
while self.peek_line() is Some(next_line) && next_line != "" {
if !next_line.has_prefix("//") {
break
}
if next_line.has_prefix("///") {
let ll = next_line.length()
if !(ll > 3 && next_line == @rb.repeat("/", ll)) {
break
}
self.read_lines_until(
terminator=next_line,
skip_first_line=true,
read_last_line=true,
skip_processing=true,
context=Some("comment"),
)
|> ignore
} else {
self.shift() |> ignore
}
}
}
///|
/// Skips single-line comments, returning them.
pub fn Reader::skip_line_comments(self : Reader) -> Array[String] {
if self.is_empty() {
return []
}
let comment_lines = []
while self.peek_line() is Some(next_line) && next_line != "" {
if !next_line.has_prefix("//") {
break
}
match self.shift() {
Some(l) => comment_lines.push(l)
None => break
}
}
comment_lines
}
///|
/// Discards all remaining lines.
pub fn Reader::terminate(self : Reader) -> Unit {
self.lineno += self.lines.length()
self.lines.clear()
self.marks = None
self.look_ahead = 0
}
///|
/// Reads lines until a terminator or break condition (Ruby `read_lines_until`).
/// `context`: None → use terminator in the warning; Some(None) → no warning.
pub fn Reader::read_lines_until(
self : Reader,
terminator? : String,
skip_processing? : Bool = false,
break_on_blank_lines? : Bool = false,
break_on_list_continuation? : Bool = false,
skip_line_comments? : Bool = false,
skip_first_line? : Bool = false,
read_last_line? : Bool = false,
preserve_last_line? : Bool = false,
context? : String?,
cursor? : Cursor,
cursor_at_mark? : Bool = false,
break_if? : (String) -> Bool,
) -> Array[String] {
let result = []
let mut restore_process_lines = false
if self.process_lines && skip_processing {
self.process_lines = false
restore_process_lines = true
}
let mut start_cursor : Cursor? = None
let mut break_on_blank_lines = break_on_blank_lines
let mut break_on_list_continuation = break_on_list_continuation
if terminator is Some(_) {
start_cursor = match cursor {
Some(c) => Some(c)
None => if cursor_at_mark { None } else { Some(self.cursor()) }
}
break_on_blank_lines = false
break_on_list_continuation = false
}
let mut preserve_last_line = preserve_last_line
let mut line_read = false
let mut line_restored = false
if skip_first_line {
self.shift() |> ignore
}
let mut last_line : String? = None
while self.read_line() is Some(line) {
last_line = Some(line)
let stop = match terminator {
Some(t) => line == t
None =>
(break_on_blank_lines && line == "") ||
(
break_on_list_continuation &&
line_read &&
line == "+" &&
({
preserve_last_line = true
true
})
) ||
(match break_if {
Some(f) => f(line)
None => false
})
}
if stop {
if read_last_line {
result.push(line)
}
if preserve_last_line {
self.unshift(line)
line_restored = true
}
break
}
if !(skip_line_comments && line.has_prefix("//") && !line.has_prefix("///")) {
result.push(line)
line_read = true
}
last_line = None
}
if restore_process_lines {
self.process_lines = true
if line_restored && terminator is None {
self.look_ahead -= 1
}
}
match terminator {
Some(t) if last_line != Some(t) => {
let ctx = match context {
Some(c) => c
None => Some(t)
}
match ctx {
Some(ctx) => {
let loc = match start_cursor {
Some(c) => c
None => self.cursor_at_mark()
}
log_warn("unterminated \{ctx} block", source_location=loc)
self.unterminated = true
}
None => ()
}
}
_ => ()
}
result
}
///|
/// Consumes the next line.
pub fn Reader::shift(self : Reader) -> String? {
if self.pre is Some(_) && self.unescape_next_line {
self.unescape_next_line = false
match self.base_shift() {
Some(line) => Some(@rb.from(line, 1))
None => None
}
} else {
self.base_shift()
}
}
///|
fn Reader::base_shift(self : Reader) -> String? {
self.lineno += 1
if self.look_ahead != 0 {
self.look_ahead -= 1
}
self.last_marked = match self.marks {
Some(m) => m.pop().unwrap_or(false)
None => false
}
self.lines.pop()
}
///|
fn Reader::unshift(self : Reader, line : String) -> Unit {
self.lineno -= 1
self.look_ahead += 1
self.lines.push(line)
match self.marks {
Some(m) => m.push(false)
None => ()
}
}
///|
fn Reader::unshift_all(self : Reader, lines : Array[String]) -> Unit {
self.lineno -= lines.length()
self.look_ahead += lines.length()
for i = lines.length() - 1; i >= 0; i = i - 1 {
self.lines.push(lines[i])
match self.marks {
Some(m) => m.push(false)
None => ()
}
}
}
///|
pub fn Reader::cursor(self : Reader) -> Cursor {
Cursor::new(file?=self.file, dir=self.dir, path=self.path, lineno=self.lineno)
}
///|
pub fn Reader::cursor_at_line(self : Reader, lineno : Int) -> Cursor {
Cursor::new(file?=self.file, dir=self.dir, path=self.path, lineno~)
}
///|
pub fn Reader::cursor_at_mark(self : Reader) -> Cursor {
match self.mark_ {
Some(m) => m.dup()
None => self.cursor()
}
}
///|
pub fn Reader::cursor_before_mark(self : Reader) -> Cursor {
match self.mark_ {
Some(m) => {
let c = m.dup()
c.lineno -= 1
c
}
None =>
Cursor::new(
file?=self.file,
dir=self.dir,
path=self.path,
lineno=self.lineno - 1,
)
}
}
///|
pub fn Reader::cursor_at_prev_line(self : Reader) -> Cursor {
Cursor::new(
file?=self.file,
dir=self.dir,
path=self.path,
lineno=self.lineno - 1,
)
}
///|
/// Marks the current position; returns the marked cursor.
pub fn Reader::mark(self : Reader) -> Cursor {
let c = self.cursor()
self.mark_ = Some(c)
c.dup()
}
///|
pub fn Reader::line_info(self : Reader) -> String {
"\{self.path}: line \{self.lineno}"
}
///|
/// The remaining lines (in order).
pub fn Reader::lines(self : Reader) -> Array[String] {
let l = self.lines.copy()
l.rev_in_place()
l
}
///|
/// The remaining lines joined by LF.
pub fn Reader::string(self : Reader) -> String {
self.lines().join("\n")
}
///|
/// The source lines joined by LF.
pub fn Reader::source(self : Reader) -> String {
self.source_lines.join("\n")
}
///|
/// Saves the reader state.
pub fn Reader::save(self : Reader) -> Unit {
self.saved = Some({
file: self.file,
dir: self.dir,
path: self.path,
lineno: self.lineno,
lines: self.lines.copy(),
mark: self.mark_,
look_ahead: self.look_ahead,
process_lines: self.process_lines,
unescape_next_line: self.unescape_next_line,
unterminated: self.unterminated,
pre: self.pre.map(p => {
(
p.maxdepth,
p.include_stack.copy(),
p.skipping,
p.conditional_stack.copy(),
)
}),
})
}
///|
/// Restores the saved reader state.
pub fn Reader::restore_save(self : Reader) -> Unit {
guard self.saved is Some(s) else { return }
self.file = s.file
self.dir = s.dir
self.path = s.path
self.lineno = s.lineno
self.lines = s.lines
self.mark_ = s.mark
self.look_ahead = s.look_ahead
self.process_lines = s.process_lines
self.unescape_next_line = s.unescape_next_line
self.unterminated = s.unterminated
match (self.pre, s.pre) {
(Some(p), Some((md, istack, sk, cs))) => {
p.maxdepth = md
p.include_stack = istack
p.skipping = sk
p.conditional_stack = cs
}
_ => ()
}
self.saved = None
}
///|
pub fn Reader::discard_save(self : Reader) -> Unit {
self.saved = None
}
// ---------------------------------------------------------------------------
// Preprocessor
///|
fn Reader::process_line(self : Reader, line : String) -> String? {
guard self.pre is Some(pre) && self.process_lines else {
if self.process_lines {
self.look_ahead += 1
}
return Some(line)
}
if line == "" {
if pre.skipping {
self.shift() |> ignore
return None
}
self.look_ahead += 1
return Some(line)
}
if line.has_suffix("]") && !line.has_prefix("[") && line.contains("::") {
if line.contains("if") && conditional_directive_rx.find(line) is Some(m) {
if m.group(1) == Some("\\") {
self.unescape_next_line = true
self.look_ahead += 1
return Some(@rb.from(line, 1))
} else if self.preprocess_conditional_directive(
m.at(2),
m.at(3),
m.group(4),
m.group(5),
) {
self.shift() |> ignore
return None
} else {
self.look_ahead += 1
return Some(line)
}
} else if pre.skipping {
self.shift() |> ignore
return None
} else if (line.has_prefix("inc") || line.has_prefix("\\inc")) &&
include_directive_rx.find(line) is Some(m) {
if m.group(1) == Some("\\") {
self.unescape_next_line = true
self.look_ahead += 1
return Some(@rb.from(line, 1))
} else {
match self.preprocess_include_directive(m.at(2), m.group(3)) {
true => return None
false => {
self.look_ahead += 1
return Some(line)
}
}
}
} else {
self.look_ahead += 1
return Some(line)
}
} else if pre.skipping {
self.shift() |> ignore
None
} else {
self.look_ahead += 1
Some(line)
}
}
///|
/// A value of an ifeval expression operand.
priv enum ExprVal {
EStr(String)
EInt(Int)
EFloat(Double)
EBool(Bool)
ENil
} derive(Eq)
///|
fn compare_expr(lhs : ExprVal, op : String, rhs : ExprVal) -> Bool raise {
let num = fn(v : ExprVal) -> Double? {
match v {
EInt(i) => Some(i.to_double())
EFloat(f) => Some(f)
_ => None
}
}
match op {
"==" =>
match (num(lhs), num(rhs)) {
(Some(a), Some(b)) => a == b
_ => lhs == rhs
}
"!=" =>
match (num(lhs), num(rhs)) {
(Some(a), Some(b)) => a != b
_ => lhs != rhs
}
_ => {
let c = match (lhs, rhs) {
(EStr(a), EStr(b)) => @rb.lex_compare(a, b)
_ =>
match (num(lhs), num(rhs)) {
(Some(a), Some(b)) => a.compare(b)
_ => raise Failure("incomparable")
}
}
match op {
"<" => c < 0
"<=" => c <= 0
">" => c > 0
">=" => c >= 0
_ => raise Failure("bad op")
}
}
}
}
///|
fn Reader::resolve_expr_val(self : Reader, val : String) -> ExprVal {
let mut val = val
let mut quoted = false
if (val.has_prefix("\"") && val.has_suffix("\"")) ||
(val.has_prefix("'") && val.has_suffix("'")) {
quoted = true
val = @rb.slice(val, 1, val.length() - 1)
}
if val.contains("{") {
val = self.pre.unwrap().document.sub_attributes(
val,
attribute_missing="drop",
)
}
if quoted {
EStr(val)
} else if val == "" {
ENil
} else if val == "true" {
EBool(true)
} else if val == "false" {
EBool(false)
} else if @rb.rstrip(val) == "" {
EStr(" ")
} else if val.contains(".") {
EFloat(@rb.to_f(val))
} else {
EInt(@rb.to_i(val))
}
}
///|
fn Reader::preprocess_conditional_directive(
self : Reader,
name : String,
target : String,
delimiter : String?,
text : String?,
) -> Bool {
let pre = self.pre.unwrap()
let doc = pre.document
let no_target = target == ""
let target = if no_target { target } else { @rb.downcase(target) }
let mut skip = false
if name == "endif" {
if text is Some(t) {
log_error(
"malformed preprocessor directive - text not permitted: endif::\{target}[\{t}]",
source_location=self.cursor(),
)
} else if pre.conditional_stack.is_empty() {
log_error(
"unmatched preprocessor directive: endif::\{target}[]",
source_location=self.cursor(),
)
} else {
let top = pre.conditional_stack[pre.conditional_stack.length() - 1]
if no_target || Some(target) == top.target {
pre.conditional_stack.pop() |> ignore
pre.skipping = match pre.conditional_stack.last() {
Some(c) => c.skipping
None => false
}
} else {
log_error(
"mismatched preprocessor directive: endif::\{target}[], expected endif::\{top.target.unwrap_or("")}[]",
source_location=self.cursor(),
)
}
}
return true
} else if pre.skipping {
if name == "ifeval" {
if !(no_target &&
(match text {
Some(t) => eval_expression_rx.matches(@rb.strip(t))
None => false
})) {
return true
}
} else if no_target {
return true
}
skip = false
} else {
match name {
"ifdef" => {
if no_target {
log_error(
"malformed preprocessor directive - missing target: ifdef::[\{text.unwrap_or("")}]",
source_location=self.cursor(),
)
return true
}
skip = match delimiter {
Some(",") =>
@rb.split(target, ",", limit=-1)
.iter()
.all(n => !doc.attributes.contains(n))
Some("+") =>
@rb.split(target, "+", limit=-1)
.iter()
.any(n => !doc.attributes.contains(n))
_ => !doc.attributes.contains(target)
}
}
"ifndef" => {
if no_target {
log_error(
"malformed preprocessor directive - missing target: ifndef::[\{text.unwrap_or("")}]",
source_location=self.cursor(),
)
return true
}
skip = match delimiter {
Some(",") =>
@rb.split(target, ",", limit=-1)
.iter()
.any(n => doc.attributes.contains(n))
Some("+") =>
@rb.split(target, "+", limit=-1)
.iter()
.all(n => doc.attributes.contains(n))
_ => doc.attributes.contains(target)
}
}
"ifeval" =>
if no_target {
match text {
Some(t) if eval_expression_rx.find(@rb.strip(t)) is Some(m) => {
let lhs = self.resolve_expr_val(m.at(1))
let op = m.at(2)
let rhs = self.resolve_expr_val(m.at(3))
skip = !(compare_expr(lhs, op, rhs) catch { _ => false })
}
_ => {
log_error(
"malformed preprocessor directive - \{if text is Some(_) { "invalid expression" } else { "missing expression" }}: ifeval::[\{text.unwrap_or("")}]",
source_location=self.cursor(),
)
return true
}
}
} else {
log_error(
"malformed preprocessor directive - target not permitted: ifeval::\{target}[\{text.unwrap_or("")}]",
source_location=self.cursor(),
)
return true
}
_ => ()
}
}
if name == "ifeval" {
if skip {
pre.skipping = true
}
pre.conditional_stack.push({
name,
target: None,
expr: text,
skip,
skipping: pre.skipping,
source_location: if pre.sourcemap {
Some(self.cursor())
} else {
None
},
})
} else {
match text {
Some(t) =>
if !pre.skipping && !skip {
self.replace_next_line(@rb.rstrip(t)) |> ignore
self.unshift("")
if t.has_prefix("include::") {
self.look_ahead -= 1
}
}
None => {
if skip {
pre.skipping = true
}
pre.conditional_stack.push({
name,
target: Some(target),
expr: None,
skip,
skipping: pre.skipping,
source_location: if pre.sourcemap {
Some(self.cursor())
} else {
None
},
})
}
}
}
true
}
///|
fn split_delimited_value(val : String) -> Array[String] {
if val.contains(",") {
@rb.split(val, ",")
} else {
@rb.split(val, ";")
}
}
///|
fn Reader::preprocess_include_directive(
self : Reader,
target : String,
attrlist : String?,
) -> Bool {
let pre = self.pre.unwrap()
let doc = pre.document
let attrlist_s = attrlist.unwrap_or("")
let mut expanded_target = target
if target.contains("{") {
let attr_missing = doc.attributes
.str("attribute-missing")
.unwrap_or(compliance.attribute_missing)
expanded_target = doc.sub_attributes(
target,
attribute_missing=if attr_missing == "warn" {
"drop-line"
} else {
attr_missing
},
)
if expanded_target == "" {
let dropped_due_to_missing = fn() {
doc.sub_attributes(
target + " ",
attribute_missing="drop-line",
drop_line_ignore=true,
) ==
""
}
if attr_missing == "drop-line" && dropped_due_to_missing() {
log_info(
"include dropped due to missing attribute: include::\{target}[\{attrlist_s}]",
source_location=self.cursor(),
)
self.shift() |> ignore
return true
} else if doc
.parse_attributes(attrlist_s, [], sub_input=true)
.truthy("optional-option") {
let reason = if attr_missing == "warn" && dropped_due_to_missing() {
"due to missing attribute"
} else {
"because resolved target is blank"
}
log_info(
"optional include dropped \{reason}: include::\{target}[\{attrlist_s}]",
source_location=self.cursor(),
)
self.shift() |> ignore
return true
} else {
let reason = if attr_missing == "warn" && dropped_due_to_missing() {
"due to missing attribute"
} else {
"because resolved target is blank"
}
log_warn(
"include dropped \{reason}: include::\{target}[\{attrlist_s}]",
source_location=self.cursor(),
)
return self.replace_next_line(
"Unresolved directive in \{self.path} - include::\{target}[\{attrlist_s}]",
)
}
}
}
match doc.extensions() {
Some(exts) =>
for ext in exts.include_processors() {
if (ext.handles)(doc, expanded_target) {
self.shift() |> ignore
(ext.process)(
doc,
self,
expanded_target,
doc.parse_attributes(attrlist_s, [], sub_input=true),
) catch {
e => doc.abort_processing(e)
}
return true
}
}
None => ()
}
if doc.safe() >= SAFE_SECURE {
let t = if expanded_target.contains(" ") {
"pass:c[\{expanded_target}]"
} else {
expanded_target
}
let link_attrlist = if doc.has_attr("compat-mode") {
attrlist_s
} else {
"role=include\{match attrlist { Some(a) => "," + a; None => "" }}"
}
return self.replace_next_line("link:\{t}[\{link_attrlist}]")
}
guard pre.maxdepth is Some(maxdepth) else { return false }
if pre.include_stack.length() >= maxdepth.curr {
log_error(
"maximum include depth of \{maxdepth.rel} exceeded",
source_location=self.cursor(),
)
return false
}
let parsed_attrs = doc.parse_attributes(attrlist_s, [], sub_input=true)
let (inc_path, target_type, relpath) = match
self.resolve_include_path(expanded_target, attrlist, parsed_attrs) {
IncludeResolved(p, t, r) => (p, t, r)
IncludeHandled(b) => return b
}
let encoding = parsed_attrs.str("encoding")
let read_text = fn() -> String? {
doc.vfs().read(inc_path).map(data => decode_text_with(data, encoding))
}
let mut inc_linenos : Array[Int]? = None // -1 means "to the end"
let mut inc_tags : Array[(String, Bool)]? = None
if attrlist is Some(_) {
if parsed_attrs.contains("lines") {
let linenos = []
let mut open_end = false
for
linedef in split_delimited_value(
parsed_attrs.str("lines").unwrap_or(""),
) {
if linedef.contains("..") {
let (from, _, to) = @rb.partition(linedef, "..")
let to_i = @rb.to_i(to)
if to == "" || to_i < 0 {
linenos.push(@rb.to_i(from))
open_end = true
} else {
for i in @rb.to_i(from)..<=to_i {
linenos.push(i)
}
}
} else {
linenos.push(@rb.to_i(linedef))
}
}
if !linenos.is_empty() {
linenos.sort()
let uniq = []
for v in linenos {
if uniq.is_empty() || uniq[uniq.length() - 1] != v {
uniq.push(v)
}
}
if open_end {
uniq.push(-1)
}
inc_linenos = Some(uniq)
} else if open_end {
inc_linenos = Some([-1])
}
} else if parsed_attrs.contains("tag") {
let tag = parsed_attrs.str("tag").unwrap_or("")
if tag != "" && tag != "!" {
inc_tags = Some(
if tag.has_prefix("!") {
[(@rb.from(tag, 1), false)]
} else {
[(tag, true)]
},
)
}
} else if parsed_attrs.contains("tags") {
let tags : Array[(String, Bool)] = []
let put = fn(k : String, v : Bool) {
match tags.search_by(t => t.0 == k) {
Some(i) => tags[i] = (k, v)
None => tags.push((k, v))
}
}
for
tagdef in split_delimited_value(parsed_attrs.str("tags").unwrap_or("")) {
if tagdef != "" && tagdef != "!" {
if tagdef.has_prefix("!") {
put(@rb.from(tagdef, 1), false)
} else {
put(tagdef, true)
}
}
}
if !tags.is_empty() {
inc_tags = Some(tags)
}
}
}
let unresolved = fn() {
log_error(
"include \{target_type} not readable: \{inc_path}",
source_location=self.cursor(),
)
self.replace_next_line(
"Unresolved directive in \{self.path} - include::\{expanded_target}[\{attrlist_s}]",
)
}
match inc_linenos {
Some(linenos) => {
guard read_text() is Some(content) else { return unresolved() }
let inc_lines = []
let mut inc_offset : Int? = None
let mut inc_lineno = 0
let mut remaining = linenos
let mut select_remaining = false
for l in each_line(content) {
inc_lineno += 1
if select_remaining || (remaining.length() > 0 && remaining[0] == -1) {
select_remaining = true
if inc_offset is None {
inc_offset = Some(inc_lineno)
}
inc_lines.push(l)
} else {
if remaining.length() > 0 && remaining[0] == inc_lineno {
if inc_offset is None {
inc_offset = Some(inc_lineno)
}
inc_lines.push(l)
remaining = remaining[1:].to_owned()
}
if remaining.is_empty() {
break
}
}
}
self.shift() |> ignore
match inc_offset {
Some(off) => {
parsed_attrs.set_str("partial-option", "")
self.push_include(
inc_lines,
Some(inc_path),
Some(relpath),
off,
parsed_attrs,
)
}
None => ()
}
}
None =>
match inc_tags {
Some(tags0) => {
guard read_text() is Some(content) else { return unresolved() }
let tags = tags0.copy()
let get_tag = fn(k : String) -> Bool? {
match tags.search_by(t => t.0 == k) {
Some(i) => Some(tags[i].1)
None => None
}
}
let remove_tag = fn(k : String) -> Bool? {
match tags.search_by(t => t.0 == k) {
Some(i) => Some(tags.remove(i).1)
None => None
}
}
let inc_lines = []
let mut inc_offset : Int? = None
let mut inc_lineno = 0
let tag_stack : Array[(String, Bool, Int)] = []
let tags_selected : Array[String] = []
let mut active_tag : String? = None
let mut select = false
let mut base_select = false
let mut wildcard : Bool? = None
if get_tag("**") is Some(_) {
select = remove_tag("**").unwrap()
base_select = select
if get_tag("*") is Some(_) {
wildcard = remove_tag("*")
} else if !select && tags.length() > 0 && !tags[0].1 {
wildcard = Some(true)
}
} else if get_tag("*") is Some(_) {
if tags[0].0 == "*" {
wildcard = remove_tag("*")
select = !wildcard.unwrap()
base_select = select
} else {
select = false
base_select = false
wildcard = remove_tag("*")
}
} else {
select = !tags.iter().any(t => t.1)
base_select = select
}
for l in each_line(content) {
inc_lineno += 1
if l.contains("::") &&
l.contains("[]") &&
tag_directive_rx.find(l) is Some(m) {
let this_tag = m.at(2)
if m.has(1) {
if Some(this_tag) == active_tag {
tag_stack.pop() |> ignore
match tag_stack.last() {
Some((t, s, _)) => {
active_tag = Some(t)
select = s
}
None => {
active_tag = None
select = base_select
}
}
} else if get_tag(this_tag) is Some(_) {
let include_cursor = self.create_include_cursor(
inc_path, expanded_target, inc_lineno,
)
match tag_stack.search_by(t => t.0 == this_tag) {
Some(idx) => {
tag_stack.remove(idx) |> ignore
log_warn(
"mismatched end tag (expected '\{active_tag.unwrap_or("")}' but found '\{this_tag}') at line \{inc_lineno} of include \{target_type}: \{inc_path}",
source_location=self.cursor(),
include_location=include_cursor,
)
}
None =>
log_warn(
"unexpected end tag '\{this_tag}' at line \{inc_lineno} of include \{target_type}: \{inc_path}",
source_location=self.cursor(),
include_location=include_cursor,
)
}
}
} else {
match get_tag(this_tag) {
Some(v) => {
select = v
if v {
tags_selected.push(this_tag)
}
active_tag = Some(this_tag)
tag_stack.push((this_tag, select, inc_lineno))
}
None =>
match wildcard {
Some(w) => {
select = if active_tag is Some(_) && !select {
false
} else {
w
}
active_tag = Some(this_tag)
tag_stack.push((this_tag, select, inc_lineno))
}
None => ()
}
}
}
} else if select {
if inc_offset is None {
inc_offset = Some(inc_lineno)
}
inc_lines.push(l)
}
}
for t in tag_stack {
log_warn(
"detected unclosed tag '\{t.0}' starting at line \{t.2} of include \{target_type}: \{inc_path}",
source_location=self.cursor(),
include_location=self.create_include_cursor(
inc_path,
expanded_target,
t.2,
),
)
}
let missing = tags
.filter(t => t.1 && !tags_selected.contains(t.0))
.map(t => t.0)
if !missing.is_empty() {
log_warn(
"tag\{if missing.length() > 1 { "s" } else { "" }} '\{missing.join(", ")}' not found in include \{target_type}: \{inc_path}",
source_location=self.cursor(),
)
}
self.shift() |> ignore
match inc_offset {
Some(off) => {
if !(base_select && wildcard != Some(false) && tags.is_empty()) {
parsed_attrs.set_str("partial-option", "")
}
self.push_include(
inc_lines,
Some(inc_path),
Some(relpath),
off,
parsed_attrs,
)
}
None => ()
}
}
None => {
guard read_text() is Some(content) else { return unresolved() }
self.shift() |> ignore
self.push_include_string(
content,
Some(inc_path),
Some(relpath),
1,
parsed_attrs,
)
}
}
}
true
}
///|
/// Splits text into lines keeping their line terminators (Ruby `each_line`).
fn each_line(content : String) -> Array[String] {
let out = []
let n = content.length()
let mut start = 0
for i in 0.. IncludeResolution {
let doc = self.pre.unwrap().document
let attrlist_s = attrlist.unwrap_or("")
if is_uriish(target) || is_uriish(self.dir) {
let target = if is_uriish(target) {
target
} else {
"\{self.dir}/\{target}"
}
if !doc.has_attr("allow-uri-read") {
log_warn(
"cannot include contents of URI: \{target} (allow-uri-read attribute not enabled)",
source_location=self.cursor(),
)
let t = if target.contains(" ") { "pass:c[\{target}]" } else { target }
let link_attrlist = if doc.has_attr("compat-mode") {
attrlist_s
} else {
"role=include\{match attrlist { Some(a) => "," + a; None => "" }}"
}
return IncludeHandled(
self.replace_next_line("link:\{t}[\{link_attrlist}]"),
)
}
return IncludeResolved(target, "uri", target)
}
let inc_path = doc.normalize_system_path(
target,
start=self.dir,
target_name="include file",
)
if !doc.vfs().is_file(inc_path) {
if attributes.truthy("optional-option") {
log_info(
"optional include dropped because include file not found: \{inc_path}",
source_location=self.cursor(),
)
self.shift() |> ignore
return IncludeHandled(true)
} else {
log_error(
"include file not found: \{inc_path}",
source_location=self.cursor(),
)
return IncludeHandled(
self.replace_next_line(
"Unresolved directive in \{self.path} - include::\{target}[\{attrlist_s}]",
),
)
}
}
let relpath = doc.path_resolver().relative_path(inc_path, doc.base_dir())
IncludeResolved(inc_path, "file", relpath)
}
///|
fn Reader::create_include_cursor(
_self : Reader,
file : String,
path : String,
lineno : Int,
) -> Cursor {
Cursor::new(file~, dir=dirname(file), path~, lineno~)
}
///|
/// Pushes include content given as lines with terminators.
fn Reader::push_include(
self : Reader,
data : Array[String],
file : String?,
path : String?,
lineno : Int,
attributes : Attributes,
) -> Unit {
self.push_include_impl(data, file, path, lineno, attributes)
}
///|
fn Reader::push_include_string(
self : Reader,
data : String,
file : String?,
path : String?,
lineno : Int,
attributes : Attributes,
) -> Unit {
self.push_include_impl(each_line(data), file, path, lineno, attributes)
}
///|
/// Pushes include content (Ruby `push_include`), also usable by include processors.
pub fn Reader::push_include_lines(
self : Reader,
data : Array[String],
file? : String,
path? : String,
lineno? : Int = 1,
attributes? : Attributes = Attributes::new(),
) -> Unit {
self.push_include_impl(data, file, path, lineno, attributes)
}
///|
fn Reader::push_include_impl(
self : Reader,
data : Array[String],
file : String?,
path : String?,
lineno : Int,
attributes : Attributes,
) -> Unit {
let pre = self.pre.unwrap()
pre.include_stack.push({
lines: self.lines,
file: self.file,
dir: self.dir,
path: self.path,
lineno: self.lineno,
maxdepth: pre.maxdepth,
process_lines: self.process_lines,
})
self.file = file
match file {
Some(f) => {
self.dir = dirname(f)
let p = path.unwrap_or(basename(f))
self.path = p
self.process_lines = [".adoc", ".asciidoc", ".asc", ".ad", ".txt"]
.iter()
.any(ext => f.has_suffix(ext))
if self.process_lines {
let key = match p.rev_find(".") {
Some(i) => @rb.slice(p, 0, i)
None => p
}
if !pre.includes.contains(key) || pre.includes[key] == false {
pre.includes[key] = !attributes.truthy("partial-option")
}
}
}
None => {
self.dir = "."
self.process_lines = true
match path {
Some(p) => {
self.path = p
let key = rootname(p)
if !pre.includes.contains(key) || pre.includes[key] == false {
pre.includes[key] = !attributes.truthy("partial-option")
}
}
None => self.path = ""
}
}
}
self.lineno = lineno
match pre.maxdepth {
Some(md) if attributes.contains("depth") => {
let rel = attributes.get("depth").unwrap().to_i()
if rel > 0 {
let mut curr = pre.include_stack.length() + rel
let mut rel = rel
if curr > md.abs {
curr = md.abs
rel = md.abs
}
pre.maxdepth = Some({ abs: md.abs, curr, rel, })
} else {
pre.maxdepth = Some({
abs: md.abs,
curr: pre.include_stack.length(),
rel: 0,
})
}
}
_ => ()
}
let indent = match attributes.get("indent") {
Some(v) if v.truthy() => Some(v.to_i())
_ => None
}
let lines = self.pre_prepare_lines(
data,
normalize_chomp=!self.process_lines,
normalize=self.process_lines,
for_include=true,
indent?,
skip_front_matter=attributes.truthy("skip-front-matter-option"),
)
if lines.is_empty() {
self.pop_include()
} else {
if attributes.contains("leveloffset") {
let leveloffset = pre.document.attr("leveloffset")
let head = match leveloffset {
Some(l) => ":leveloffset: \{l}"
None => ":leveloffset!:"
}
// the line stack is reversed: the last element is read first
lines.rev_in_place()
self.lines = [head, ""] +
lines +
["", ":leveloffset: \{attributes.str("leveloffset").unwrap_or("")}"]
self.lineno -= 2
} else {
lines.rev_in_place()
self.lines = lines
}
self.look_ahead = 0
}
}
///|
fn Reader::pop_include(self : Reader) -> Unit {
guard self.pre is Some(pre) && pre.include_stack.pop() is Some(frame) else {
return
}
self.lines = frame.lines
self.file = frame.file
self.dir = frame.dir
self.path = frame.path
self.lineno = frame.lineno
pre.maxdepth = frame.maxdepth
self.process_lines = frame.process_lines
self.look_ahead = 0
}
///|
/// Current include depth.
pub fn Reader::include_depth(self : Reader) -> Int {
match self.pre {
Some(p) => p.include_stack.length()
None => 0
}
}