///|
/// An immutable MDC document. Cmark locations are relative to `body()`;
/// `source_range` translates them to the original input. Offsets count UTF-16
/// code units.
struct Document {
frontmatter : Frontmatter?
markdown : @cmark.Doc
body : String
body_offset : Int32
body_line : Int32
} derive(Debug)
///|
pub extend Document with @debug.Debug::{to_repr}
///|
pub fn Document::frontmatter(self : Document) -> Frontmatter? {
self.frontmatter
}
///|
pub fn Document::body(self : Document) -> String {
self.body
}
///|
pub fn Document::body_offset(self : Document) -> Int32 {
self.body_offset
}
///|
pub fn Document::body_line(self : Document) -> Int32 {
self.body_line
}
///|
/// The range in the original input of a cmark location in the body, or `None`
/// for a location that cmark did not record or that is not a location in this
/// document's body.
pub fn Document::source_range(
self : Document,
loc : @cmark_base.TextLoc,
) -> SourceRange? {
guard !loc.is_none() else { return None }
let position = (line : @cmark_base.LinePos, ccode : Int32) => {
// A first body line that is also the first source line starts after the
// byte order mark, when there is one.
let shift = if self.body_line == 1 && line.0 == 1 {
self.body_offset
} else {
0
}
TextPosition(
line=self.body_line + line.0 - 1,
column=ccode - line.1 + 1 + shift,
)
}
let start_offset = self.body_offset + loc.first_ccode
let end_offset = if loc.is_empty() {
start_offset
} else {
self.body_offset + loc.last_ccode + 1
}
guard end_offset <= self.body_offset + self.body.length() else { return None }
try {
let start = position(loc.first_line, loc.first_ccode)
let end = if loc.is_empty() {
start
} else {
position(loc.last_line, loc.last_ccode)
}
SourceRange(start~, end~, start_offset~, end_offset~)
} catch {
_ => None
} noraise {
range => Some(range)
}
}
///|
/// An owned cmark copy for rendering or transformation. Its mutable maps and
/// sequences share no storage with the document or its traversal views.
pub fn Document::to_cmark(self : Document) -> @cmark.Doc {
copy_doc(self.markdown)
}
///|
pub fn Document::root(self : Document) -> Node {
Node::Document(self)
}
///|
pub(all) suberror ParseError {
/// The frontmatter has no closing delimiter line.
UnterminatedFrontmatter
/// The frontmatter payload is not one YAML mapping. An `InvalidYaml`
/// position is a position in the document.
InvalidFrontmatter(FrontmatterError)
} derive(Debug)
///|
pub extend ParseError with @debug.Debug::{to_repr}
///|
// Return the line text and the offset after its newline, accepting LF, CRLF
// and CR. Delimiters may have trailing horizontal whitespace.
fn line_at(source : String, start : Int32) -> (String, Int32) {
let mut end = start
while end < source.length() {
let c = source.code_unit_at(end)
if c == 10 || c == 13 {
break
}
end += 1
}
let text = source[start:end].to_owned()
let mut next = end
if next < source.length() {
next += 1
if source.code_unit_at(end) == 13 &&
next < source.length() &&
source.code_unit_at(next) == 10 {
next += 1
}
}
(text, next)
}
///|
/// Parse CommonMark with an optional leading YAML mapping between `---` lines.
/// `...` also closes frontmatter. A UTF-8 BOM decoded as U+FEFF is accepted.
/// An opener must be the first line, with no leading whitespace. An empty
/// payload is an empty mapping. Malformed YAML and non-mappings are errors.
/// Cmark's `strict` option defaults to true. Set it to false for extensions.
pub fn Document::Document(
source : String,
strict~ : Bool = true,
layout~ : Bool = true,
locs~ : Bool = true,
file~ : String = "",
) -> Document raise ParseError {
let start = if source.has_prefix("\u{FEFF}") { 1 } else { 0 }
let (first, after_first) = line_at(source, start)
let mut body_offset = start
let mut body_line = 1
let frontmatter = if first.trim_end(chars=" \t") == "---" {
let mut offset = after_first
let mut line = 2
let mut end = None
while offset < source.length() {
let (text, next) = line_at(source, offset)
let delimiter = text.trim_end(chars=" \t")
if delimiter == "---" || delimiter == "..." {
end = Some(offset)
body_offset = next
body_line = if next > offset + text.length() { line + 1 } else { line }
break
}
offset = next
line += 1
}
guard end is Some(end) else { raise UnterminatedFrontmatter }
let frontmatter = Frontmatter::Frontmatter(
source[after_first:end].to_owned(),
) catch {
// The payload starts on the second line of the document.
InvalidYaml(at~, message~) =>
raise InvalidFrontmatter(
InvalidYaml(at={ ..at, line: at.line + 1, }, message~),
)
error => raise InvalidFrontmatter(error)
}
Some(frontmatter)
} else {
None
}
let body = source[body_offset:].to_owned()
let markdown = @cmark.Doc::from_string(body, strict~, layout~, locs~, file~)
{ frontmatter, markdown, body, body_offset, body_line, }
}
///|
/// Parse an immutable document. Equivalent to `Document::Document`.
pub fn parse(
source : String,
strict~ : Bool = true,
layout~ : Bool = true,
locs~ : Bool = true,
file~ : String = "",
) -> Document raise ParseError {
Document::Document(source, strict~, layout~, locs~, file~)
}