///|
/// A frontmatter value. Every scalar is kept as text, so dates, timestamps and
/// numbers stay as written. Typed accessors read the text when asked.
pub(all) enum Value {
Scalar(String)
Seq(ReadOnlyArray[Value])
Mapping(Fields)
Null
} derive(Eq, Debug)
///|
/// An ordered set of keys and values. Keys keep their source order.
struct Fields {
entries : ReadOnlyArray[(String, Value)]
} derive(Eq, Debug)
///|
/// Builds fields from ordered pairs. A later pair for the same key replaces
/// the value of the earlier pair and keeps its position.
pub fn Fields::Fields(pairs : ArrayView[(String, Value)]) -> Fields {
let out : Array[(String, Value)] = []
for pair in pairs {
match out.search_by(p => p.0 == pair.0) {
Some(i) => out[i] = pair
None => out.push(pair)
}
}
{ entries: ReadOnlyArray::from_array(out), }
}
///|
pub fn Fields::entries(self : Fields) -> ReadOnlyArray[(String, Value)] {
self.entries
}
///|
pub fn Fields::keys(self : Fields) -> Array[String] {
self.entries.iter().map(e => e.0).collect()
}
///|
pub fn Fields::get(self : Fields, key : String) -> Value? {
for entry in self.entries {
if entry.0 == key {
return Some(entry.1)
}
}
None
}
///|
/// The text of a scalar value, or `None` for other values.
pub fn Value::as_text(self : Value) -> String? {
match self {
Scalar(text) => Some(text)
_ => None
}
}
///|
/// Reads a value as a list of texts. A scalar becomes a list with one item.
/// Items that are not scalars are left out.
pub fn Value::as_texts(self : Value) -> Array[String] {
match self {
Scalar(text) => [text]
Seq(items) => items.iter().filter_map(v => v.as_text()).collect()
_ => []
}
}
///|
/// Reads a value as a list of mappings. A mapping becomes a list with one item.
pub fn Value::as_mappings(self : Value) -> Array[Fields] {
match self {
Mapping(fields) => [fields]
Seq(items) =>
items
.iter()
.filter_map(v => {
match v {
Mapping(fields) => Some(fields)
_ => None
}
})
.collect()
_ => []
}
}
///|
/// Reads `true`, `yes` and `on` as true and `false`, `no` and `off` as false,
/// ignoring letter case.
pub fn Value::as_bool(self : Value) -> Bool? {
match self {
Scalar(text) =>
match text.to_lower() {
"true" | "yes" | "on" => Some(true)
"false" | "no" | "off" => Some(false)
_ => None
}
_ => None
}
}
///|
pub fn Value::as_int(self : Value) -> Int32? {
match self {
Scalar(text) => parse_int_text(text)
_ => None
}
}
///|
fn parse_int_text(text : String) -> Int32? {
let text = text.trim(chars=" \t").to_owned()
if text.length() == 0 {
return None
}
let mut i = 0
let negative = text[0] == '-'
if text[0] == '-' || text[0] == '+' {
i = 1
}
if i >= text.length() {
return None
}
let mut value = 0
while i < text.length() {
let c = text[i]
if c < '0' || c > '9' {
return None
}
value = value * 10 + (c.to_int() - '0'.to_int())
i += 1
}
Some(if negative { -value } else { value })
}
///|
/// Converts a YAML value into a frontmatter value.
fn value_of_yaml(yaml : @yaml.Yaml) -> Value {
match yaml {
Real(_, repr~) => Scalar(repr)
Integer(n) => Scalar(n.to_string())
String(s) => Scalar(s)
Boolean(b) => Scalar(b.to_string())
Array(items) => Seq(ReadOnlyArray::from_array(items.map(value_of_yaml)))
Map(map) => {
let pairs = []
for key, value in map {
pairs.push((key, value_of_yaml(value)))
}
Mapping(Fields(pairs))
}
Null | BadValue => Null
}
}
///|
/// The parsed frontmatter of a document: the raw block, its ordered fields and
/// the location of each top-level key.
struct Metadata {
raw : String
fields : Fields
entries : ReadOnlyArray[RawEntry]
/// The file line of the first line of the raw block.
first_line : Int32
} derive(Eq, Debug)
///|
/// Raised when a frontmatter block cannot be read.
pub(all) suberror MetadataError {
/// The YAML is invalid, or not a mapping. The line and column are file positions.
InvalidYaml(line~ : Int32, column~ : Int32, message~ : String)
/// The block has no closing delimiter line.
Unterminated
} derive(Eq, Debug)
///|
/// Parses a raw frontmatter block. `first_line` is the file line of the first
/// line of `raw`; error positions are file positions.
pub fn Metadata::Metadata(
raw : String,
first_line~ : Int32 = 2,
) -> Metadata raise MetadataError {
let frontmatter = @mdc.Frontmatter::Frontmatter(raw) catch {
InvalidYaml(at~, message~) =>
raise InvalidYaml(
line=at.line() + first_line - 1,
column=at.column(),
message~,
)
NotAMapping | MultipleDocuments(_) =>
raise InvalidYaml(
line=first_line,
column=1,
message="frontmatter must be a YAML mapping",
)
}
let entries = scan_entries(raw)
let pairs = []
match frontmatter.value() {
Map(map) =>
for key, value in map {
let value = match value {
Integer(_) | Real(_, ..) | Boolean(_) =>
match entries.search_by(e => e.key == key) {
Some(i) if entries[i].inline.length() > 0 =>
Scalar(entries[i].inline)
_ => value_of_yaml(value)
}
_ => value_of_yaml(value)
}
pairs.push((key, value))
}
_ => ()
}
{
raw,
fields: Fields(pairs),
entries: ReadOnlyArray::from_array(entries),
first_line,
}
}
///|
/// An empty frontmatter, as in an empty block.
pub fn Metadata::empty() -> Metadata {
{
raw: "",
fields: Fields([]),
entries: ReadOnlyArray::from_array([]),
first_line: 2,
}
}
///|
/// Metadata from fields, for derived metadata that has no source text.
pub fn Metadata::of_fields(fields : Fields) -> Metadata {
{ raw: "", fields, entries: ReadOnlyArray::from_array([]), first_line: 0, }
}
///|
pub fn Metadata::raw(self : Metadata) -> String {
self.raw
}
///|
pub fn Metadata::fields(self : Metadata) -> Fields {
self.fields
}
///|
/// The top-level keys in source order.
pub fn Metadata::keys(self : Metadata) -> Array[String] {
self.fields.keys()
}
///|
pub fn Metadata::get(self : Metadata, key : String) -> Value? {
self.fields.get(key)
}
///|
pub fn Metadata::contains(self : Metadata, key : String) -> Bool {
self.fields.get(key) is Some(_)
}
///|
/// The text of a scalar key. An empty or missing value gives `None`.
pub fn Metadata::text(self : Metadata, key : String) -> String? {
match self.fields.get(key) {
Some(Scalar(text)) if text.length() > 0 => Some(text)
_ => None
}
}
///|
/// A key read as a list of texts; a scalar gives a list with one item.
pub fn Metadata::texts(self : Metadata, key : String) -> Array[String] {
match self.fields.get(key) {
Some(value) => value.as_texts()
None => []
}
}
///|
pub fn Metadata::bool(self : Metadata, key : String) -> Bool? {
match self.fields.get(key) {
Some(value) => value.as_bool()
None => None
}
}
///|
pub fn Metadata::int(self : Metadata, key : String) -> Int32? {
match self.fields.get(key) {
Some(value) => value.as_int()
None => None
}
}
///|
/// A key read as a list of mappings; a mapping gives a list with one item.
pub fn Metadata::mappings(self : Metadata, key : String) -> Array[Fields] {
match self.fields.get(key) {
Some(value) => value.as_mappings()
None => []
}
}
///|
/// The raw source text after `key:` on its line, as written.
pub fn Metadata::raw_text(self : Metadata, key : String) -> String? {
for entry in self.entries {
if entry.key == key {
return Some(entry.inline)
}
}
None
}
///|
/// The file line of a top-level key, when the metadata has source text.
pub fn Metadata::line_of(self : Metadata, key : String) -> Int32? {
for entry in self.entries {
if entry.key == key {
return Some(self.first_line + entry.first_line)
}
}
None
}
///|
/// The located top-level keys.
pub fn Metadata::raw_entries(self : Metadata) -> ReadOnlyArray[RawEntry] {
self.entries
}
///|
/// A text field of a mapping.
pub fn Fields::text(self : Fields, key : String) -> String? {
match self.get(key) {
Some(Scalar(text)) if text.length() > 0 => Some(text)
_ => None
}
}
///|
pub fn Fields::texts(self : Fields, key : String) -> Array[String] {
match self.get(key) {
Some(value) => value.as_texts()
None => []
}
}
///|
pub fn Fields::bool(self : Fields, key : String) -> Bool? {
match self.get(key) {
Some(value) => value.as_bool()
None => None
}
}
///|
pub fn Fields::int(self : Fields, key : String) -> Int32? {
match self.get(key) {
Some(value) => value.as_int()
None => None
}
}
///|
pub extend Value with Eq::{not_equal, equal}
///|
pub extend Value with @debug.Debug::{to_repr}
///|
pub extend Fields with Eq::{not_equal, equal}
///|
pub extend Fields with @debug.Debug::{to_repr}
///|
pub extend Metadata with Eq::{not_equal, equal}
///|
pub extend Metadata with @debug.Debug::{to_repr}
///|
pub extend MetadataError with Eq::{not_equal, equal}
///|
pub extend MetadataError with @debug.Debug::{to_repr}