///|
/// Where a requirement or a diagnostic came from. A text range covers the
/// sentence or the line without surrounding white space; a Markdown range
/// covers the paragraph, including markup. Markdown paths resolve against the
/// retained MDC document. Ranges are absent if that document was parsed with
/// `locs=false`.
pub enum Origin {
Text(@mdc.SourceRange)
Markdown(path~ : @mdc.NodePath, span~ : @mdc.SourceRange?)
} derive(Eq, Debug)
///|
struct Occurrence {
requirement : Requirement
origin : Origin
} derive(Eq, Debug)
///|
pub fn Occurrence::requirement(self : Occurrence) -> Requirement {
self.requirement
}
///|
pub fn Occurrence::origin(self : Occurrence) -> Origin {
self.origin
}
///|
struct Diagnostic {
text : String
error : ParseError
origin : Origin
} derive(Eq, Debug)
///|
pub fn Diagnostic::text(self : Diagnostic) -> String {
self.text
}
///|
pub fn Diagnostic::error(self : Diagnostic) -> ParseError {
self.error
}
///|
pub fn Diagnostic::origin(self : Diagnostic) -> Origin {
self.origin
}
///|
/// A requirement block: the requirements and diagnostics under one Markdown
/// requirement heading. The name is the rest of the heading text, free text.
struct RequirementBlock {
name : String
heading : @mdc.SourceRange?
requirements : ReadOnlyArray[Occurrence]
diagnostics : ReadOnlyArray[Diagnostic]
} derive(Debug)
///|
pub fn RequirementBlock::name(self : RequirementBlock) -> String {
self.name
}
///|
/// The source range of the heading, when the document has locations.
pub fn RequirementBlock::heading(self : RequirementBlock) -> @mdc.SourceRange? {
self.heading
}
///|
pub fn RequirementBlock::requirements(
self : RequirementBlock,
) -> ReadOnlyArray[Occurrence] {
self.requirements
}
///|
pub fn RequirementBlock::diagnostics(
self : RequirementBlock,
) -> ReadOnlyArray[Diagnostic] {
self.diagnostics
}
///|
/// An immutable extraction result. Syntax errors do not discard valid siblings.
/// The Markdown constructor retains its MDC document and frontmatter. With
/// Markdown requirement headings, `blocks()` gives the blocks, and
/// `requirements()` and `diagnostics()` list the contents of every block.
struct Specification {
requirements : ReadOnlyArray[Occurrence]
diagnostics : ReadOnlyArray[Diagnostic]
blocks : ReadOnlyArray[RequirementBlock]
document : @mdc.Document?
} derive(Debug)
///|
pub fn Specification::requirements(
self : Specification,
) -> ReadOnlyArray[Occurrence] {
self.requirements
}
///|
pub fn Specification::diagnostics(
self : Specification,
) -> ReadOnlyArray[Diagnostic] {
self.diagnostics
}
///|
pub fn Specification::document(self : Specification) -> @mdc.Document? {
self.document
}
///|
/// The requirement blocks, in source order. Without heading rules, none.
pub fn Specification::blocks(
self : Specification,
) -> ReadOnlyArray[RequirementBlock] {
self.blocks
}
///|
fn collect_candidate(
text : String,
origin : Origin,
requirements : Array[Occurrence],
diagnostics : Array[Diagnostic],
vocabulary : Vocabulary,
) -> Unit {
let requirement = parse_sentence(text, vocabulary).requirement catch {
error => {
diagnostics.push({ text, error, origin, })
return
}
}
requirements.push({ requirement, origin, })
}
///|
/// Parse one requirement per nonblank line, with the vocabulary of
/// `dialects`. Every nonblank line is checked. Accept LF, CRLF, CR and a
/// leading BOM. Spans exclude indentation/newlines.
pub fn parse_text(
source : String,
dialects~ : ArrayView[&Dialect] = [],
) -> Specification {
let vocabulary = Vocabulary::of(dialects)
let requirements = []
let diagnostics = []
let mut offset = if source.has_prefix("\u{FEFF}") { 1 } else { 0 }
let mut line = 1
let mut line_start = 0
while offset < source.length() {
let mut end = offset
while end < source.length() &&
source.code_unit_at(end) != 10 &&
source.code_unit_at(end) != 13 {
end += 1
}
let mut first = offset
let mut last = end
while first < last && is_space(source.code_unit_at(first)) {
first += 1
}
while last > first && is_space(source.code_unit_at(last - 1)) {
last -= 1
}
if first < last {
collect_candidate(
source[first:last].to_owned(),
Text(range_from(source, first, last, line~, line_start~)),
requirements,
diagnostics,
vocabulary,
)
}
offset = end
if offset < source.length() {
offset += 1
if source.code_unit_at(end) == 13 &&
offset < source.length() &&
source.code_unit_at(offset) == 10 {
offset += 1
}
}
line += 1
line_start = offset
}
{
requirements: ReadOnlyArray::from_array(requirements[:]),
diagnostics: ReadOnlyArray::from_array(diagnostics[:]),
blocks: [],
document: None,
}
}
///|
/// The ranges of the sentences of `text`, without surrounding white space. A
/// sentence ends at a full stop, a question mark or an exclamation mark that
/// white space or the end follows, outside backticks; closing brackets and
/// quote marks after the mark belong to the sentence. A blank line also ends
/// a sentence. A backtick with no closing backtick on its line is text.
fn sentence_ranges(text : String) -> Array[(Int32, Int32)] {
let raw = []
let n = text.length()
let mut start = 0
// The length of the backtick run that opened the code span, or 0 outside
// code.
let mut code_run = 0
let closing = (c : UInt16) => {
c == 41 || c == 93 || c == 34 || c == 39 || c == 0x201D || c == 0x2019
}
let mut i = 0
while i < n {
let c = text.code_unit_at(i)
if c == 10 || c == 13 {
code_run = 0
// A blank line: a line break, optional white space, a line break.
let mut j = i + 1
if c == 13 && j < n && text.code_unit_at(j) == 10 {
j += 1
}
let mut k = j
while k < n && (text.code_unit_at(k) == 32 || text.code_unit_at(k) == 9) {
k += 1
}
if k < n && (text.code_unit_at(k) == 10 || text.code_unit_at(k) == 13) {
raw.push((start, i))
start = k
i = k
continue
}
} else if c == 96 {
// A run of backticks opens a code span only when a run of the same
// length closes it on the same line, as in Markdown; an unclosed run is
// text. Inside code, only a run of the opening length closes it.
let mut run_end = i
while run_end < n && text.code_unit_at(run_end) == 96 {
run_end += 1
}
let run = run_end - i
if code_run > 0 {
if run == code_run {
code_run = 0
}
} else {
let mut j = run_end
let mut closes = false
while j < n &&
!closes &&
text.code_unit_at(j) != 10 &&
text.code_unit_at(j) != 13 {
if text.code_unit_at(j) == 96 {
let mut k = j
while k < n && text.code_unit_at(k) == 96 {
k += 1
}
closes = k - j == run
j = k
} else {
j += 1
}
}
if closes {
code_run = run
}
}
i = run_end
continue
} else if code_run == 0 && (c == 46 || c == 63 || c == 33) {
let mut end = i + 1
while end < n && closing(text.code_unit_at(end)) {
end += 1
}
if end == n || is_space(text.code_unit_at(end)) {
raw.push((start, end))
start = end
i = end
continue
}
}
i += 1
}
if start < n {
raw.push((start, n))
}
let out = []
for range in raw {
let mut first = range.0
let mut last = range.1
while first < last && is_space(text.code_unit_at(first)) {
first += 1
}
while last > first && is_space(text.code_unit_at(last - 1)) {
last -= 1
}
if first < last {
out.push((first, last))
}
}
out
}
///|
/// The sentences of a block so far: their requirements and diagnostics, and
/// the last system that a pronoun can refer to.
priv struct Sentences {
requirements : Array[Occurrence]
diagnostics : Array[Diagnostic]
mut last_system : String?
}
///|
fn Sentences::new() -> Sentences {
{ requirements: [], diagnostics: [], last_system: None, }
}
///|
/// Parses each sentence of `text`. A pronoun subject refers to the system of
/// the nearest earlier sentence that has one; without one, the requirement
/// stays, with an `UnresolvedPronoun` diagnostic.
fn Sentences::add(
self : Sentences,
text : String,
vocabulary : Vocabulary,
origin_of : (Int32, Int32) -> Origin,
) -> Unit {
for range in sentence_ranges(text) {
let sentence = text[range.0:range.1].to_owned()
let origin = origin_of(range.0, range.1)
let parsed = parse_sentence(sentence, vocabulary) catch {
error => {
self.diagnostics.push({ text: sentence, error, origin, })
continue
}
}
let requirement = match parsed.requirement.subject {
Pronoun(opener~, resolved=None) =>
match self.last_system {
Some(system) =>
{
..parsed.requirement,
subject: Pronoun(opener~, resolved=Some(system)),
}
None => {
self.diagnostics.push({
text: sentence,
error: UnresolvedPronoun(pronoun=opener, at=parsed.subject_at),
origin,
})
parsed.requirement
}
}
_ => parsed.requirement
}
match requirement.system() {
Some(system) => self.last_system = Some(system)
None => ()
}
self.requirements.push({ requirement, origin, })
}
}
///|
/// The source range of `start.. @mdc.SourceRange {
let mut end_line = line
let mut end_line_start = line_start
let last = end - 1
for i in start.. abort("a text range is not a source range")
}
}
///|
/// Whether the code unit at `i` ends a line: a line feed, or a carriage return
/// that no line feed follows.
fn is_line_break(text : String, i : Int32) -> Bool {
let c = text.code_unit_at(i)
c == 10 ||
(c == 13 && !(i + 1 < text.length() && text.code_unit_at(i + 1) == 10))
}
///|
/// The source range of `start.. @mdc.SourceRange {
let mut line = 1
let mut line_start = 0
for i in 0.. Specification {
let vocabulary = Vocabulary::of(dialects)
let sentences = Sentences::new()
sentences.add(text, vocabulary, (start, end) => {
Text(range_in(text, start, end))
})
{
requirements: ReadOnlyArray::from_array(sentences.requirements),
diagnostics: ReadOnlyArray::from_array(sentences.diagnostics),
blocks: [],
document: None,
}
}
///|
pub extend Origin with Eq::{equal, not_equal}
///|
pub extend Origin with @debug.Debug::{to_repr}
///|
pub extend Occurrence with Eq::{equal, not_equal}
///|
pub extend Occurrence with @debug.Debug::{to_repr}
///|
pub extend Diagnostic with Eq::{equal, not_equal}
///|
pub extend Diagnostic with @debug.Debug::{to_repr}
///|
pub extend Specification with @debug.Debug::{to_repr}
///|
pub extend RequirementBlock with @debug.Debug::{to_repr}