// Field classification over the reader-native projection (#434 PR 6b):
// the ONE field state machine runs over exactly what it consumes -- each
// contribution's source identity and owning physical paragraph, the
// carrier event stream, and the scan itself -- through annotation slots,
// and the three field annotations are copied back per contribution. No
// facade: neither projection fabricates the other's contribution type.
///|
/// The physical paragraph owning a source, for carrier resolution: the
/// nearest `w:p` ancestor. Content outside every paragraph gets a sentinel
/// no parent index can equal, so its carrier resolves to `None` exactly as
/// the legacy walker's flow-level contributions do.
fn reader_projection_owning_paragraph(
elements : Array[ScannedElement],
source_identity : Int,
budget : ReaderFieldBudget,
) -> Int raise DocxError {
let mut current = elements[source_identity].parent_index
while current >= 0 {
budget.charge_work()
let element = elements[current]
if is_wml_uri(element.uri) && element.local_name == "p" {
return current
}
current = element.parent_index
}
-2
}
///|
/// Classifies complex fields over a reader-native projection: one slot
/// per contribution, the machine run once, annotations copied back and
/// the index stored. A separate post-adapter step, exactly as the legacy
/// pipeline separates the walk from classification.
fn classify_reader_projection_fields(
projection : ReaderProjection,
budget? : ReaderFieldBudget,
) -> Unit raise DocxError {
let budget = match budget {
Some(value) => value
None => reader_field_budget()
}
let elements = projection.scan.elements()
let slots : Array[ReaderFieldSlot] = []
let sources : Array[ReaderProjectionContribution] = []
// Run slots ride along: a run with no contributions still sits in a
// field region, and surgery consults the run record for exactly that
// case -- an empty run inside an instruction region must refuse.
let run_sources : Array[ReaderProjectionRun] = []
for paragraph in projection.paragraphs {
budget.charge_work()
for contribution in paragraph.contributions {
budget.charge_work()
let SourceElementId(source_identity) = contribution.source
slots.push({
source_identity,
paragraph_identity: reader_projection_owning_paragraph(
elements, source_identity, budget,
),
field_identity: None,
field_region: OutsideField,
field_refusal: None,
})
sources.push(contribution)
}
}
// run slots strictly AFTER every contribution slot: the copy-back
// below splits the one slot array at sources.length()
for paragraph in projection.paragraphs {
budget.charge_work()
for run in paragraph.runs {
budget.charge_work()
let SourceElementId(source_identity) = run.source
slots.push({
source_identity,
paragraph_identity: reader_projection_owning_paragraph(
elements, source_identity, budget,
),
field_identity: None,
field_region: OutsideField,
field_refusal: None,
})
run_sources.push(run)
}
}
let index = classify_reader_field_slots(
elements,
projection.field_carriers,
slots,
budget,
)
for at, slot in slots {
budget.charge_work()
if at < sources.length() {
let contribution = sources[at]
contribution.field_identity = slot.field_identity
contribution.field_region = slot.field_region
contribution.field_refusal = slot.field_refusal
} else {
let run = run_sources[at - sources.length()]
run.field_region = slot.field_region
run.field_refusal = slot.field_refusal
}
}
projection.field_index = index
}