// N0c1 -- whole-run surgery.
//
// Turns "replace the text of `p[i]/r[j]`" into byte edits against the source
// part. The projection says which bytes produced which reader-visible text;
// this says which bytes to change so the reader produces something else.
//
// A request this slice cannot carry out safely is refused by raising, with a
// message naming the reason: an unaddressable run, text drawn from CDATA, an
// element the projection could not classify, or replacement text XML cannot
// represent. Each is a case where the edit's byte footprint is not knowable
// from the projection alone, and approximating it would corrupt a document.
//
// Everything here works on physical byte ranges taken from the projection's
// source elements. Nothing reconstructs XML: the first `w:t`'s *content* range
// is rewritten in place, which leaves its tag, namespace prefix and any
// existing `xml:space` untouched.
///|
/// One byte-range replacement in the source part.
#warnings("-unused_field")
priv struct RunSurgeryEdit {
byte_start : Int
byte_end : Int
replacement : String
}
///|
/// The XML characters that cannot appear literally in element content.
///
/// `>` does not strictly require escaping outside `]]>`, but escaping it
/// unconditionally is what every writer here already does and keeps the output
/// stable under later re-reads.
fn run_surgery_escape(text : String) -> String raise DocxError {
let out = StringBuilder()
for char in text {
let code = char.to_int()
// XML 1.0 forbids most C0 controls outright. Tab, newline and carriage
// return are the exceptions, and they survive as themselves.
if code < 0x20 && code != 0x09 && code != 0x0A && code != 0x0D {
raise Unsupported(
message="replacement text contains a control character U+\{code} that XML cannot represent",
)
}
// Surrogate code points are not characters. A lone one cannot be encoded
// as UTF-8 and would produce a part no parser accepts.
if code >= 0xD800 && code <= 0xDFFF {
raise Unsupported(
message="replacement text contains an unpaired surrogate U+\{code}",
)
}
// XML 1.0 excludes `U+FFFE` and `U+FFFF` from Char. The supplementary
// planes' equivalents are discouraged but permitted, and refusing them
// would reject text a conforming parser accepts.
if code == 0xFFFE || code == 0xFFFF {
raise Unsupported(
message="replacement text contains the non-character U+\{code}",
)
}
match char {
'&' => out.write_string("&")
'<' => out.write_string("<")
'>' => out.write_string(">")
// A literal carriage return does not survive: XML end-of-line handling
// turns CR and CRLF into LF when the part is read back. Writing it as a
// character reference is what keeps the text the caller asked for.
'\r' => out.write_string("
")
_ => out.write_char(char)
}
}
out.to_string()
}
///|
/// Whether replacing a `w:t`'s content with this text needs
/// `xml:space="preserve"` to survive a round trip.
///
/// Word strips leading and trailing whitespace from a `w:t` without it, so
/// text that starts or ends with a space would come back different.
fn run_surgery_needs_space_preserve(text : String) -> Bool {
if text.length() == 0 {
return false
}
fn is_space(unit : Int) -> Bool {
unit == 0x20 || unit == 0x09 || unit == 0x0A || unit == 0x0D
}
let first = text.view(start_offset=0, end_offset=1).to_owned()
let last = text.view(start_offset=text.length() - 1).to_owned()
fn leading(one : String) -> Int {
let mut code = 0
for char in one {
code = char.to_int()
}
code
}
is_space(leading(first)) || is_space(leading(last))
}
///|
/// True when this element is written in the self-closing form.
///
/// The scanner puts an empty content range at the element's end for ``,
/// where a paired `` would have its content range start after the
/// open tag. That difference is what tells a run with no children from a run
/// that was written without a body.
fn run_surgery_is_self_closing(element : ScannedElement) -> Bool {
element.content_start == element.byte_end
}
///|
/// Plan the byte edits that replace the whole projecting content of one run.
///
/// The run is named by its position in the reader's story -- `paragraph_index`
/// among the projection's paragraphs, `run_index` among that paragraph's runs
/// -- which is the address the projection assigns and the only one stable
/// across the transforms the reader applies.
///
/// What the reader ends up seeing is `text` and nothing else from this run:
/// the first `w:t` carries it, every later `w:t` is emptied, and every other
/// projecting atom the run contributed is removed. Transparent seams are left
/// alone; they produce no text and removing them would change the run's
/// structure for no reason.
#warnings("-unused_value")
fn plan_whole_run_replacement(
projection : ReaderProjection,
source : BytesView,
paragraph_index : Int,
run_index : Int,
text : String,
// Every refusal below still runs; the EDIT RECORDS and the synthesised
// run body are what is withheld. A caller replacing a run's text with
// itself needs the run's writability established and the plan
// discarded, and the refusal rules stay in one copy because this is
// the same walk, not a second one.
//
// Escaping is NOT withheld, and must not be: it is where a control
// character, an unpaired surrogate and a non-character are refused, so
// deferring it would make the mode lose validation, which is the one
// thing it may never do.
validate_only? : Bool = false,
) -> Array[RunSurgeryEdit] raise DocxError {
guard paragraph_index >= 0 && paragraph_index < projection.paragraphs.length() else {
raise Unsupported(
message="no paragraph \{paragraph_index} in this projection",
)
}
let paragraph = projection.paragraphs[paragraph_index]
// A logical paragraph joined from several physical `w:p` nodes cannot
// be mutated: the locked rule in the roadmap says so, and the reason
// is that the address names ONE paragraph while the bytes belong to
// several, so a plan is complete for none of them. N0c2 has always
// refused this; N0c1 accepted it, and N0c1 is the one the public
// surface reaches.
guard paragraph.sources.length() == 1 else {
raise Unsupported(
message="paragraph \{paragraph_index} joins \{paragraph.sources.length()} physical paragraphs, which cannot be edited as one",
)
}
// The address is STRUCTURAL: the run record exists whether or not the
// run contributed anything, so an atomless `` is addressable.
guard run_index >= 0 && run_index < paragraph.runs.length() else {
raise Unsupported(
message="no run \{run_index} in paragraph \{paragraph_index}",
)
}
let elements = projection.scan.elements()
let target_run = paragraph.runs[run_index]
let target = target_run.source
let SourceElementId(target_identity) = target
let run_element = elements[target_identity]
// The run's OWN region guards the empty-run hole: a run with no
// contributions still sits somewhere, and synthesizing text into a
// field's instruction region would change what the field computes.
match target_run.field_region {
FieldInstruction =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} carries field instruction text",
)
MalformedField =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} sits in a malformed field",
)
_ => ()
}
// A refusal is not a region: truncation is discovered at story END,
// after regions are assigned, so a truncated field's result run keeps
// FieldResult and carries only the refusal. A field the classifier
// refused has unreliable boundaries; nothing in it is writable.
match target_run.field_refusal {
Some(_) =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} sits in a field the classifier refused",
)
None => ()
}
// The cached-result region is checked AFTER the classifier's refusal,
// not inside the region match above. A refused field's boundaries are
// unreliable, so NOTHING in it is writable for a stronger reason than
// "the next recalculation discards this" -- and a truncated field's
// result run carries FieldResult and the refusal together, so region
// order alone would report the weaker reason.
//
// The run slot carries this separately from the contributions because
// an atomless run inside a field result is addressable, and
// synthesising into one writes text the recalculation discards.
if target_run.field_region is FieldResult {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} holds a field's cached result, which the next recalculation discards",
)
}
// Every element that will be consumed by its WHOLE span passes
// through here, before any branch decides whether it becomes a
// removal, a synthesis anchor, or both. Guarding the branches instead
// is how two review rounds missed a shape: the atom-only run takes its
// anchor OUT of the removals list, so a drain-time check never saw it.
fn require_consumable(element : ScannedElement) -> Unit raise DocxError {
if run_surgery_consumed_span_has_children(elements, element) {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} contains a \{element.local_name} carrying nested markup the reader does not model",
)
}
}
let owned : Array[ReaderProjectionContribution] = []
for contribution in paragraph.contributions {
if contribution.run_source == Some(target) {
owned.push(contribution)
}
}
// A content control that declares a checkbox rewrites whatever its content
// projects, so no run inside one can be edited by writing bytes.
//
// Inferring this from a blanked contribution was not enough. The transform
// replaces the *first* text in the whole control, so editing an earlier run
// that had none moves the replacement onto the new text and lets the text it
// had been hiding through. Measured on a control holding a tab-only run and
// then `secret`: asked to set the first run to "MINE", the document went from
// showing a tab to showing "secret". The caller's text vanished and hidden
// text appeared, which is worse than either alone.
//
// So the whole construct is refused, by ancestry rather than by symptom.
if run_surgery_inside_checkbox_control(elements, run_element) {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} sits inside a checkbox content control, whose content the reader rewrites",
)
}
// Restricted ancestry, checked AFTER the checkbox case so the more
// specific refusal keeps precedence over the general one. The run slot
// carries this on its own because an atomless run is addressable and
// synthesising into a restricted region is the same mistake as writing
// over one.
if run_surgery_restricted_ancestor(elements, run_element) is Some(construct) {
let named = run_surgery_restricted_name(construct)
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} sits in \{named}, which the reader may match but must not rewrite",
)
}
// A physical run that also contributes to another logical paragraph cannot
// be edited by naming one of them: the same bytes carry both, so the plan
// would be incomplete for one and a surprise for the other. No generated
// document produces this and the projection is asserted not to, but the
// check is cheap and the failure it prevents is silent.
for index, other in projection.paragraphs {
if index != paragraph_index {
for candidate in other.runs {
if candidate.source == target {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} also contributes to paragraph \{index}",
)
}
}
}
}
// A hyperlink NESTED INSIDE the run. Whole-run surgery replaces one
// run's text, which looked like it could never span a link's edge --
// but `abcd`
// is well-formed, this reader projects it as one run reading `abcd`,
// and replacing that run would empty the link rather than refuse.
//
// The rule is N0c2's: compare the nearest enclosing link IDENTITY
// across the run's own projecting contributions and refuse on any
// difference, `None` against `Some` included. A run wholly inside one
// link still edits, which is the common and legitimate shape.
let mut link_anchor : Int?? = None
for contribution in owned {
if contribution.kind is ProjectedText(_) {
let SourceElementId(source_identity) = contribution.source
let link = run_surgery_hyperlink_ancestor(
elements,
elements[source_identity],
)
match link_anchor {
None => link_anchor = Some(link)
Some(first) =>
if first != link {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} spans a hyperlink boundary, so replacing it would change what the link covers",
)
}
}
}
}
let escaped = run_surgery_escape(text)
let edits : Array[RunSurgeryEdit] = []
// The one place the mode is observed. It takes a THUNK, not an edit:
// MoonBit is strict, so passing the edit itself would build the record
// and the synthesised run body before the flag was ever consulted.
//
// Refusals are raised by the walk itself, so withholding an edit
// cannot withhold a refusal.
let emit = build => if !validate_only { edits.push(build()) }
let texts : Array[ScannedElement] = []
let removals : Array[ScannedElement] = []
// The same physical bytes must not be planned twice: two contributions
// sharing a source would compose two edits over one span.
let planned_sources : Array[Int] = []
for contribution in owned {
let SourceElementId(source_identity) = contribution.source
let element = elements[source_identity]
// A field's instruction text is machinery, not content: rewriting it
// changes what the field computes rather than what it shows. A malformed
// field has no reliable boundaries at all. Neither is safe to write
// through, and the projection already classified both.
match contribution.field_region {
FieldInstruction =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} carries field instruction text",
)
MalformedField =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} sits in a malformed field",
)
// A field's CACHED RESULT reads like ordinary content and is not:
// the next recalculation recomputes it and discards whatever was
// written here. Nothing in this repository can detect that loss --
// the reader projects the cached result, so a caller's read-back
// confirms an edit that will not survive.
FieldResult =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} holds a field's cached result, which the next recalculation discards",
)
_ => ()
}
// A restricted container may enclose one contribution of an
// otherwise ordinary run, so the run slot's own check above is not
// sufficient.
if run_surgery_restricted_ancestor(elements, element) is Some(construct) {
let named = run_surgery_restricted_name(construct)
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} draws text from \{named}, which the reader may match but must not rewrite",
)
}
match contribution.kind {
// Visible non-text -- a break, a reference mark, an image, a
// synthesized checkbox glyph -- is reader-visible content this plan
// has no representation for, so it cannot say what removing it
// would cost.
VisibleNonText =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} holds visible non-text content",
)
// Boundaries produce no text: the run's own opening marker and the
// wrappers the reader flattens. They stay -- unless the boundary is
// an empty drawing or embedded object, which the inventory calls a
// hard barrier and this plan has no more representation for than it
// does for a visible atom.
Transparent =>
if run_surgery_is_hard_barrier_container(element) {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} holds visible non-text content",
)
}
// Suppressed content projects no TEXT, which is not the same as
// being no CONTENT. The run-level suppressed set is dominated by
// things a reader of the document can still see or that this
// reader could not model: `w:pict`, a `w:sym` whose character it
// could not map, `w:footnoteRef`/`w:endnoteRef`, `w:txbxContent`,
// `w:del`, and -- widest of all -- any element it did not
// recognise, which it suppresses with a warning.
//
// Replacing the run's text while leaving those bytes puts the
// caller's text next to content they never saw. That is the same
// failure the checkbox-control refusal above documents: the
// caller's text appears AND hidden content survives, which is
// worse than either alone. `VisibleNonText` is already refused for
// exactly this reason; suppression is not a weaker case, only a
// less legible one.
SuppressedContent =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} holds suppressed content the reader could not model",
)
ProjectedText(FromText) => {
if planned_sources.contains(source_identity) {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} projects the same physical element twice",
)
}
planned_sources.push(source_identity)
if is_wml_uri(element.uri) && element.local_name == "t" {
match element.text_map {
Some(map) =>
if map.contains_cdata {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} draws text from a CDATA section",
)
}
None => ()
}
texts.push(element)
} else {
require_consumable(element)
removals.push(element)
}
}
// Tabs, symbols and the two hyphens all project text of their own,
// so replacing the run's text means removing their atoms.
ProjectedText(FromTab | FromNoBreakHyphen | FromSoftHyphen | FromSymbol) => {
if planned_sources.contains(source_identity) {
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} projects the same physical element twice",
)
}
planned_sources.push(source_identity)
require_consumable(element)
removals.push(element)
}
}
}
match texts {
// The common case: the first `w:t` takes the new text in place, so its tag,
// prefix and any existing `xml:space` are untouched.
[first, .. rest] => {
let wants_space = run_surgery_needs_space_preserve(text)
let existing = run_surgery_space_declaration(source, first)
// An existing declaration is replaced rather than joined: it may say
// `default`, which is the opposite of what edge whitespace needs.
match existing {
Some((from, to)) =>
if wants_space {
match
run_surgery_space_declaration_is_preserve(source, (from, to)) {
Some(true) => ()
Some(false) =>
emit(() => {
byte_start: from,
byte_end: to,
replacement: "xml:space=\"preserve\"",
})
None =>
raise Unsupported(
message="run \{run_index} in paragraph \{paragraph_index} carries an xml:space declaration whose value cannot be decoded",
)
}
}
None => ()
}
let add_space = wants_space && existing is None
let attribute = if add_space { " xml:space=\"preserve\"" } else { "" }
let text_prefix = run_surgery_qualified_prefix(source, first)
if run_surgery_is_self_closing(first) {
// `` has no content range to write into -- the scanner puts it at
// the element's end -- so the self-closing marker becomes a body. Only
// the `/>` is rewritten, which keeps any attributes already there.
emit(() => {
byte_start: first.byte_end - 2,
byte_end: first.byte_end,
replacement: "\{attribute}>\{escaped}\{text_prefix}t>",
})
} else {
if add_space {
emit(() => {
byte_start: first.content_start - 1,
byte_end: first.content_start - 1,
replacement: attribute,
})
}
emit(() => {
byte_start: first.content_start,
byte_end: first.content_end,
replacement: escaped,
})
}
for later in rest {
// A self-closing `w:t` already contributes nothing.
if !run_surgery_is_self_closing(later) {
emit(() => {
byte_start: later.content_start,
byte_end: later.content_end,
replacement: "",
})
}
}
}
// No `w:t` at all: an atom-only run, an empty one, or ``. One has to
// be synthesised, and the run's own element says where it goes.
[] => {
let prefix = run_surgery_qualified_prefix(source, run_element)
// Where the text goes matters. Putting it at the run's content end walks
// it past every seam the run contains, so for
// `` the tab would be deleted and the
// replacement would land outside the bookmark -- reading the same and
// meaning something else. It goes where the first atom it replaces was.
// The first atom is *replaced* rather than removed-and-inserted-at.
// Two edits sharing a byte offset would have their order decided by a
// sort documented as unstable, and the splice requires the insertion
// first.
let (anchor, consumed) = match removals {
[first, ..] => (first.byte_start, first.byte_end)
[] => {
let at = run_surgery_first_content_position(elements, run_element)
(at, at)
}
}
emit(() => {
run_surgery_synthesise_text(
run_element,
prefix,
escaped,
text,
anchor~,
consumed~,
)
})
if removals.length() > 0 {
removals.remove(0) |> ignore
}
}
}
for element in removals {
emit(() => {
byte_start: element.byte_start,
byte_end: element.byte_end,
replacement: "",
})
}
if !validate_only {
edits.sort_by_key(edit => edit.byte_start)
}
edits
}
///|
/// Whether an existing declaration's value is already `preserve`.
///
/// The locked rule ensures `xml:space="preserve"` by ADDING it, or by
/// replacing a `default` value-span — an existing `preserve` is left
/// byte-identical. Rewriting it anyway would change quote style or
/// spacing for no reason and put bytes the caller never asked about
/// inside the declared footprint.
///
/// The comparison is against the NORMALIZED value, not the raw bytes.
/// XML resolves references inside an attribute value before the value
/// means anything, so `xml:space="preserve"` already says
/// `preserve` and must be left alone. `None` means the value could not
/// be decoded at all; the caller refuses rather than guessing, because
/// the alternative is rewriting bytes whose meaning is unknown.
///
/// Normalization also folds literal whitespace to spaces, which this
/// does not do. It cannot change the answer: `preserve` holds no
/// whitespace, so a value carrying any differs from it either way.
fn run_surgery_space_declaration_is_preserve(
source : BytesView,
span : (Int, Int),
) -> Bool? {
let (from, to) = span
let mut at = from
while at < to && source[at] != b'"' && source[at] != b'\'' {
at = at + 1
}
if at >= to {
return None
}
let quote = source[at]
let start = at + 1
let mut end = start
while end < to && source[end] != quote {
end = end + 1
}
if end >= to {
return None
}
let decoded = StringBuilder()
let mut cursor = start
while cursor < end {
if source[cursor] == b'&' {
// The reader's own decoder, which admits the five predefined
// entities and strict numeric references — the same set the
// scanner documents for attribute values. Nothing wider can
// arrive: the strict gate rejects any DOCTYPE, so a document
// declaring its own entities never reaches a surgery.
let decoded_reference = decode_entity_token(source, cursor, end) catch {
_ => return None
}
let (character, _, next) = decoded_reference
// The scan must make progress on every iteration. The decoder
// always returns a position past the `;`, but this walks
// attacker-controlled bytes, and a decoder that ever failed to
// advance would hang the surgery rather than refuse it.
if next <= cursor {
return None
}
decoded.write_char(character)
cursor = next
} else {
// Everything outside a reference is literal, so the raw byte run
// up to the next `&` decodes as UTF-8 exactly once.
let mut run_end = cursor
while run_end < end && source[run_end] != b'&' {
run_end = run_end + 1
}
let text = @utf8.decode(source[cursor:run_end]) catch { _ => return None }
decoded.write_string(text)
cursor = run_end
}
}
Some(decoded.to_string() == "preserve")
}
///|
/// Whether a Transparent contribution is really a HARD BARRIER wearing a
/// seam's clothes.
///
/// The reader routes `w:drawing` and `w:object` through the
/// transparent-boundary path, so an EMPTY one projects a single
/// Transparent contribution — indistinguishable, by kind alone, from a
/// bookmark or a run boundary. The five-class inventory calls drawings
/// and embedded objects hard barriers, and both surgeries already refuse
/// visible non-text; they simply could not recognise this shape.
fn run_surgery_is_hard_barrier_container(element : ScannedElement) -> Bool {
// WML `drawing` and `object`, and every VML container the reader
// dispatches through the same transparent path. `w:pict` is NOT here:
// the reader emits it as Suppressed, so it never reaches a Transparent
// branch, and the suppressed-content hole is recorded N0 debt rather
// than something this predicate can close.
if is_wml_uri(element.uri) {
return element.local_name == "drawing" || element.local_name == "object"
}
element.uri == "urn:schemas-microsoft-com:vml" &&
(
element.local_name == "shape" ||
element.local_name == "rect" ||
element.local_name == "roundrect" ||
element.local_name == "textbox" ||
element.local_name == "group"
)
}
///|
/// Where an element's open tag ends: just before its content, or before the
/// `/>` when it is self-closing.
fn run_surgery_open_tag_end(element : ScannedElement) -> Int {
if run_surgery_is_self_closing(element) {
element.byte_end - 2
} else {
element.content_start
}
}
///|
/// The byte range of an existing `xml:space` declaration in this open tag, if
/// there is one.
///
/// Detecting the attribute is not enough: `xml:space="default"` declares the
/// opposite of what edge whitespace needs, so a plan that only avoided
/// duplicates would leave the text to be stripped. The declaration is replaced
/// wholesale when it is there and appended when it is not.
fn run_surgery_space_declaration(
source : BytesView,
element : ScannedElement,
) -> (Int, Int)? {
let stop = run_surgery_open_tag_end(element)
if stop <= element.byte_start || stop > source.length() {
return None
}
let needle = b"xml:space"
let mut at = element.byte_start
// Quote state, so a match is only ever considered where an attribute
// NAME can appear. Another attribute's VALUE can carry the whole shape
// -- `data='x xml:space = "bogus"'` satisfies both the name-start and
// `=` checks below -- and rewriting there corrupts that attribute while
// leaving the element with no real declaration at all.
let mut quote = b'\x00'
while at + needle.length() <= stop {
if quote != b'\x00' {
if source[at] == quote {
quote = b'\x00'
}
at = at + 1
continue
}
if source[at] == b'"' || source[at] == b'\'' {
quote = source[at]
at = at + 1
continue
}
let mut matched = true
for offset in 0.. element.byte_start &&
(
before == b' ' ||
before == b'\t' ||
before == b'\n' ||
before == b'\r'
)
let mut after = at + needle.length()
while after < stop &&
(
source[after] == b' ' ||
source[after] == b'\t' ||
source[after] == b'\n' ||
source[after] == b'\r'
) {
after = after + 1
}
if !starts_name || after >= stop || source[after] != b'=' {
matched = false
}
}
if matched {
// run to the end of the quoted value
let mut cursor = at + needle.length()
while cursor < stop && source[cursor] != b'"' && source[cursor] != b'\'' {
cursor = cursor + 1
}
if cursor >= stop {
return None
}
let quote = source[cursor]
cursor = cursor + 1
while cursor < stop && source[cursor] != quote {
cursor = cursor + 1
}
if cursor >= stop {
return None
}
return Some((at, cursor + 1))
}
at = at + 1
}
None
}
///|
/// Whether an element the reader modelled as a LEAF actually has element
/// children.
///
/// Both surgeries consume some elements by their whole span: an atom
/// being replaced, a `w:t` being emptied, a removal. The reader reaches
/// `w:tab` and its siblings WITHOUT walking their children, so a
/// well-formed
/// `cd`
/// projects a single `\t` and nothing reports the insertion inside it.
/// An ancestor walk cannot see it either -- it is a DESCENDANT of the
/// consumed element, not an ancestor of it.
///
/// So the check is not "does this contain a restricted name". Every
/// enumeration of names I have written for this gate has turned out to
/// miss a shape. The reader modelled this element as having no element
/// children; if it has some, the model and the bytes disagree and the
/// surgery cannot say what removing them costs.
fn run_surgery_consumed_span_has_children(
elements : Array[ScannedElement],
element : ScannedElement,
) -> Bool {
element.first_child_index >= 0 &&
element.first_child_index < elements.length()
}
///|
/// The identity of the nearest enclosing `w:hyperlink`, if any.
///
/// A hyperlink is restricted only at its BOUNDARY. Text inside one is
/// ordinary editable text, and text outside one is too; what must refuse
/// is an edit that spans the edge, because the replacement lands at a
/// single carrier and would either drag unlinked text inside the link or
/// leave linked text outside it.
///
/// Comparing nearest-ancestor identities across an edit's contributions
/// decides that, and does so in both directions. Looking for a
/// zero-width hyperlink contribution instead would not: the reader emits
/// an opening boundary but no closing one, so entry crossings would be
/// caught and exit crossings missed.
fn run_surgery_hyperlink_ancestor(
elements : Array[ScannedElement],
element : ScannedElement,
) -> Int? {
let mut at = element.identity
while at >= 0 && at < elements.length() {
let ancestor = elements[at]
if is_wml_uri(ancestor.uri) && ancestor.local_name == "hyperlink" {
return Some(at)
}
at = ancestor.parent_index
}
None
}
///|
/// A projecting-restricted construct: text that MATCHES but must not be
/// MUTATED. Kept as a type rather than a message so the predicate
/// dispatches on XML names only, and the prose lives with the renderer.
priv enum RestrictedConstruct {
TrackedInsertion
ContentControl
TextBoxContent
FallbackContent
}
///|
/// The user-facing name of a restricted construct.
///
/// One renderer, so a construct is named the same way wherever it IS
/// named. The two surgeries do not report alike: N0c1 names the
/// construct in its message, while N0c2 reports its stable class,
/// `RestrictedRegion`, because that taxonomy is what its callers match
/// on.
fn run_surgery_restricted_name(construct : RestrictedConstruct) -> String {
// Each names the ELEMENT alongside the English, so a caller can go
// look at the markup rather than guess which construct was meant.
match construct {
TrackedInsertion => "a tracked insertion (w:ins)"
ContentControl => "a content control (w:sdt)"
TextBoxContent => "textbox content (w:txbxContent)"
FallbackContent => "fallback content (mc:Fallback)"
}
}
///|
/// Which restricted construct encloses this element, if any.
///
/// Each fails for its own reason, and the surgeries name the construct
/// rather than reporting a shared "restricted", so a caller learns what
/// they hit.
///
/// Ancestry is walked over the PHYSICAL scan, not the projection. The
/// reader splices `mc:Fallback` and drops `mc:Choice`, so by projection
/// time the distinction is gone; the scan still carries the real parent
/// chain. The walk is INCLUSIVE -- an owned contribution can be sourced
/// at the container itself, not only beneath it.
fn run_surgery_restricted_ancestor(
elements : Array[ScannedElement],
element : ScannedElement,
) -> RestrictedConstruct? {
let mut at = element.identity
while at >= 0 && at < elements.length() {
let ancestor = elements[at]
if is_wml_uri(ancestor.uri) {
match ancestor.local_name {
// Writing here does not lose the text; it REATTRIBUTES it, so
// the document records that the insertion's named author wrote
// words they never wrote.
"ins" => return Some(TrackedInsertion)
// A data-bound control is repopulated from the custom XML part
// when the document opens, discarding the edit silently -- and
// this reader cannot tell a bound one from a plain one, so the
// whole construct is refused.
"sdt" => return Some(ContentControl)
// The same text is commonly carried twice, once per
// AlternateContent branch, so editing the projected copy leaves
// the other saying something else.
"txbxContent" => return Some(TextBoxContent)
_ => ()
}
}
// The branch this reader projects is the one a consumer that
// UNDERSTANDS the Choice will ignore, so an edit here is invisible
// to modern Word and visible to older consumers.
if ancestor.uri == MC_NAMESPACE && ancestor.local_name == "Fallback" {
return Some(FallbackContent)
}
at = ancestor.parent_index
}
None
}
///|
/// Whether any ancestor of this element is a content control declaring a
/// checkbox.
fn run_surgery_inside_checkbox_control(
elements : Array[ScannedElement],
element : ScannedElement,
) -> Bool {
let mut at = element.parent_index
while at >= 0 && at < elements.length() {
let ancestor = elements[at]
if is_wml_uri(ancestor.uri) && ancestor.local_name == "sdt" {
let mut child = ancestor.first_child_index
while child >= 0 {
let properties = elements[child]
if is_wml_uri(properties.uri) && properties.local_name == "sdtPr" {
let mut declared = properties.first_child_index
while declared >= 0 {
let marker = elements[declared]
if marker.uri == W14_NAMESPACE && marker.local_name == "checkbox" {
return true
}
declared = marker.next_sibling_index
}
break
}
child = properties.next_sibling_index
}
}
at = ancestor.parent_index
}
false
}
///|
/// Where content may start inside a run that has no atoms to replace.
///
/// After `w:rPr` if there is one -- run properties have to come first -- and at
/// the start of the run's content otherwise.
fn run_surgery_first_content_position(
elements : Array[ScannedElement],
run : ScannedElement,
) -> Int {
let mut child = run.first_child_index
let mut position = run.content_start
while child >= 0 {
let element = elements[child]
if is_wml_uri(element.uri) && element.local_name == "rPr" {
position = element.byte_end
}
child = element.next_sibling_index
}
position
}
///|
/// The qualified name an element is written with, taken from the source.
///
/// Synthesising `` is only right when the document happens to bind `w` to
/// the WML namespace with that prefix. A part that binds it as `x`, or as the
/// default namespace, needs the prefix its own runs use -- and a hard-coded
/// closing `` against an `` opening would not even be well-formed.
fn run_surgery_qualified_prefix(
source : BytesView,
element : ScannedElement,
) -> String {
let stop = run_surgery_open_tag_end(element)
let mut at = element.byte_start + 1
let prefix : Array[Byte] = []
while at < stop && at < source.length() {
let byte = source[at]
if byte == b':' {
// Decoded, not walked byte by byte: XML permits non-ASCII prefixes, and
// reinterpreting each UTF-8 byte as a character would spell a different
// name. These bytes came from a part the scanner already accepted, so a
// decode failure here is not reachable; an undecodable prefix falls back
// to none, which the caller answers by binding the default namespace.
return try @utf8.decode(Bytes::from_array(prefix)) + ":" catch {
_ => ""
}
}
if byte == b' ' ||
byte == b'\t' ||
byte == b'\n' ||
byte == b'\r' ||
byte == b'/' ||
byte == b'>' {
break
}
prefix.push(byte)
at = at + 1
}
""
}
///|
/// Put a `w:t` into a run that has none.
///
/// A self-closing `` has to become a paired element to hold anything, so
/// its `/>` is rewritten. A paired run keeps its tags and gains the element at
/// the end of its content, after whatever properties it already declares --
/// `w:rPr` has to stay first.
fn run_surgery_synthesise_text(
run : ScannedElement,
prefix : String,
escaped : String,
raw : String,
anchor~ : Int,
consumed~ : Int,
) -> RunSurgeryEdit {
let attribute = if run_surgery_needs_space_preserve(raw) {
" xml:space=\"preserve\""
} else {
""
}
// The prefix comes from the run's own tag: a part that binds WML as `x`, or
// as the default namespace, needs `` or ``, and a hard-coded
// `` closing an `` would not even be well-formed.
//
// The prefix alone is not enough, though. Synthesis can land inside a
// wrapper that rebound it, in which case the element would carry the run's
// spelling and somebody else's namespace -- text the reader cannot see. The
// binding travels with the element so it means WML wherever it lands.
// The run's own namespace, not a constant. This project reads both the
// Transitional and Strict WML vocabularies, and binding the Transitional URI
// into a Strict document would produce a run whose text element belongs to a
// different vocabulary than the run around it.
let wml = run.uri
let binding = if prefix == "" {
" xmlns=\"\{wml}\""
} else {
let name = prefix.view(end_offset=prefix.length() - 1).to_owned()
" xmlns:\{name}=\"\{wml}\""
}
let synthesised = "<\{prefix}t\{binding}\{attribute}>\{escaped}\{prefix}t>"
if run_surgery_is_self_closing(run) {
// `` -> `...`, replacing the two bytes `/>`.
{
byte_start: run.byte_end - 2,
byte_end: run.byte_end,
replacement: ">\{synthesised}\{prefix}r>",
}
} else {
{ byte_start: anchor, byte_end: consumed, replacement: synthesised, }
}
}