///|
/// An immutable MDC document. Cmark locations are relative to `body()`.
/// Add `body_offset()` to cmark string offsets and `body_line() - 1` to line numbers
/// to locate Markdown in 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
}
///|
/// 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 {
UnterminatedFrontmatter
InvalidFrontmatter(line~ : Int32, column~ : Int32, message~ : String)
} 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 {
InvalidFrontmatter(line~, column~, message~) =>
raise InvalidFrontmatter(line=line + 1, column~, message~)
error => raise 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~)
}