///|
/// The keys of a lexicon file.
let lexicon_keys : Array[String] = [
"name", "modals", "determiners", "pronouns", "systems", "lead_ins", "headings",
]
///|
/// The file lines of a top-level key: the line of the key, and the line of
/// each item when its value is a list.
priv struct KeyLines {
key : Int32
items : Array[Int32]
}
///|
/// Collects the file lines of the top-level keys from the events of the YAML
/// parser, so that flow lists, quoted keys and block scalars give the lines
/// of their own items.
priv struct LineReader {
lines : Map[String, KeyLines]
/// How many collections are open: 1 inside the top-level mapping.
mut depth : Int32
/// The top-level key whose value is read.
mut key : String?
/// The value of the key is a list.
mut in_list : Bool
}
///|
impl @yaml.MarkedEventReceiver for LineReader with fn on_event(
self,
event,
mark,
) {
let item = () => {
if self.depth == 2 && self.in_list {
if self.key is Some(key) && self.lines.get(key) is Some(lines) {
lines.items.push(mark.line)
}
}
}
match event {
Scalar(value~, ..) if self.depth == 1 =>
match self.key {
None => {
self.lines.set(value, { key: mark.line, items: [], })
self.key = Some(value)
}
Some(_) => self.key = None
}
Alias(..) if self.depth == 1 => self.key = None
Scalar(..) | Alias(..) => item()
SequenceStart(..) | MappingStart(..) => {
item()
if self.depth == 1 {
self.in_list = event is SequenceStart(..)
}
self.depth += 1
}
SequenceEnd | MappingEnd => {
self.depth -= 1
if self.depth == 1 {
self.key = None
}
}
_ => ()
}
}
///|
/// The file lines of the top-level keys of `text`. A key that the parser
/// does not reach has no lines.
fn key_lines(text : String) -> Map[String, KeyLines] {
let reader = { lines: {}, depth: 0, key: None, in_list: false, }
@yaml.Parser::new(text).load(reader, false) catch {
_ => ()
}
reader.lines
}
///|
/// The file line of the top-level `key`, or 1.
fn key_line(lines : Map[String, KeyLines], key : String) -> Int32 {
match lines.get(key) {
Some(found) => found.key
None => 1
}
}
///|
/// The file line of each of the `count` items of the list of `key`. An item
/// whose line is not known has the line of the key.
fn item_lines(
lines : Map[String, KeyLines],
key : String,
count : Int32,
) -> Array[Int32] {
match lines.get(key) {
Some(found) if found.items.length() == count => found.items
_ => Array::make(count, key_line(lines, key))
}
}
///|
/// The text entries of the list of `key`, each with its line.
fn text_entries(
lines : Map[String, KeyLines],
key : String,
value : @yaml.Yaml,
) -> Array[(String, Int32)] raise LexiconError {
guard value is Array(items) else {
raise WrongType(key~, line=key_line(lines, key), expected=List)
}
let at_lines = item_lines(lines, key, items.length())
let out = []
for i, item in items {
let at = at_lines[i]
guard item is String(s) else {
raise WrongType(key~, line=at, expected=Text)
}
out.push((s, at))
}
out
}
///|
fn words_of(
lines : Map[String, KeyLines],
key : String,
value : @yaml.Yaml,
) -> Array[Word] raise LexiconError {
text_entries(lines, key, value).map(entry => {
Word(entry.0) catch {
e => raise InvalidEntry(key~, line=entry.1, error=e)
}
})
}
///|
fn phrases_of(
lines : Map[String, KeyLines],
key : String,
value : @yaml.Yaml,
) -> Array[Phrase] raise LexiconError {
text_entries(lines, key, value).map(entry => {
Phrase(entry.0) catch {
e => raise InvalidEntry(key~, line=entry.1, error=e)
}
})
}
///|
fn headings_of(
lines : Map[String, KeyLines],
value : @yaml.Yaml,
) -> Array[HeadingRule] raise LexiconError {
let key = "headings"
guard value is Array(items) else {
raise WrongType(key~, line=key_line(lines, key), expected=List)
}
let at_lines = item_lines(lines, key, items.length())
let out = []
for i, item in items {
let at = at_lines[i]
guard item is Map(fields) else {
raise WrongType(key~, line=at, expected=Mapping)
}
for name, _ in fields {
if name != "level" && name != "prefix" {
raise UnknownKey(key="headings." + name, line=at)
}
}
let level = match fields.get("level") {
Some(Integer(n)) =>
match
(if n >= 1L && n <= 6L {
@mdc.HeadingLevel::of_int(n.to_int())
} else {
None
}) {
Some(level) => level
None =>
raise InvalidEntry(
key~,
line=at,
error=InvalidHeadingLevel(
if n > 2147483647L {
2147483647
} else if n < -2147483648L {
-2147483648
} else {
n.to_int()
},
),
)
}
_ => raise WrongType(key="headings.level", line=at, expected=Number)
}
let prefix = match fields.get("prefix") {
Some(String(s)) =>
HeadingPrefix(s) catch {
e => raise InvalidEntry(key~, line=at, error=e)
}
_ => raise WrongType(key="headings.prefix", line=at, expected=Text)
}
out.push(HeadingRule(level, prefix))
}
out
}
///|
/// Parses a lexicon file: a YAML mapping with the keys `name`, `modals`,
/// `determiners`, `pronouns`, `systems`, `lead_ins` and `headings`. Each key
/// is optional. A list key holds a list of texts; `headings` holds a list of
/// mappings with `level` (1 to 6) and `prefix`. An unknown key, a value of
/// the wrong kind and an invalid entry raise `LexiconError` with the line.
/// It reads no file: the caller gives the text.
pub fn Lexicon::from_yaml(text : String) -> Lexicon raise LexiconError {
let frontmatter = @mdc.Frontmatter::Frontmatter(text) catch {
InvalidYaml(at~, message~) =>
raise InvalidLexiconFile(line=at.line(), message~)
NotAMapping =>
raise InvalidLexiconFile(
line=1,
message="a lexicon file is a YAML mapping",
)
MultipleDocuments(..) =>
raise InvalidLexiconFile(
line=1,
message="a lexicon file has one YAML document",
)
}
guard frontmatter.value() is Map(fields) else {
raise InvalidLexiconFile(line=1, message="a lexicon file is a YAML mapping")
}
let lines = key_lines(text)
for key, _ in fields {
if !lexicon_keys.contains(key) {
raise UnknownKey(key~, line=key_line(lines, key))
}
}
let name = match fields.get("name") {
None => "lexicon"
Some(String(s)) => s
Some(_) =>
raise WrongType(key="name", line=key_line(lines, "name"), expected=Text)
}
let words = (key : String) => {
match fields.get(key) {
Some(value) => words_of(lines, key, value)
None => []
}
}
let phrases = (key : String) => {
match fields.get(key) {
Some(value) => phrases_of(lines, key, value)
None => []
}
}
Lexicon(
name~,
modals=words("modals"),
determiners=words("determiners"),
pronouns=words("pronouns"),
systems=phrases("systems"),
lead_ins=phrases("lead_ins"),
headings=match fields.get("headings") {
Some(value) => headings_of(lines, value)
None => []
},
)
}