// `office insert-paragraph` — the N3a structural verb. The CLI half
// owns REQUEST policy: the anchor flags, the `docx.paragraph/1`
// payload parse, and the output contract. Minting, planning, and the
// identity readback live in the engine and the transaction.
///|
fn insert_paragraph_command() -> @argparse.Command {
let summary = match @lib.find_capability_command("insert-paragraph") {
Some(command) => command.summary
None =>
"Insert one resource-free paragraph beside a direct body paragraph, minting its stable identity"
}
Command(
"insert-paragraph",
about=summary,
positionals=[
PositionArg(
"file",
about="existing .docx package (never modified in place)",
num_args=@argparse.ValueRange::single(),
),
PositionArg(
"out",
about="destination .docx; created atomically, nothing written on refusal",
num_args=@argparse.ValueRange::single(),
),
],
options=[
OptionArg(
"before",
long="before",
about="insert before this DIRECT body paragraph (p[3]); exclusive with --after",
),
OptionArg(
"after",
long="after",
about="insert after this DIRECT body paragraph (p[3]); exclusive with --before",
),
OptionArg(
"content",
long="content",
about="docx.paragraph/1 JSON: {\"style\"?: \"StyleId\", \"runs\": [{\"text\": \"…\", \"bold\"?, \"italic\"?, \"underline\"?}]} — resource-free only; style ids must exist in the target",
),
],
flags=[
FlagArg(
"dry-run",
long="dry-run",
about="run the identical mint/plan/validate pipeline, exit as the real run would, write nothing",
),
FlagArg(
"overwrite",
long="overwrite",
about="replace an existing destination",
),
docx_json_flag(),
],
)
}
///|
/// Parse the dedicated `docx.paragraph/1` payload. Strict: unknown
/// keys, wrong types, and non-boolean flags refuse naming the field —
/// a typo must fail loudly, not silently drop formatting.
fn parse_insert_paragraph_content(
payload : String,
) -> @docx.DocxInsertContent raise CliFailure {
fn refuse(reason : String) -> CliFailure {
docx_cli_failure(
"office.invalid_arguments",
"--content is a docx.paragraph/1 payload: " + reason,
)
}
// Duplicate members collapse last-wins in a parsed object, so the
// RAW text is scanned first: two spellings of the same key in the
// same object refuse before any value is trusted.
match insert_payload_duplicate_key(payload) {
Some(key) => raise refuse("duplicate member \"" + key + "\"")
None => ()
}
let parsed : Json = @json.parse(payload) catch {
_ => raise refuse("the value is not valid JSON")
}
guard parsed is Object(fields) else {
raise refuse("the payload is a JSON object")
}
let mut style : String? = None
let runs : Array[@docx.DocxInsertRun] = []
for key, value in fields {
match key {
"style" =>
match value {
String(name) => style = Some(name)
_ => raise refuse("\"style\" is a string style id")
}
"runs" => {
guard value is Array(entries) else {
raise refuse("\"runs\" is an array of run objects")
}
for entry in entries {
guard entry is Object(run_fields) else {
raise refuse("each run is an object")
}
let mut text : String? = None
let mut bold = false
let mut italic = false
let mut underline = false
for run_key, run_value in run_fields {
match run_key {
"text" =>
match run_value {
String(content) => text = Some(content)
_ => raise refuse("\"text\" is a string")
}
"bold" =>
match run_value {
True => bold = true
False => bold = false
_ => raise refuse("\"bold\" is a boolean")
}
"italic" =>
match run_value {
True => italic = true
False => italic = false
_ => raise refuse("\"italic\" is a boolean")
}
"underline" =>
match run_value {
True => underline = true
False => underline = false
_ => raise refuse("\"underline\" is a boolean")
}
other => raise refuse("unknown run key \"" + other + "\"")
}
}
guard text is Some(content) else {
raise refuse("each run carries \"text\"")
}
runs.push({ text: content, bold, italic, underline, })
}
}
other => raise refuse("unknown key \"" + other + "\"")
}
}
{ style, runs, }
}
///|
async fn run_insert_paragraph(matches : @argparse.Matches) -> Unit {
let file = required_value(matches, "file")
let output = required_value(matches, "out")
let mode = office_validate_output_mode(matches)
guard file.to_lower().has_suffix(".docx") else {
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments", "insert-paragraph requires a .docx document",
),
)
}
guard output.to_lower().has_suffix(".docx") else {
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments", "the destination must end in .docx to match the edited format",
),
)
}
let before_anchor = optional_value(matches, "before")
let after_anchor = optional_value(matches, "after")
let (at, before) = match (before_anchor, after_anchor) {
(Some(_), Some(_)) =>
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments", "--before and --after are exclusive; an insertion has one side",
),
)
(Some(anchor), None) => (anchor, true)
(None, Some(anchor)) => (anchor, false)
(None, None) =>
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments", "insert-paragraph requires --before or --after with a direct body paragraph path (p[3])",
),
)
}
let payload = match optional_value(matches, "content") {
Some(value) => value
None =>
raise CliFailure(
@lib.protocol_error(
"office.invalid_arguments", "insert-paragraph requires --content with a docx.paragraph/1 JSON payload",
),
)
}
let content = parse_insert_paragraph_content(payload)
let dry_run = matches.flags.get_or_default("dry-run", false)
let options = @transaction.transaction_options(
file,
output_path=output,
dry_run~,
overwrite=matches.flags.get_or_default("overwrite", false),
) catch {
error => raise transaction_failure(error)
}
let result = @office_docx.transact_docx_insert_paragraph(
options,
at~,
before~,
content~,
) catch {
error => raise map_insert_paragraph_failure(error)
}
match mode {
// The record is FIXED-SIZE (one path, one eight-hex id, the
// bounded transaction report) — it cannot approach the output
// ceiling, so no in-transaction preflight is needed; the bounded
// emission is still the shared exit for JSON.
JsonDocument | JsonLines => {
let record = Json::object({
"schema": Json::string("office.docx.insert-paragraph/1"),
"file": Json::string(file),
"format": Json::string("docx"),
"output": Json::string(output),
"at": Json::string(at),
"side": Json::string(if before { "before" } else { "after" }),
"dry_run": Json::boolean(dry_run),
"path": Json::string(result.report().path()),
"para_id": Json::string(result.report().para_id()),
"changed": Json::boolean(result.transaction().changed),
"transaction": result.transaction().to_json(),
})
let payload = bounded_docx_payload(
record,
[],
docx_cli_default_max_output_chars,
)
println(checked_office_json_output(payload))
}
Human => {
let verb = if dry_run { "would insert" } else { "inserted" }
println(
"insert-paragraph: " +
verb +
" \{result.report().path()} (w14:paraId \{result.report().para_id()}) -> " +
human_text(output, 160),
)
}
}
}
///|
/// Map a transaction-layer failure to the CLI contract, preserving the
/// typed codes the transaction raises.
fn map_insert_paragraph_failure(error : Error) -> CliFailure {
match error {
CliFailure(_) as failure => failure
@transaction.TransactionError(_) as failure => transaction_failure(failure)
_ =>
unexpected_cli_failure(
"office.insert_paragraph.failed", "insert-paragraph", error,
)
}
}
///|
/// A string-aware, frame-aware scan for duplicate object members in
/// the raw payload text. Frames get identities on '{'; a key seen
/// twice in ONE frame is the duplicate — the same key in two sibling
/// run objects is fine. Malformed text returns None and lets the JSON
/// parser produce its own refusal.
fn insert_payload_duplicate_key(text : String) -> String? {
let units = text.code_units()
let seen : Map[String, Bool] = Map([])
let frames : Array[Int] = []
let mut next_frame = 0
let mut expecting_key = false
let mut index = 0
while index < units.length() {
let unit = units[index].to_int()
if unit == '"'.to_int() {
// Read the string verbatim, honouring escapes.
let start = index + 1
let mut end = start
let mut escaped = false
while end < units.length() {
let inner = units[end].to_int()
if escaped {
escaped = false
} else if inner == '\\'.to_int() {
escaped = true
} else if inner == '"'.to_int() {
break
}
end += 1
}
if expecting_key && frames.length() > 0 {
// Keys compare DECODED: "\u0074ext" and "text" are the same
// member after parsing, so they must collide here too.
let builder = StringBuilder()
let mut at = start
while at < end {
let inner = units[at].to_int()
if inner == '\\'.to_int() && at + 1 < end {
let escape = units[at + 1].to_int()
if escape == 'u'.to_int() && at + 5 < end {
let mut code = 0
let mut valid = true
for digit in (at + 2)..<(at + 6) {
let d = units[digit].to_int()
let value = if d >= '0'.to_int() && d <= '9'.to_int() {
d - '0'.to_int()
} else if d >= 'a'.to_int() && d <= 'f'.to_int() {
d - 'a'.to_int() + 10
} else if d >= 'A'.to_int() && d <= 'F'.to_int() {
d - 'A'.to_int() + 10
} else {
valid = false
0
}
code = code * 16 + value
}
if valid {
// UNIT identity, not scalar identity: a lone surrogate
// half is a legal decoded JSON unit and must neither
// vanish nor conflate with a different key.
builder.write_string("\{code};")
at += 6
continue
}
}
let decoded = match escape.to_char() {
Some('n') => '\n'.to_int()
Some('t') => '\t'.to_int()
Some('r') => '\r'.to_int()
Some('b') => 8
Some('f') => 12
_ => escape
}
builder.write_string("\{decoded};")
at += 2
continue
}
builder.write_string("\{inner};")
at += 1
}
let identity = builder.to_string()
let key = "\{frames[frames.length() - 1]}:" + identity
if seen.contains(key) {
return Some(render_duplicate_key(identity))
}
seen[key] = true
expecting_key = false
}
index = end + 1
continue
}
if unit == '{'.to_int() {
frames.push(next_frame)
next_frame += 1
expecting_key = true
} else if unit == '}'.to_int() {
if frames.length() > 0 {
ignore(frames.pop())
}
expecting_key = false
} else if unit == ','.to_int() {
expecting_key = frames.length() > 0
} else if unit == ':'.to_int() || unit == '['.to_int() {
expecting_key = false
}
index += 1
}
None
}
///|
/// Render an identity unit-sequence ("116;101;…") back to a readable
/// key for the diagnostic: valid scalars as themselves, everything
/// else as \uXXXX. Identity comparison never uses this form.
fn render_duplicate_key(identity : String) -> String {
// Decode the unit sequence first, then render: surrogate PAIRS
// recombine to their scalar, lone halves and controls become
// \uXXXX, and every other scalar shows as itself.
let units : Array[Int] = []
let mut value = 0
let mut has_digit = false
for character in identity {
if character == ';' {
if has_digit {
units.push(value)
}
value = 0
has_digit = false
continue
}
let digit = character.to_int() - '0'.to_int()
if digit >= 0 && digit <= 9 {
value = value * 10 + digit
has_digit = true
}
}
let builder = StringBuilder()
let mut index = 0
while index < units.length() {
let unit = units[index]
if unit >= 0xD800 &&
unit <= 0xDBFF &&
index + 1 < units.length() &&
units[index + 1] >= 0xDC00 &&
units[index + 1] <= 0xDFFF {
let scalar = 0x10000 +
((unit - 0xD800) << 10) +
(units[index + 1] - 0xDC00)
match scalar.to_char() {
Some(character) => builder.write_char(character)
None => {
builder.write_string(render_unit_escape(unit))
builder.write_string(render_unit_escape(units[index + 1]))
}
}
index += 2
continue
}
if unit >= 0x20 && unit != 0x7F && (unit < 0xD800 || unit > 0xDFFF) {
match unit.to_char() {
Some(character) => builder.write_char(character)
None => builder.write_string(render_unit_escape(unit))
}
} else {
builder.write_string(render_unit_escape(unit))
}
index += 1
}
builder.to_string()
}
///|
fn render_unit_escape(value : Int) -> String {
let digits = "0123456789ABCDEF"
let units = digits.code_units()
let builder = StringBuilder()
builder.write_string("\\u")
for shift in [12, 8, 4, 0] {
builder.write_char(units[(value >> shift) & 0xF].to_int().unsafe_to_char())
}
builder.to_string()
}