///|
pub(all) enum ContentPage {
Source(String, Int32)
Empty
} derive(Eq, Debug)
///|
pub extend ContentPage with Eq::{equal, not_equal}
///|
pub extend ContentPage with Debug::{to_repr}
///|
pub(all) struct Section {
source : String
source_pages : Int32
selection : String
start_on_recto : Bool
}
///|
pub(all) struct SectionSpan {
source : String
first : Int32
count : Int32
}
///|
pub(all) struct Document {
pages : Array[ContentPage]
sections : Array[SectionSpan]
}
///|
/// Join selected source pages. Odd logical pages are rectos for either binding.
/// Alignment blanks belong to the document and remain distinguishable from padding.
pub fn assemble_document(
sections : Array[Section],
) -> Result[Document, WorkflowError] {
if sections.is_empty() || sections.length() > 1000 {
return Err(workflow_error("sections", "expected 1..1000 sections"))
}
let pages : Array[ContentPage] = []
let spans : Array[SectionSpan] = []
let source_lengths : Map[String, Int32] = Map([])
for index, section in sections {
let field = "sections[\{index}]"
if section.source.is_blank() || section.source.length() > 128 {
return Err(
workflow_error(
field + ".source",
"expected a nonblank identifier of at most 128 code units",
),
)
}
match source_lengths.get(section.source) {
Some(previous) if previous != section.source_pages =>
return Err(
workflow_error(
field + ".source_pages",
"conflicting lengths for the same source",
),
)
_ => source_lengths.set(section.source, section.source_pages)
}
let selected = match
parse_selection(section.selection, section.source_pages) {
Err(e) => return Err(workflow_error(field + "." + e.field, e.message))
Ok(pages) => pages
}
let align = section.start_on_recto && pages.length() % 2 == 1
let extra = if align { 1 } else { 0 }
if pages.length() + selected.length() + extra > 100000 {
return Err(
workflow_error(field, "assembled document exceeds 100000 logical pages"),
)
}
if align {
pages.push(Empty)
}
spans.push({
source: section.source,
first: pages.length() + 1,
count: selected.length(),
})
for page in selected {
pages.push(
match page {
Page(n) => Source(section.source, n)
Blank => Empty
},
)
}
}
Ok({ pages, sections: spans, })
}
///|
/// Validate externally constructed documents before using their page map.
pub fn validate_document(document : Document) -> Result[Unit, WorkflowError] {
if document.pages.is_empty() || document.pages.length() > 100000 {
return Err(workflow_error("document.pages", "expected 1..100000 pages"))
}
for index, page in document.pages {
match page {
Source(source, number) =>
if source.is_blank() ||
source.length() > 128 ||
number < 1 ||
number > 100000 {
return Err(
workflow_error(
"document.pages[\{index}]",
"invalid source reference",
),
)
}
Empty => ()
}
}
let mut previous_end = 0
if document.sections.length() > 1000 {
return Err(
workflow_error(
"document.sections", "at most 1000 section spans are allowed",
),
)
}
for index, span in document.sections {
if span.source.is_blank() || span.source.length() > 128 {
return Err(
workflow_error(
"document.sections[\{index}].source",
"invalid source identifier",
),
)
}
if span.first <= previous_end ||
span.count < 1 ||
span.count > document.pages.length() ||
span.first > document.pages.length() - span.count + 1 {
return Err(
workflow_error(
"document.sections[\{index}]",
"overlapping or out-of-range span",
),
)
}
for gap in previous_end..<(span.first - 1) {
if document.pages[gap] != Empty {
return Err(
workflow_error(
"document.sections[\{index}]",
"unclaimed content before section",
),
)
}
}
for position in (span.first - 1)..<(span.first - 1 + span.count) {
match document.pages[position] {
Source(source, _) if source != span.source =>
return Err(
workflow_error(
"document.sections[\{index}]",
"source does not match section provenance",
),
)
_ => ()
}
}
previous_end = span.first + span.count - 1
}
if !document.sections.is_empty() {
for position in previous_end..