@attentionform/astra 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +132 -84
  2. package/README.md +1 -1
  3. package/dist/cli.js +886 -371
  4. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -5662,6 +5662,23 @@ function writeCorePropertiesPart(adapter, xml) {
5662
5662
  } catch {
5663
5663
  }
5664
5664
  }
5665
+ function registerExistingCorePropertiesPart(adapter) {
5666
+ let present = false;
5667
+ try {
5668
+ present = adapter.readTextOrNull(CORE_PROPERTIES_PART) !== null;
5669
+ } catch {
5670
+ return;
5671
+ }
5672
+ if (!present) return;
5673
+ try {
5674
+ ensureContentTypeOverride(adapter);
5675
+ } catch {
5676
+ }
5677
+ try {
5678
+ ensurePackageRel(adapter);
5679
+ } catch {
5680
+ }
5681
+ }
5665
5682
  function patchToMetadata(patch) {
5666
5683
  const out = {};
5667
5684
  if (patch.title !== void 0) out.title = patch.title;
@@ -9907,6 +9924,41 @@ function hasLiveNumberingForSync(ast) {
9907
9924
  }
9908
9925
  }
9909
9926
 
9927
+ // ../engine/src/ooxml/quote-style.ts
9928
+ var QUOTE_STYLE_ID = "Quote";
9929
+ var QUOTE_INDENT_TWIPS = 720;
9930
+ var QUOTE_STYLE_BODY = `<w:style w:type="paragraph" w:styleId="Quote">
9931
+ <w:name w:val="Quote"/>
9932
+ <w:basedOn w:val="Normal"/>
9933
+ <w:next w:val="Normal"/>
9934
+ <w:uiPriority w:val="29"/>
9935
+ <w:qFormat/>
9936
+ <w:pPr>
9937
+ <w:pBdr>
9938
+ <w:left w:val="single" w:sz="18" w:space="10" w:color="94A3B8"/>
9939
+ </w:pBdr>
9940
+ <w:spacing w:before="120" w:after="120"/>
9941
+ <w:ind w:left="${QUOTE_INDENT_TWIPS}"/>
9942
+ </w:pPr>
9943
+ <w:rPr>
9944
+ <w:i/>
9945
+ <w:color w:val="475569"/>
9946
+ </w:rPr>
9947
+ </w:style>`;
9948
+ var QUOTE_STYLE_XML = QUOTE_STYLE_BODY.replace("<w:style ", '<w:style xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ');
9949
+ function ensureQuoteStyle(stylesDom) {
9950
+ if (!stylesDom) return false;
9951
+ const root = stylesDom.getElementsByTagName("w:styles")[0];
9952
+ if (!root) return false;
9953
+ for (const style of Array.from(stylesDom.getElementsByTagName("w:style"))) {
9954
+ if (style.getAttribute("w:styleId") === QUOTE_STYLE_ID) return false;
9955
+ }
9956
+ const fragment = new DOMParser().parseFromString(QUOTE_STYLE_XML, "text/xml").documentElement;
9957
+ if (!fragment) return false;
9958
+ root.appendChild(stylesDom.importNode(fragment, true));
9959
+ return true;
9960
+ }
9961
+
9910
9962
  // ../engine/src/parser.ts
9911
9963
  function hasAncestorTag(el, tag) {
9912
9964
  for (let n = el.parentNode; n; n = n.parentNode) {
@@ -10574,6 +10626,7 @@ var DocxParser = class _DocxParser {
10574
10626
  } else {
10575
10627
  this.stylesDom = null;
10576
10628
  }
10629
+ registerExistingCorePropertiesPart(this.getZipAdapter());
10577
10630
  this.invalidateMediaCache();
10578
10631
  this.assignStableIds();
10579
10632
  this._bumpNodePagesRevision();
@@ -11139,6 +11192,27 @@ var DocxParser = class _DocxParser {
11139
11192
  return tracked;
11140
11193
  }
11141
11194
  // --- AST layer: stable id assignment ---
11195
+ /** Orders AST blocks by where their element sits in the body; blocks inside one body child keep their order. */
11196
+ sortBlocksInDocumentOrder(nodes) {
11197
+ const body = this.documentDom.getElementsByTagName("w:body")[0];
11198
+ if (!body || nodes.length < 2) return;
11199
+ const topIndex = /* @__PURE__ */ new Map();
11200
+ let position2 = 0;
11201
+ for (let child = body.firstChild; child; child = child.nextSibling) {
11202
+ if (child.nodeType === 1) topIndex.set(child, position2++);
11203
+ }
11204
+ const keyOf = (element) => {
11205
+ let current = element;
11206
+ while (current && current.parentNode !== body) current = current.parentNode;
11207
+ return current ? topIndex.get(current) ?? Number.MAX_SAFE_INTEGER : Number.MAX_SAFE_INTEGER;
11208
+ };
11209
+ const keyed = nodes.map((node, index) => ({ node, index, key: keyOf(node.xml) }));
11210
+ if (keyed.every((entry, i) => i === 0 || keyed[i - 1].key <= entry.key)) return;
11211
+ keyed.sort((a, b) => a.key - b.key || a.index - b.index);
11212
+ keyed.forEach((entry, i) => {
11213
+ nodes[i] = entry.node;
11214
+ });
11215
+ }
11142
11216
  async getAST() {
11143
11217
  const ast = {
11144
11218
  metadata: {},
@@ -11516,6 +11590,7 @@ var DocxParser = class _DocxParser {
11516
11590
  ast.sectPrFlags = { titlePg: false, evenAndOddHeaders: false };
11517
11591
  }
11518
11592
  ast.nextChangeId = firstFreeRevisionId(this.maxPackageRevisionId());
11593
+ this.sortBlocksInDocumentOrder(rootNodes);
11519
11594
  ast.sections.push(...splitIntoSections(rootNodes, fileSections));
11520
11595
  const prevNumberingAst = this.lastAstForNumbering;
11521
11596
  if (prevNumberingAst && prevNumberingAst !== ast) {
@@ -13290,6 +13365,7 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
13290
13365
  <w:spacing w:before="120" w:after="120" w:line="240" w:lineRule="auto"/>
13291
13366
  </w:pPr>
13292
13367
  </w:style>
13368
+ ${QUOTE_STYLE_BODY}
13293
13369
  <w:style w:type="character" w:styleId="Code">
13294
13370
  <w:name w:val="Code"/>
13295
13371
  <w:rPr>
@@ -13672,15 +13748,26 @@ var import_isomorphic_git = __toESM(require("isomorphic-git"));
13672
13748
  var import_fs3 = __toESM(require("fs"));
13673
13749
  var import_promises = __toESM(require("fs/promises"));
13674
13750
  var import_path2 = __toESM(require("path"));
13751
+ function hasUriScheme(ref) {
13752
+ if (/^[a-zA-Z]:[\\/]/.test(ref)) return false;
13753
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref);
13754
+ }
13755
+ function withoutTrailingSeparator(p, normalized) {
13756
+ const root = p.parse(normalized).root;
13757
+ let out = normalized;
13758
+ while (out.length > root.length && out.endsWith(p.sep)) out = out.slice(0, -1);
13759
+ return out;
13760
+ }
13675
13761
  function isPathInsideDir(candidate, dir, platform = process.platform) {
13676
- const normC = import_path2.default.normalize(candidate);
13677
- const normD = import_path2.default.normalize(dir);
13762
+ const p = platform === "win32" ? import_path2.default.win32 : import_path2.default.posix;
13763
+ let c = withoutTrailingSeparator(p, p.normalize(candidate));
13764
+ let d = withoutTrailingSeparator(p, p.normalize(dir));
13678
13765
  if (platform === "win32") {
13679
- const lc = normC.toLowerCase();
13680
- const ld = normD.toLowerCase();
13681
- return lc === ld || lc.startsWith(ld + import_path2.default.sep.toLowerCase());
13766
+ c = c.toLowerCase();
13767
+ d = d.toLowerCase();
13682
13768
  }
13683
- return normC === normD || normC.startsWith(normD + import_path2.default.sep);
13769
+ if (c === d) return true;
13770
+ return c.startsWith(d.endsWith(p.sep) ? d : d + p.sep);
13684
13771
  }
13685
13772
  function canonicalizeForAsset(resolved) {
13686
13773
  try {
@@ -14928,6 +15015,19 @@ function warnIfLowContrast(warnings, seen, fill, textColor, where) {
14928
15015
  warnings.push(msg);
14929
15016
  }
14930
15017
  }
15018
+ function autoTextForFill(fill, current) {
15019
+ if (!/^[0-9A-Fa-f]{6}$/.test(fill)) return void 0;
15020
+ if (contrastRatio((current ?? "000000").toUpperCase(), fill.toUpperCase()) >= TABLE_CONTRAST_MIN_RATIO) return void 0;
15021
+ return contrastRatio("FFFFFF", fill.toUpperCase()) > contrastRatio("000000", fill.toUpperCase()) ? "FFFFFF" : "000000";
15022
+ }
15023
+ function noteAutoText(warnings, seen, fill, text2, where) {
15024
+ const key = `auto:${fill}:${text2}`;
15025
+ if (seen.has(key)) return;
15026
+ seen.add(key);
15027
+ warnings.push(
15028
+ `Table note: text set to #${text2} at ${where} because black text is not readable on fill #${fill} (contrast ${contrastRatio(text2, fill).toFixed(2)}:1). Pass textColor to choose another colour.`
15029
+ );
15030
+ }
14931
15031
  function paintCellTextColor(tc, doc, normColor) {
14932
15032
  let ps = directChildren5(tc, "w:p");
14933
15033
  if (ps.length === 0) {
@@ -15554,6 +15654,13 @@ async function executeTable(opIn, ast, guide) {
15554
15654
  let effectiveText;
15555
15655
  if (rIdx === 0) {
15556
15656
  effectiveText = explicitTextColor ?? guideTextForFill(rowFill);
15657
+ if (effectiveText === void 0) {
15658
+ const auto = autoTextForFill(rowFill, firstExplicitCellTextColor(tc));
15659
+ if (auto) {
15660
+ effectiveText = auto;
15661
+ noteAutoText(warnings, warnedContrastPairs, rowFill, auto, `header cell (${rIdx + 1},${cIdx + 1}) of table '${tableNode.id}'`);
15662
+ }
15663
+ }
15557
15664
  if (effectiveText && tblEl.ownerDocument) {
15558
15665
  paintCellTextColor(tc, tblEl.ownerDocument, effectiveText);
15559
15666
  }
@@ -16080,7 +16187,14 @@ async function executeTable(opIn, ast, guide) {
16080
16187
  shd.setAttribute("w:color", "auto");
16081
16188
  shd.setAttribute("w:fill", normFill);
16082
16189
  sortChildrenBySchema(tcPr, TCPR_CHILD_ORDER);
16083
- const effectiveText = explicitTextColor ?? guideTextForFill(normFill);
16190
+ let effectiveText = explicitTextColor ?? guideTextForFill(normFill);
16191
+ if (effectiveText === void 0) {
16192
+ const auto = autoTextForFill(normFill, firstExplicitCellTextColor(tc));
16193
+ if (auto) {
16194
+ effectiveText = auto;
16195
+ noteAutoText(warnings, warnedContrastPairs, normFill, auto, `cell (${shown(rowIndex)},${shown(colIndex)}) of table '${tableNode.id}'`);
16196
+ }
16197
+ }
16084
16198
  if (effectiveText && tblEl.ownerDocument) {
16085
16199
  paintCellTextColor(tc, tblEl.ownerDocument, effectiveText);
16086
16200
  }
@@ -16364,7 +16478,7 @@ async function executeTable(opIn, ast, guide) {
16364
16478
  break;
16365
16479
  }
16366
16480
  case "setCellTextColor": {
16367
- const colorInput = raw.color ?? op.color;
16481
+ const colorInput = raw.color ?? op.color ?? raw.textColor ?? op.textColor;
16368
16482
  if (colorInput === void 0 || colorInput === null || String(colorInput).trim() === "") {
16369
16483
  throw new Error(
16370
16484
  `E_VALIDATION: setCellTextColor requires color (got no color). Remedy: pass color as 6 hex digits without '#', e.g. color:'FFFFFF' for white header text.`
@@ -16820,6 +16934,13 @@ function sanitizeXml(xmlStr) {
16820
16934
  // ../engine/src/operation-schema.ts
16821
16935
  var import_zod2 = require("zod");
16822
16936
 
16937
+ // ../engine/src/anchor-alias.ts
16938
+ function anchorAliasOf(value) {
16939
+ if (value === "start" || value === "@start") return "start";
16940
+ if (value === "end" || value === "@end") return "end";
16941
+ return null;
16942
+ }
16943
+
16823
16944
  // ../engine/src/operation-params.ts
16824
16945
  var STRUCTURAL = /* @__PURE__ */ new Set(["action", "target", "tableId", "operation", "op", "params", "trackChanges", "expectedVersion"]);
16825
16946
  var ROW = ["rowIndex", "row"];
@@ -16845,9 +16966,17 @@ var TABLE_SUBOP_KEYS = {
16845
16966
  setCellAlignment: ["align", "alignment", ...ROW, ...COL],
16846
16967
  setTableAlignment: ["align", "alignment"],
16847
16968
  setCellPadding: ["padding", ...ROW, ...COL],
16848
- setCellTextColor: ["color", ...ROW, ...COL],
16969
+ setCellTextColor: ["color", "textColor", ...ROW, ...COL],
16849
16970
  setKeepTogether: []
16850
16971
  };
16972
+ function describeTableSubOpKeys() {
16973
+ const aliases = /* @__PURE__ */ new Set(["rowIndex", "colIndex", "columnIndex"]);
16974
+ const parts = Object.entries(TABLE_SUBOP_KEYS).map(([sub, keys]) => {
16975
+ const shown2 = keys.filter((k) => !aliases.has(k));
16976
+ return shown2.length > 0 ? `${sub}(${shown2.join(",")})` : sub;
16977
+ });
16978
+ return `Parameters each operation reads (row and col count from 1): ${parts.join("; ")}.`;
16979
+ }
16851
16980
  function isRecord(value) {
16852
16981
  return value !== null && typeof value === "object" && !Array.isArray(value);
16853
16982
  }
@@ -20801,6 +20930,8 @@ var expectedVersion = import_zod2.z.number().int().nonnegative().optional();
20801
20930
  var nestedParams = import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional();
20802
20931
  var sectionNumber = import_zod2.z.number().int().positive("sections count from 1 (1 is the first section)").describe("Section number, counting from 1 (1 is the first section).");
20803
20932
  var position = import_zod2.z.enum(["before", "after"]);
20933
+ var ANCHOR_REF_DESCRIPTION = "A node id from inspect_document, or 'start' / 'end' for the first / last block of the document (no inspect needed; '@start' and '@end' work too). With 'start', position defaults to 'before'; with 'end', to 'after'.";
20934
+ var ANCHOR_REF_SHORT = "A node id, or 'start' / 'end' for the first / last block.";
20804
20935
  var operationInsertPosition = import_zod2.z.enum(["before", "after"]).optional();
20805
20936
  var FONT_SIZE_PT_REGEX = /^\s*(\d+(?:\.\d+)?)\s*pt\s*$/i;
20806
20937
  function parseFontSize(sz) {
@@ -20903,7 +21034,7 @@ var insertOp = import_zod2.z.object({
20903
21034
  // engine appends in call order without ever reading them) need no dummy
20904
21035
  // anchor. Body inserts still require both — enforced by the strict
20905
21036
  // validators below, not by this shape.
20906
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21037
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_DESCRIPTION).optional(),
20907
21038
  position: operationInsertPosition,
20908
21039
  text: import_zod2.z.string().max(5e5).optional(),
20909
21040
  xml: import_zod2.z.string().max(5e5).optional(),
@@ -21025,7 +21156,7 @@ var clearFormatOp = import_zod2.z.object({
21025
21156
  var moveOp = import_zod2.z.object({
21026
21157
  action: import_zod2.z.literal("move"),
21027
21158
  target: nodeIdOrList,
21028
- anchor: import_zod2.z.string().min(1).max(256),
21159
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
21029
21160
  position,
21030
21161
  expectedVersion,
21031
21162
  params: nestedParams
@@ -21062,8 +21193,8 @@ var tableOp = import_zod2.z.object({
21062
21193
  action: import_zod2.z.literal("table"),
21063
21194
  target: import_zod2.z.string().min(1).max(256).optional(),
21064
21195
  tableId: import_zod2.z.string().min(1).max(256).optional(),
21065
- operation: tableSubOp.optional(),
21066
- op: tableSubOp.optional(),
21196
+ operation: tableSubOp.describe(describeTableSubOpKeys()).optional(),
21197
+ op: tableSubOp.describe("Alias of operation.").optional(),
21067
21198
  trackChanges: import_zod2.z.boolean().optional(),
21068
21199
  author: revisionAuthor,
21069
21200
  date: revisionDate,
@@ -21093,7 +21224,6 @@ var tableOp = import_zod2.z.object({
21093
21224
  border: import_zod2.z.string().max(32).optional(),
21094
21225
  color: import_zod2.z.string().max(16).optional(),
21095
21226
  sz: fontSizeOrSzSchema.optional(),
21096
- fontSize: fontSizeOrSzSchema.optional(),
21097
21227
  vAlign: import_zod2.z.string().max(16).optional(),
21098
21228
  align: tableAlignmentSchema.optional(),
21099
21229
  alignment: tableAlignmentSchema.optional(),
@@ -21155,7 +21285,7 @@ var namedStyleOp = import_zod2.z.object({
21155
21285
  var fieldOp = import_zod2.z.object({
21156
21286
  action: import_zod2.z.literal("field"),
21157
21287
  operation: import_zod2.z.enum(["insertTOC", "insertPageNumber", "insertWordCount", "updateFields"]),
21158
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21288
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21159
21289
  position: operationInsertPosition,
21160
21290
  title: import_zod2.z.string().max(1e3).optional(),
21161
21291
  levels: import_zod2.z.number().int().min(1).max(9).optional(),
@@ -21171,7 +21301,7 @@ var equationOp = import_zod2.z.object({
21171
21301
  action: import_zod2.z.literal("equation"),
21172
21302
  operation: import_zod2.z.enum(["insert", "replace"]),
21173
21303
  latex: import_zod2.z.string().min(1).max(5e4),
21174
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21304
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21175
21305
  position: operationInsertPosition,
21176
21306
  target: import_zod2.z.string().min(1).max(256).optional(),
21177
21307
  id: import_zod2.z.string().min(1).max(256).optional(),
@@ -21285,7 +21415,7 @@ var chartOp = import_zod2.z.object({
21285
21415
  categories: import_zod2.z.array(import_zod2.z.string().max(500)).max(1e3).optional(),
21286
21416
  series: import_zod2.z.array(chartSeries).max(100).optional(),
21287
21417
  style: chartStyle.optional(),
21288
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21418
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21289
21419
  id: import_zod2.z.string().min(1).max(256).optional(),
21290
21420
  widthEmu: import_zod2.z.number().positive().max(1e8).optional(),
21291
21421
  heightEmu: import_zod2.z.number().positive().max(1e8).optional(),
@@ -21295,7 +21425,7 @@ var chartOp = import_zod2.z.object({
21295
21425
  var imageOp = import_zod2.z.object({
21296
21426
  action: import_zod2.z.literal("image"),
21297
21427
  operation: import_zod2.z.enum(["insert", "resize", "reposition", "setWrap", "setCaption", "delete", "setAltText"]),
21298
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21428
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21299
21429
  target: import_zod2.z.string().min(1).max(256).optional(),
21300
21430
  fileData: import_zod2.z.string().max(2e7).optional(),
21301
21431
  filePath: import_zod2.z.string().min(1).max(1024).optional(),
@@ -21309,7 +21439,8 @@ var imageOp = import_zod2.z.object({
21309
21439
  var calloutOp = import_zod2.z.object({
21310
21440
  action: import_zod2.z.literal("callout"),
21311
21441
  text: import_zod2.z.string().min(1).max(1e5),
21312
- anchor: import_zod2.z.string().min(1).max(256).optional(),
21442
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT).optional(),
21443
+ id: import_zod2.z.string().min(1).max(256).optional(),
21313
21444
  title: import_zod2.z.string().max(1e3).optional(),
21314
21445
  theme: import_zod2.z.string().max(32).optional(),
21315
21446
  position: operationInsertPosition,
@@ -21319,7 +21450,7 @@ var calloutOp = import_zod2.z.object({
21319
21450
  var footnoteOp = import_zod2.z.object({
21320
21451
  action: import_zod2.z.literal("footnote"),
21321
21452
  operation: import_zod2.z.enum(["insert"]).optional(),
21322
- anchor: import_zod2.z.string().min(1).max(256),
21453
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
21323
21454
  text: import_zod2.z.string().min(1).max(5e4),
21324
21455
  id: import_zod2.z.string().min(1).max(256).optional(),
21325
21456
  offset: import_zod2.z.number().int().nonnegative().optional(),
@@ -21328,7 +21459,7 @@ var footnoteOp = import_zod2.z.object({
21328
21459
  var endnoteOp = import_zod2.z.object({
21329
21460
  action: import_zod2.z.literal("endnote"),
21330
21461
  operation: import_zod2.z.enum(["insert"]).optional(),
21331
- anchor: import_zod2.z.string().min(1).max(256),
21462
+ anchor: import_zod2.z.string().min(1).max(256).describe(ANCHOR_REF_SHORT),
21332
21463
  text: import_zod2.z.string().min(1).max(5e4),
21333
21464
  id: import_zod2.z.string().min(1).max(256).optional(),
21334
21465
  offset: import_zod2.z.number().int().nonnegative().optional(),
@@ -21507,11 +21638,11 @@ function insertBodyAnchorDetail(op) {
21507
21638
  if (effectiveTarget === "header" || effectiveTarget === "footer") return null;
21508
21639
  const missing = [];
21509
21640
  if (typeof rec["anchor"] !== "string" || rec["anchor"].length < 1) {
21510
- missing.push("anchor: anchor is required for body inserts [invalid_type]");
21641
+ missing.push("anchor: anchor is required for body inserts: a node id from inspect_document, or 'start' / 'end' for the first / last block [invalid_type]");
21511
21642
  }
21512
21643
  const pos = rec["position"];
21513
- if (pos !== "before" && pos !== "after") {
21514
- missing.push("position: position is required for body inserts [invalid_type]");
21644
+ if (pos !== "before" && pos !== "after" && anchorAliasOf(rec["anchor"]) === null) {
21645
+ missing.push("position: position is required for body inserts (or use anchor 'start' / 'end', which imply it) [invalid_type]");
21515
21646
  }
21516
21647
  return missing.length > 0 ? missing.join("; ") : null;
21517
21648
  }
@@ -28225,7 +28356,7 @@ function resolveImageFilePath(allowedDir, filePath) {
28225
28356
  "use a plain relative file path inside the allowed directory"
28226
28357
  );
28227
28358
  }
28228
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
28359
+ if (hasUriScheme(trimmed)) {
28229
28360
  throw toError(
28230
28361
  "E_SANDBOX",
28231
28362
  "image filePath points outside the allowed directory",
@@ -28396,7 +28527,7 @@ function resolveAssetPath(allowedDir, refPath, line2, what) {
28396
28527
  if (trimmed.includes("\0") || /%00/i.test(trimmed) || trimmed.includes("\n") || trimmed.includes("\r")) {
28397
28528
  fail(line2, `${what} reference path is not usable`, "use a plain relative file path inside the allowed directory");
28398
28529
  }
28399
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
28530
+ if (hasUriScheme(trimmed)) {
28400
28531
  fail(line2, `${what} reference points outside the allowed directory`, "keep referenced files inside the allowed directory and use a relative path");
28401
28532
  }
28402
28533
  const candidate = import_path5.default.isAbsolute(trimmed) ? import_path5.default.resolve(trimmed) : import_path5.default.resolve(allowedDir, trimmed);
@@ -29666,7 +29797,8 @@ async function executeCallout(op, ast, parser2, guide) {
29666
29797
  if (!targetSection) throw new Error("No section available for callout box insertion");
29667
29798
  const ownerDoc = targetSection.nodes[0]?.xml?.ownerDocument || ownerDocumentOf(ast.sections);
29668
29799
  if (!ownerDoc) throw new Error("Cannot resolve XML DOM document");
29669
- const calloutId = isCalloutReproducible() ? (() => {
29800
+ const clientId = pick("id", op.id);
29801
+ const calloutId = typeof clientId === "string" && clientId.length > 0 ? clientId : isCalloutReproducible() ? (() => {
29670
29802
  const base = `callout_${calloutHash8(`${resolvedAnchor ?? ""}:${resolvedPosition}:${themeKey}:${resolvedTitle ?? ""}:${resolvedText}`)}`;
29671
29803
  const live = /* @__PURE__ */ new Set();
29672
29804
  try {
@@ -30977,6 +31109,208 @@ function executePageBorder(op, ast, parser2) {
30977
31109
  section.version += 1;
30978
31110
  }
30979
31111
 
31112
+ // ../engine/src/compose/pristine-seed.ts
31113
+ function pristineTemplateSeedIds(parser2, requestedTemplate) {
31114
+ if (!parser2.hasPristineBuiltinSeed()) return null;
31115
+ let summary;
31116
+ try {
31117
+ summary = parser2.getStructuralSummary();
31118
+ } catch {
31119
+ return null;
31120
+ }
31121
+ const nodes = summary.nodes ?? [];
31122
+ if (![1, 2, 4, 5, 6].includes(nodes.length)) return null;
31123
+ if (summary.hasTrackedChanges === true) return null;
31124
+ if (Object.keys(summary.mediaFiles ?? {}).length > 0) return null;
31125
+ if ((summary.headers ?? []).length > 0) return null;
31126
+ if ((summary.footers ?? []).length > 0) return null;
31127
+ if ((summary.charts ?? []).length > 0) return null;
31128
+ try {
31129
+ const zip = parser2.getZipAdapter?.();
31130
+ if (typeof zip?.hasFile === "function") {
31131
+ for (const part of [
31132
+ "word/footnotes.xml",
31133
+ "word/endnotes.xml",
31134
+ "word/comments.xml",
31135
+ "word/commentsExtended.xml"
31136
+ ]) {
31137
+ try {
31138
+ if (zip.hasFile(part)) return null;
31139
+ } catch {
31140
+ return null;
31141
+ }
31142
+ }
31143
+ } else {
31144
+ return null;
31145
+ }
31146
+ } catch {
31147
+ return null;
31148
+ }
31149
+ for (const n of nodes) {
31150
+ if (n.hasTrackedChanges === true) return null;
31151
+ const raw = n.rawXml ?? "";
31152
+ if (raw.includes("w:footnoteReference") || raw.includes("w:endnoteReference") || raw.includes("w:commentReference") || raw.includes("w:commentRangeStart") || raw.includes("w:commentRangeEnd")) {
31153
+ return null;
31154
+ }
31155
+ }
31156
+ const ids = nodes.map((n) => n.id ?? "").filter((id) => id.length > 0);
31157
+ if (ids.length !== nodes.length) return null;
31158
+ if (nodes.length === 1) {
31159
+ const node = nodes[0];
31160
+ if (node.type !== "paragraph") return null;
31161
+ if ((node.text ?? "").trim() !== "") return null;
31162
+ return ids;
31163
+ }
31164
+ if (nodes.length === 2) {
31165
+ const [first, second] = nodes;
31166
+ const t0 = (first.text ?? "").trim();
31167
+ const t1 = (second.text ?? "").trim();
31168
+ if (t0 === BLANK_SEED_TITLE && t1 === BLANK_SEED_HINT) {
31169
+ const firstRaw = first.rawXml ?? "";
31170
+ const secondRaw = second.rawXml ?? "";
31171
+ if (!firstRaw.includes('w:val="Title"') || !secondRaw.includes('w:val="Subtitle"')) return null;
31172
+ return ids;
31173
+ }
31174
+ const tmpl = requestedTemplate?.toLowerCase();
31175
+ if (t0 === "MEMORANDUM" && tmpl === "memo") return ids;
31176
+ if (t0 === "Letter" && tmpl === "letter") return ids;
31177
+ if (t0 === "Proposal" && tmpl === "proposal") return ids;
31178
+ if (t0 === "INVOICE" && tmpl === "invoice") return ids;
31179
+ return null;
31180
+ }
31181
+ if (nodes.length === 4) {
31182
+ const t0 = (nodes[0].text ?? "").trim();
31183
+ const tmpl = requestedTemplate?.toLowerCase();
31184
+ if (t0 === "Research Benchmark Report" && tmpl === "research") return ids;
31185
+ if (t0 === "Corporate Strategy Roadmap" && tmpl === "corporate") return ids;
31186
+ if (t0 === "Velocity Audit & Performance" && tmpl === "bar-chart-3d") return ids;
31187
+ return null;
31188
+ }
31189
+ if (nodes.length === 5) {
31190
+ const t0 = (nodes[0].text ?? "").trim();
31191
+ const tmpl = requestedTemplate?.toLowerCase();
31192
+ if (t0 === "Financial Report & Audit" && tmpl === "financial") return ids;
31193
+ return null;
31194
+ }
31195
+ if (nodes.length === 6) {
31196
+ const t0 = (nodes[0].text ?? "").trim();
31197
+ const t2 = (nodes[2].text ?? "").trim();
31198
+ const tmpl = requestedTemplate?.toLowerCase();
31199
+ if (t0 === "Q3 Executive Financial Performance Report" && t2 === "1. Executive Summary") {
31200
+ if (tmpl === "report") return ids;
31201
+ return null;
31202
+ }
31203
+ if (t0 === "Manuscript Title" && t2 === "Chapter 1: The Beginning" && tmpl === "novel") {
31204
+ return ids;
31205
+ }
31206
+ if (t0 === "Neural Document Architectures: A Comparative Benchmark" && tmpl === "academic") {
31207
+ return ids;
31208
+ }
31209
+ if (t0 === "The ASTra Dispatch \u2014 Monthly Engineering Gazette" && tmpl === "newsletter") return ids;
31210
+ if (t0 === "ASTra Document Engine API Specification" && (tmpl === "api" || tmpl === "api-spec")) return ids;
31211
+ return null;
31212
+ }
31213
+ return null;
31214
+ }
31215
+ var SEED_TEMPLATE_NAMES = [
31216
+ "memo",
31217
+ "letter",
31218
+ "proposal",
31219
+ "invoice",
31220
+ "research",
31221
+ "corporate",
31222
+ "bar-chart-3d",
31223
+ "financial",
31224
+ "report",
31225
+ "novel",
31226
+ "academic",
31227
+ "newsletter",
31228
+ "api-spec"
31229
+ ];
31230
+ function pristineSeedIdsOfAnyTemplate(parser2) {
31231
+ const blank = pristineTemplateSeedIds(parser2);
31232
+ if (blank !== null) return blank;
31233
+ for (const name2 of SEED_TEMPLATE_NAMES) {
31234
+ const ids = pristineTemplateSeedIds(parser2, name2);
31235
+ if (ids !== null) return ids;
31236
+ }
31237
+ return null;
31238
+ }
31239
+
31240
+ // ../engine/src/document-title.ts
31241
+ var TEMPLATE_NAMES = [
31242
+ "blank",
31243
+ "memo",
31244
+ "report",
31245
+ "letter",
31246
+ "proposal",
31247
+ "invoice",
31248
+ "research",
31249
+ "corporate",
31250
+ "financial",
31251
+ "novel",
31252
+ "academic",
31253
+ "newsletter",
31254
+ "api-spec",
31255
+ "bar-chart-3d"
31256
+ ];
31257
+ var PLACEHOLDER_TITLES = new Set(TEMPLATE_NAMES.map((name2) => newDocumentTitle(name2).toLowerCase()));
31258
+ var DERIVED_TITLE_MAX = 200;
31259
+ var DEFAULT_CREATOR = "ASTra";
31260
+ function isPlaceholderTitle(title) {
31261
+ if (typeof title !== "string") return true;
31262
+ const clean = title.replace(/\s+/g, " ").trim().toLowerCase();
31263
+ return clean === "" || PLACEHOLDER_TITLES.has(clean);
31264
+ }
31265
+ function cleanTitle(text2) {
31266
+ const plain = typeof text2 === "string" ? Array.from(text2, (ch) => ch.charCodeAt(0) < 32 || ch.charCodeAt(0) === 127 ? " " : ch).join("").replace(/\s+/g, " ").trim() : "";
31267
+ return plain.length > DERIVED_TITLE_MAX ? plain.slice(0, DERIVED_TITLE_MAX).trimEnd() : plain;
31268
+ }
31269
+ function deriveTitleFromContent(ast) {
31270
+ const nodes = ast.sections.flatMap((section) => section.nodes);
31271
+ const styleOf = (node) => {
31272
+ const pPr = Array.from(node.xml.childNodes).find((c) => c.nodeType === 1 && c.tagName === "w:pPr");
31273
+ const pStyle = pPr ? Array.from(pPr.childNodes).find((c) => c.nodeType === 1 && c.tagName === "w:pStyle") : void 0;
31274
+ return pStyle?.getAttribute("w:val") ?? node.metadata?.namedStyle ?? void 0;
31275
+ };
31276
+ const textOf4 = (node) => {
31277
+ const runs = node.xml.getElementsByTagName("w:t");
31278
+ let text2 = "";
31279
+ for (let i = 0; i < runs.length; i++) text2 += runs[i].textContent ?? "";
31280
+ return text2 !== "" ? text2 : node.metadata?.text ?? "";
31281
+ };
31282
+ const pick = (match, from) => {
31283
+ for (const node of nodes) {
31284
+ if (node.type !== "paragraph" || !match(node)) continue;
31285
+ const title = cleanTitle(textOf4(node));
31286
+ if (title !== "" && !isPlaceholderTitle(title)) return { title, from };
31287
+ }
31288
+ return null;
31289
+ };
31290
+ return pick((n) => n.id.startsWith("cover_title_"), "cover page") ?? pick((n) => styleOf(n) === "Title", "Title") ?? pick((n) => styleOf(n) === "Heading1", "Heading 1");
31291
+ }
31292
+ function applyDocumentIdentity(ast, parser2) {
31293
+ const result = {};
31294
+ const patch = {};
31295
+ if (isPlaceholderTitle(ast.metadata?.title)) {
31296
+ const derived = deriveTitleFromContent(ast);
31297
+ if (derived) {
31298
+ patch.title = derived.title;
31299
+ result.title = derived.title;
31300
+ result.titleSetFrom = derived.from;
31301
+ }
31302
+ }
31303
+ const author = typeof ast.settings?.defaultAuthor === "string" ? ast.settings.defaultAuthor.trim() : "";
31304
+ if (author !== "" && author !== DEFAULT_CREATOR && ast.metadata?.creator === DEFAULT_CREATOR) {
31305
+ patch.creator = author;
31306
+ patch.lastModifiedBy = author;
31307
+ result.creatorSet = author;
31308
+ }
31309
+ if (Object.keys(patch).length === 0) return result;
31310
+ new CorePropertiesManipulator(ast, parser2?.getZipAdapter()).update(patch);
31311
+ return result;
31312
+ }
31313
+
30980
31314
  // ../engine/src/operations/coverPage.ts
30981
31315
  function isCoverReproducible(explicit, env) {
30982
31316
  try {
@@ -31041,6 +31375,7 @@ function executeCoverPage(op, ast, parser2) {
31041
31375
  if (!firstSection || firstSection.nodes.length === 0) {
31042
31376
  throw new Error("Cannot insert cover page into an empty document");
31043
31377
  }
31378
+ const seedIds = parser2 ? pristineSeedIdsOfAnyTemplate(parser2) : null;
31044
31379
  const doc = firstSection.nodes[0].xml.ownerDocument;
31045
31380
  if (!doc) throw new Error("Document not found on first section");
31046
31381
  const sectPr = doc.getElementsByTagName("w:sectPr")[0];
@@ -31107,8 +31442,8 @@ function executeCoverPage(op, ast, parser2) {
31107
31442
  titleColor.setAttribute("w:val", accentColor);
31108
31443
  titleRPr.appendChild(titleFont);
31109
31444
  titleRPr.appendChild(titleBold);
31110
- titleRPr.appendChild(titleSz);
31111
31445
  titleRPr.appendChild(titleColor);
31446
+ titleRPr.appendChild(titleSz);
31112
31447
  titleRun.appendChild(titleRPr);
31113
31448
  titleRun.appendChild(createTextElement(doc, titleText));
31114
31449
  titleP.appendChild(titleRun);
@@ -31134,8 +31469,8 @@ function executeCoverPage(op, ast, parser2) {
31134
31469
  subSz.setAttribute("w:val", "28");
31135
31470
  const subColor = createWordElement(doc, "w:color");
31136
31471
  subColor.setAttribute("w:val", "64748B");
31137
- subRPr.appendChild(subSz);
31138
31472
  subRPr.appendChild(subColor);
31473
+ subRPr.appendChild(subSz);
31139
31474
  subRun.appendChild(subRPr);
31140
31475
  subRun.appendChild(createTextElement(doc, subtitleText));
31141
31476
  subP.appendChild(subRun);
@@ -31185,8 +31520,8 @@ function executeCoverPage(op, ast, parser2) {
31185
31520
  metaSz.setAttribute("w:val", "22");
31186
31521
  const metaColor = createWordElement(doc, "w:color");
31187
31522
  metaColor.setAttribute("w:val", "475569");
31188
- metaRPr.appendChild(metaSz);
31189
31523
  metaRPr.appendChild(metaColor);
31524
+ metaRPr.appendChild(metaSz);
31190
31525
  metaRun.appendChild(metaRPr);
31191
31526
  const metaLines = [authorText, orgText, dateText].filter(Boolean).join(" \u2022 ");
31192
31527
  metaRun.appendChild(createTextElement(doc, metaLines || dateText));
@@ -31231,6 +31566,13 @@ function executeCoverPage(op, ast, parser2) {
31231
31566
  }
31232
31567
  }
31233
31568
  }
31569
+ if (seedIds !== null && seedIds.length > 0) {
31570
+ executeDelete({ action: "delete", target: seedIds }, ast, parser2);
31571
+ }
31572
+ try {
31573
+ applyDocumentIdentity(ast, parser2);
31574
+ } catch {
31575
+ }
31234
31576
  }
31235
31577
 
31236
31578
  // ../engine/src/operations/metadata.ts
@@ -31402,8 +31744,9 @@ function collectBatchTouchedIds(ops, ast) {
31402
31744
  const touch = (raw) => {
31403
31745
  if (typeof raw !== "string" || raw.length === 0) return;
31404
31746
  let resolved = raw;
31405
- if (raw === "start") resolved = order[0] ?? raw;
31406
- else if (raw === "end") resolved = order[order.length - 1] ?? raw;
31747
+ const alias = anchorAliasOf(raw);
31748
+ if (alias === "start") resolved = order[0] ?? raw;
31749
+ else if (alias === "end") resolved = order[order.length - 1] ?? raw;
31407
31750
  else {
31408
31751
  const cell = parseCellAddress(raw);
31409
31752
  if (cell && order.includes(cell.tableId)) resolved = cell.tableId;
@@ -31996,13 +32339,13 @@ var OperationEngine = class _OperationEngine {
31996
32339
  /**
31997
32340
  * R-4 helper: true when the op mints a live node id from the client id.
31998
32341
  * Only these routes use `op.id` verbatim as the node id; other actions
31999
- * either ignore `id` (footnote/callout/field generate their own) or are
32342
+ * either ignore `id` (footnote/field generate their own) or are
32000
32343
  * rejected at the schema gate, so live collision applies here only.
32001
32344
  */
32002
32345
  static mintsNodeId(op) {
32003
32346
  const fields = op;
32004
32347
  const action = typeof fields["action"] === "string" ? String(fields["action"]) : "";
32005
- if (action === "insert") return true;
32348
+ if (action === "insert" || action === "callout") return true;
32006
32349
  if (action === "equation") {
32007
32350
  const sub = fields["operation"] ?? fields["params"]?.["operation"];
32008
32351
  return sub === "insert";
@@ -32102,7 +32445,7 @@ var OperationEngine = class _OperationEngine {
32102
32445
  * E_VALIDATION + remedy.
32103
32446
  */
32104
32447
  static isAnchorAlias(value) {
32105
- return value === "start" || value === "end";
32448
+ return anchorAliasOf(value) !== null;
32106
32449
  }
32107
32450
  resolveAnchorAlias(op) {
32108
32451
  const fields = op;
@@ -32118,6 +32461,10 @@ var OperationEngine = class _OperationEngine {
32118
32461
  }
32119
32462
  }
32120
32463
  if (aliasSeen === null) return;
32464
+ for (const h of holders) {
32465
+ const alias = anchorAliasOf(h["anchor"]);
32466
+ if (alias !== null && h["position"] === void 0) h["position"] = alias === "start" ? "before" : "after";
32467
+ }
32121
32468
  let first;
32122
32469
  let last;
32123
32470
  for (const section of this.ast.sections ?? []) {
@@ -32134,8 +32481,9 @@ var OperationEngine = class _OperationEngine {
32134
32481
  );
32135
32482
  }
32136
32483
  for (const h of holders) {
32137
- if (h["anchor"] === "start") h["anchor"] = first;
32138
- else if (h["anchor"] === "end") h["anchor"] = last;
32484
+ const alias = anchorAliasOf(h["anchor"]);
32485
+ if (alias === "start") h["anchor"] = first;
32486
+ else if (alias === "end") h["anchor"] = last;
32139
32487
  }
32140
32488
  }
32141
32489
  async routeOperation(op, allowedDir) {
@@ -33392,7 +33740,11 @@ ${textLines.map((l) => `> ${l}`).join("\n")}
33392
33740
  `);
33393
33741
  } else {
33394
33742
  const textLines = text2 ? text2.split("\n") : [];
33395
- lines.push(`${textLines.map((l) => `> ${l}`).join("\n")}
33743
+ const ind = node.xml?.getElementsByTagName?.("w:pPr")?.[0]?.getElementsByTagName?.("w:ind")?.[0] ?? null;
33744
+ const left = ind ? parseInt(ind.getAttribute("w:left") || "0", 10) : 0;
33745
+ const depth = style === "quote" && Number.isFinite(left) && left > 0 ? Math.min(3, Math.max(1, Math.round(left / 720))) : 1;
33746
+ const marker = ">".repeat(1).concat(" >".repeat(depth - 1));
33747
+ lines.push(`${textLines.map((l) => `${marker} ${l}`).join("\n")}
33396
33748
  `);
33397
33749
  }
33398
33750
  } else {
@@ -33896,17 +34248,19 @@ function directKids(parent, tag) {
33896
34248
  return out;
33897
34249
  }
33898
34250
  function cellTextOf(tc) {
33899
- let text2 = "";
34251
+ const parts = [];
33900
34252
  try {
33901
34253
  const paras = directKids(tc, "w:p");
33902
34254
  for (const p of paras) {
34255
+ let text2 = "";
33903
34256
  const ts = p.getElementsByTagName("w:t");
33904
34257
  for (let i = 0; i < ts.length; i++) text2 += ts[i].textContent || "";
34258
+ if (text2.trim().length > 0) parts.push(text2);
33905
34259
  }
33906
34260
  } catch {
33907
34261
  return "";
33908
34262
  }
33909
- return text2;
34263
+ return parts.join(" ");
33910
34264
  }
33911
34265
  function displayLen(value) {
33912
34266
  return [...value].length;
@@ -38075,6 +38429,134 @@ var HeadlessVisualRenderer = class {
38075
38429
  };
38076
38430
  var headlessScreenshotProvider = new HeadlessVisualRenderer();
38077
38431
 
38432
+ // ../engine/src/stats.ts
38433
+ function nearestOwner(el, tagName) {
38434
+ let cur = el.parentNode;
38435
+ while (cur && typeof cur === "object") {
38436
+ const e = cur;
38437
+ if (e.tagName === tagName) return e;
38438
+ cur = e.parentNode;
38439
+ }
38440
+ return null;
38441
+ }
38442
+ function tableCellTextSpaced(node) {
38443
+ try {
38444
+ const xml = node.xml;
38445
+ if (!xml || typeof xml.getElementsByTagName !== "function") return void 0;
38446
+ const tcs = xml.getElementsByTagName("w:tc");
38447
+ if (!tcs || tcs.length === 0) return void 0;
38448
+ const cells = [];
38449
+ for (let i = 0; i < tcs.length; i++) {
38450
+ const tc = tcs[i];
38451
+ const paras = tc.getElementsByTagName("w:p");
38452
+ if (paras.length > 0) {
38453
+ const parts = [];
38454
+ for (let p = 0; p < paras.length; p++) {
38455
+ const para = paras[p];
38456
+ if (nearestOwner(para, "w:tc") !== tc) continue;
38457
+ const runs = para.getElementsByTagName("w:t");
38458
+ let t = "";
38459
+ for (let r = 0; r < runs.length; r++) {
38460
+ const run2 = runs[r];
38461
+ if (nearestOwner(run2, "w:tc") !== tc) continue;
38462
+ if (nearestOwner(run2, "w:p") !== para) continue;
38463
+ t += run2.textContent || "";
38464
+ }
38465
+ if (t.trim().length > 0) parts.push(t.trim());
38466
+ }
38467
+ if (parts.length > 0) cells.push(parts.join(" "));
38468
+ } else {
38469
+ const runs = tc.getElementsByTagName("w:t");
38470
+ let t = "";
38471
+ for (let r = 0; r < runs.length; r++) {
38472
+ const run2 = runs[r];
38473
+ if (nearestOwner(run2, "w:tc") !== tc) continue;
38474
+ t += run2.textContent || "";
38475
+ }
38476
+ if (t.trim().length > 0) cells.push(t.trim());
38477
+ }
38478
+ }
38479
+ if (cells.length === 0) return "";
38480
+ return cells.join(" ");
38481
+ } catch {
38482
+ return void 0;
38483
+ }
38484
+ }
38485
+ function nestedTableCount(node) {
38486
+ try {
38487
+ const xml = node.xml;
38488
+ if (!xml || typeof xml.getElementsByTagName !== "function") return 0;
38489
+ return xml.getElementsByTagName("w:tbl").length;
38490
+ } catch {
38491
+ return 0;
38492
+ }
38493
+ }
38494
+ function isCalloutTable(node, dims) {
38495
+ try {
38496
+ const xml = node.xml;
38497
+ if (!xml || typeof xml.getElementsByTagName !== "function") return false;
38498
+ if (dims) {
38499
+ if (dims.rows !== 1 || dims.cols !== 1) return false;
38500
+ } else {
38501
+ const rows = Array.from(xml.childNodes).filter((n) => n.nodeType === 1 && n.tagName === "w:tr");
38502
+ if (rows.length !== 1) return false;
38503
+ const cells = Array.from(rows[0].childNodes).filter((n) => n.nodeType === 1 && n.tagName === "w:tc");
38504
+ if (cells.length !== 1) return false;
38505
+ }
38506
+ const fill = xml.getElementsByTagName("w:shd")[0]?.getAttribute("w:fill") ?? "";
38507
+ return /^[0-9A-Fa-f]{6}$/.test(fill);
38508
+ } catch {
38509
+ return false;
38510
+ }
38511
+ }
38512
+ function countExplicitPageBreaks(node) {
38513
+ if (node.type === "pageBreak") return 1;
38514
+ const text2 = node.metadata?.text || "";
38515
+ let count = text2 === "[Page Break]" ? 1 : 0;
38516
+ try {
38517
+ const xml = node.xml;
38518
+ if (xml && typeof xml.getElementsByTagName === "function") {
38519
+ const brs = xml.getElementsByTagName("w:br");
38520
+ let brCount = 0;
38521
+ for (let i = 0; i < brs.length; i++) {
38522
+ if (brs[i].getAttribute("w:type") === "page") brCount++;
38523
+ }
38524
+ if (brCount > count) count = brCount;
38525
+ }
38526
+ } catch {
38527
+ }
38528
+ return count;
38529
+ }
38530
+ function computeDocStats(ast) {
38531
+ let words = 0;
38532
+ let paragraphs = 0;
38533
+ let tables = 0;
38534
+ let callouts = 0;
38535
+ let images = 0;
38536
+ let pageBreaks = 0;
38537
+ for (const sec of ast.sections) {
38538
+ for (const node of sec.nodes) {
38539
+ if (node.type === "paragraph") paragraphs += 1;
38540
+ if (node.type === "table") {
38541
+ if (isCalloutTable(node)) callouts += 1;
38542
+ else tables += 1;
38543
+ tables += nestedTableCount(node);
38544
+ }
38545
+ if (node.type === "image") images += 1;
38546
+ pageBreaks += countExplicitPageBreaks(node);
38547
+ let text2 = node.metadata?.text ?? "";
38548
+ if (node.type === "table") {
38549
+ const spaced = tableCellTextSpaced(node);
38550
+ if (spaced !== void 0) text2 = spaced;
38551
+ }
38552
+ const hits = text2.match(/\S+/g);
38553
+ if (hits) words += hits.length;
38554
+ }
38555
+ }
38556
+ const pageBudget = words / 450 + (tables + callouts) * 0.45 + images * 0.5 + pageBreaks;
38557
+ return { words, paragraphs, pages: Math.max(1, Math.ceil(pageBudget)), tables, callouts, images };
38558
+ }
38559
+
38078
38560
  // ../engine/src/visual/telemetry.ts
38079
38561
  var VISUAL_TELEMETRY_TOKEN_BUDGET = 500;
38080
38562
  var VISUAL_WIREFRAME_WIDTH = 68;
@@ -38171,17 +38653,6 @@ var WIREFRAME_TABLE_ROWS = 3;
38171
38653
  function flatText(value) {
38172
38654
  return value.replace(/\s+/g, " ").trim();
38173
38655
  }
38174
- function isCalloutTable(node, grid) {
38175
- if (!grid || grid.rows !== 1 || grid.cols !== 1) return false;
38176
- try {
38177
- const xml = node.xml;
38178
- const shd = xml?.getElementsByTagName("w:shd")[0];
38179
- const fill = shd?.getAttribute("w:fill") ?? "";
38180
- return /^[0-9A-Fa-f]{6}$/.test(fill);
38181
- } catch {
38182
- return false;
38183
- }
38184
- }
38185
38656
  function tableRowLines(grid, width) {
38186
38657
  const byRow = /* @__PURE__ */ new Map();
38187
38658
  for (const cell of grid.cells) {
@@ -38404,7 +38875,7 @@ function generateVisualWireframe(ast, targetPage = 1, pageMap, maxNodes = 24, op
38404
38875
  } else if (node.type === "table") {
38405
38876
  lines.push(emptyLine);
38406
38877
  const grid = tableGridOf(node);
38407
- if (isCalloutTable(node, grid)) {
38878
+ if (isCalloutTable(node, grid ? { rows: grid.rows, cols: grid.cols } : null)) {
38408
38879
  const note = flatText(grid?.cells[0]?.text ?? "");
38409
38880
  const body = note.length > 48 ? note.slice(0, 45) + "..." : note;
38410
38881
  lines.push(`| [CALLOUT] ${body}`.slice(0, W2 - 2).padEnd(W2 - 1) + "|");
@@ -38591,6 +39062,10 @@ function buildLineIndex(src, tokens) {
38591
39062
  }
38592
39063
  return out;
38593
39064
  }
39065
+ var QUOTE_INDENT = QUOTE_INDENT_TWIPS;
39066
+ var QUOTE_LIST_INDENT = 360;
39067
+ var QUOTE_MAX_LEVEL = 3;
39068
+ var CODE_BLOCK_BASE_INDENT = 80;
38594
39069
  function subLine(blockRaw, blockLine, excerpt) {
38595
39070
  const found = blockRaw.indexOf(excerpt);
38596
39071
  if (found === -1) return blockLine;
@@ -38762,12 +39237,17 @@ function compileMarkdownDetailed(markdown, options) {
38762
39237
  const pendingNotes = [];
38763
39238
  let seq = 0;
38764
39239
  let lastInsertId = null;
39240
+ const idPrefix = options?.idPrefix ?? "md";
38765
39241
  const nextId = () => {
38766
39242
  seq += 1;
38767
- return `md_${String(seq).padStart(4, "0")}`;
39243
+ return `${idPrefix}_${String(seq).padStart(4, "0")}`;
39244
+ };
39245
+ const startAnchor = {
39246
+ anchor: options?.anchor ?? "end",
39247
+ position: options?.position ?? "after"
38768
39248
  };
38769
39249
  const insertAnchor = () => {
38770
- if (lastInsertId === null) return { anchor: "end", position: "after" };
39250
+ if (lastInsertId === null) return { ...startAnchor };
38771
39251
  return { anchor: lastInsertId, position: "after" };
38772
39252
  };
38773
39253
  const pushInsertParagraph = (text2, spans) => {
@@ -38895,12 +39375,12 @@ function compileMarkdownDetailed(markdown, options) {
38895
39375
  const inlineMime = inlineKind === "jpeg" ? "image/jpeg" : inlineKind === "svg" ? "image/svg+xml" : inlineKind === "tiff" ? "image/tiff" : `image/${inlineKind}`;
38896
39376
  const inlineFitted = fitImageEmus(decodeImagePixels(inlineBuf, inlineKind));
38897
39377
  const inlineId = nextId();
38898
- const inlineAnchor = lastInsertId === null ? "end" : lastInsertId;
39378
+ const inlineAnchor = insertAnchor();
38899
39379
  ops.push({
38900
39380
  action: "insert",
38901
39381
  type: "image",
38902
- anchor: inlineAnchor,
38903
- position: "after",
39382
+ anchor: inlineAnchor.anchor,
39383
+ position: inlineAnchor.position,
38904
39384
  id: inlineId,
38905
39385
  fileData: `data:${inlineMime};base64,${inlineBuf.toString("base64")}`,
38906
39386
  ...inlineFitted ? { width: inlineFitted.width, height: inlineFitted.height } : {},
@@ -38909,7 +39389,7 @@ function compileMarkdownDetailed(markdown, options) {
38909
39389
  lastInsertId = inlineId;
38910
39390
  return { id: inlineId, kind: "image" };
38911
39391
  }
38912
- if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(target)) {
39392
+ if (hasUriScheme(target)) {
38913
39393
  fail(line2, "image reference points outside the allowed directory", "keep referenced files inside the allowed directory and use a relative path");
38914
39394
  }
38915
39395
  if (allowedDir === void 0) {
@@ -38918,14 +39398,17 @@ function compileMarkdownDetailed(markdown, options) {
38918
39398
  const isChart = target.toLowerCase().endsWith(CHART_JSON_SUFFIX);
38919
39399
  const absPath = resolveAssetPath(allowedDir, target, line2, isChart ? "chart" : "image");
38920
39400
  const id = nextId();
38921
- const anchorId = lastInsertId === null ? "end" : lastInsertId;
39401
+ const chartAnchor = insertAnchor();
38922
39402
  if (isChart) {
39403
+ if (chartAnchor.position === "before") {
39404
+ fail(line2, "a chart cannot be the first block when the compose goes before a node", "start the Markdown with a paragraph, or place the compose after the node");
39405
+ }
38923
39406
  const asset2 = readChartAsset(absPath, line2, allowedDir);
38924
39407
  const title = asset2.title ?? (alt.trim().length > 0 ? alt.trim() : "Chart");
38925
39408
  ops.push({
38926
39409
  action: "chart",
38927
39410
  operation: "insert",
38928
- anchor: anchorId,
39411
+ anchor: chartAnchor.anchor,
38929
39412
  id,
38930
39413
  chartType: asset2.chartType,
38931
39414
  title,
@@ -38936,7 +39419,7 @@ function compileMarkdownDetailed(markdown, options) {
38936
39419
  return { id, kind: "chart" };
38937
39420
  }
38938
39421
  const asset = readImageAsset(absPath, line2, allowedDir);
38939
- const anchor = lastInsertId === null ? { anchor: "end", position: "after" } : { anchor: lastInsertId, position: "after" };
39422
+ const anchor = insertAnchor();
38940
39423
  ops.push({
38941
39424
  action: "insert",
38942
39425
  type: "image",
@@ -38954,15 +39437,11 @@ function compileMarkdownDetailed(markdown, options) {
38954
39437
  const walkAlert = (list) => {
38955
39438
  for (const tok of list) {
38956
39439
  const t = tok;
38957
- if (t.type === "strong" || t.type === "em" || t.type === "codespan") {
38958
- fail(line2, "rich text inside alerts is out of scope", "move bold, italic, or code into a body paragraph");
38959
- }
38960
39440
  if (t.type === "link") {
38961
39441
  const raw = t.raw;
38962
39442
  if (typeof raw === "string" && raw.startsWith("<")) {
38963
39443
  fail(line2, "raw HTML is out of scope in an alert", "use plain Markdown blocks instead of HTML tags");
38964
39444
  }
38965
- fail(line2, "links inside alerts are out of scope", "move the link into a body paragraph");
38966
39445
  }
38967
39446
  if (t.type === "image") {
38968
39447
  fail(line2, "images are out of scope in an alert", "put images on their own block line instead");
@@ -38980,7 +39459,7 @@ function compileMarkdownDetailed(markdown, options) {
38980
39459
  if (alertTokens) walkAlert(alertTokens);
38981
39460
  };
38982
39461
  const handleAlertBlockquote = (token, line2) => {
38983
- const inner = token.tokens ?? [];
39462
+ const inner = (token.tokens ?? []).filter((child) => child.type !== "space");
38984
39463
  if (inner.length === 0) {
38985
39464
  fail(line2, "blockquotes are out of scope", "use a plain paragraph");
38986
39465
  }
@@ -39006,6 +39485,7 @@ ${rest}` : sameLineBody;
39006
39485
  }
39007
39486
  const restLines = rest.split("\n");
39008
39487
  let title;
39488
+ let titleSegments;
39009
39489
  let bodySrc = rest;
39010
39490
  if (restLines.length > 0 && ALERT_TITLE_PATTERN.test(restLines[0].trim())) {
39011
39491
  const innerTitle = (ALERT_TITLE_PATTERN.exec(restLines[0].trim())?.[1] ?? "").trim();
@@ -39028,6 +39508,22 @@ ${rest}` : sameLineBody;
39028
39508
  CITATION_REF_PATTERN.lastIndex = 0;
39029
39509
  title = innerTitle;
39030
39510
  bodySrc = restLines.slice(1).join("\n");
39511
+ let titleTokens;
39512
+ try {
39513
+ titleTokens = lexMarkdown(restLines[0].trim(), { dollarMath: options?.dollarMath });
39514
+ } catch (err) {
39515
+ const detail = err instanceof Error ? err.message : String(err);
39516
+ throw toError("E_VALIDATION", `line ${line2}: alert title rejected (${detail})`, "keep the alert title to one line of inline text");
39517
+ }
39518
+ const titleBlocks = titleTokens.filter((t) => t.type !== "space");
39519
+ if (titleBlocks.length !== 1 || titleBlocks[0].type !== "paragraph") {
39520
+ fail(line2, `alert [!${alertType}] title must be one line of inline text`, "keep the title to one bold line");
39521
+ }
39522
+ const titleInlineTokens = titleBlocks[0].tokens;
39523
+ failOnSpanTokensInAlert(titleInlineTokens, line2);
39524
+ const parsedTitle = parseInlineTokens(titleInlineTokens, line2, "an alert title", citationDefs, citationOrder, citationNum, usedCitations);
39525
+ title = parsedTitle.plainText.length > 0 ? parsedTitle.plainText : innerTitle;
39526
+ titleSegments = parsedTitle.segments;
39031
39527
  }
39032
39528
  for (const bodyLine of bodySrc.split("\n")) {
39033
39529
  if (CONTAINER_FENCE_PATTERN.test(bodyLine)) {
@@ -39048,6 +39544,7 @@ ${rest}` : sameLineBody;
39048
39544
  throw toError("E_VALIDATION", `line ${line2}: alert body rejected (${detail})`, "keep alert bodies plain text");
39049
39545
  }
39050
39546
  const cleanParas = [];
39547
+ const bodySegments = [];
39051
39548
  for (const bodyToken of bodyTokens) {
39052
39549
  if (bodyToken.type === "space") continue;
39053
39550
  if (bodyToken.type !== "paragraph") {
@@ -39063,31 +39560,214 @@ ${rest}` : sameLineBody;
39063
39560
  FOOTNOTE_REF_PATTERN.lastIndex = 0;
39064
39561
  failOnImageOrHtml(paraText2, paraLine, "an alert");
39065
39562
  failOnSpanTokensInAlert(para.tokens, paraLine);
39066
- cleanParas.push(plainJoin(replaceCitationsInPlainCell(paraText2, paraLine)));
39563
+ const parsedPara = parseInlineTokens(para.tokens, paraLine, "an alert", citationDefs, citationOrder, citationNum, usedCitations);
39564
+ if (parsedPara.footnoteRefs.length > 0) {
39565
+ fail(paraLine, "footnote markers inside alerts are out of scope", "move the note reference into a body paragraph");
39566
+ }
39567
+ for (const key of parsedPara.citationKeys) usedCitations.add(key);
39568
+ if (cleanParas.length > 0) bodySegments.push({ text: " ", bold: false, italic: false, code: false });
39569
+ bodySegments.push(...parsedPara.segments);
39570
+ cleanParas.push(parsedPara.plainText);
39067
39571
  }
39068
39572
  const text2 = plainJoin(cleanParas.join(" "));
39069
39573
  if (text2.length === 0) {
39070
39574
  fail(line2, `alert [!${alertType}] needs body text`, "write the alert text on > lines after the marker");
39071
39575
  }
39072
- const anchorId = lastInsertId === null ? "end" : lastInsertId;
39576
+ const calloutAnchor = insertAnchor();
39073
39577
  const alertTag = alertType.toLowerCase();
39074
39578
  const tagThemeRaw = styleGuide?.callouts?.tagThemes?.[alertTag];
39075
39579
  const tagTheme = typeof tagThemeRaw === "string" && tagThemeRaw.length > 0 ? tagThemeRaw : void 0;
39076
39580
  const guideThemeRaw = styleGuide?.callouts?.defaultTheme;
39077
39581
  const guideTheme = typeof guideThemeRaw === "string" && guideThemeRaw.length > 0 ? guideThemeRaw : void 0;
39582
+ const calloutId = nextId();
39078
39583
  const op = {
39079
39584
  action: "callout",
39080
- anchor: anchorId,
39081
- position: "after",
39585
+ id: calloutId,
39586
+ anchor: calloutAnchor.anchor,
39587
+ position: calloutAnchor.position,
39082
39588
  text: text2,
39083
39589
  theme: tagTheme ?? guideTheme ?? ALERT_TYPE_TO_THEME[alertType] ?? "indigo"
39084
39590
  };
39085
39591
  if (title !== void 0) op["title"] = title;
39592
+ const spanParams = {};
39593
+ if (titleSegments !== void 0 && segmentsNeedSplit(titleSegments)) spanParams["mdTitleSpans"] = titleSegments;
39594
+ if (segmentsNeedSplit(bodySegments)) spanParams["mdBodySpans"] = bodySegments;
39595
+ if (Object.keys(spanParams).length > 0) op["params"] = spanParams;
39086
39596
  ops.push(op);
39087
- lastInsertId = null;
39597
+ lastInsertId = calloutId;
39088
39598
  };
39089
39599
  let underTitle = false;
39090
39600
  let lastBlockKind = "none";
39601
+ const compileList = (token, line2, quoteLevel = 0) => {
39602
+ const listType = token.ordered === true ? "numbered" : "bullet";
39603
+ const items = token.items ?? [];
39604
+ if (items.length === 0) {
39605
+ fail(line2, "empty list block", "add at least one list item");
39606
+ }
39607
+ const itemIds = [];
39608
+ const itemBaseLine = line2;
39609
+ for (const item of items) {
39610
+ const itemLine = subLine(token.raw, itemBaseLine, item.raw);
39611
+ if (item.task === true) {
39612
+ fail(itemLine, "task-list items are out of scope", "use a plain bullet or numbered item");
39613
+ }
39614
+ for (const child of item.tokens ?? []) {
39615
+ if (child.type === "list") {
39616
+ const nestedRaw = child.raw;
39617
+ const nestedLine = typeof nestedRaw === "string" && nestedRaw ? subLine(token.raw, itemBaseLine, nestedRaw) : itemLine;
39618
+ fail(nestedLine, "nested lists are out of scope", "flatten nested items into the top-level list");
39619
+ }
39620
+ if (child.type !== "text" && child.type !== "paragraph" && child.type !== "space") {
39621
+ fail(itemLine, `list items carry ${child.type} blocks, out of scope`, "keep list items plain text");
39622
+ }
39623
+ }
39624
+ if (item.text.split("\n").some((entry) => FOOTNOTE_DEF_PATTERN.test(entry))) {
39625
+ fail(itemLine, "footnote definition must stand alone as its own block", "move the definition out of the list");
39626
+ }
39627
+ if (item.text.split("\n").some((entry) => CITATION_DEF_PATTERN.test(entry))) {
39628
+ fail(itemLine, "citation definition must stand alone as its own block", "move the definition out of the list");
39629
+ }
39630
+ failOnImageOrHtml(item.text, itemLine, "a list item");
39631
+ const firstChild = (item.tokens ?? [])[0];
39632
+ const inlineTokens = firstChild?.tokens ?? [];
39633
+ if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
39634
+ fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39635
+ }
39636
+ for (const it of inlineTokens) {
39637
+ if (it.type === "image") {
39638
+ fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39639
+ }
39640
+ }
39641
+ const inline = parseInlineTokens(inlineTokens, itemLine, "a list item", citationDefs, citationOrder, citationNum, usedCitations);
39642
+ for (const key of inline.citationKeys) usedCitations.add(key);
39643
+ for (const label of inline.footnoteRefs) {
39644
+ if (!defs.has(label)) {
39645
+ fail(itemLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39646
+ }
39647
+ }
39648
+ const id = pushInsertParagraph(inline.plainText, inline.segments);
39649
+ trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, itemLine);
39650
+ itemIds.push(id);
39651
+ }
39652
+ ops.push({ action: "list", operation: "create", target: itemIds, listType });
39653
+ if (lastBlockKind === "table" && itemIds.length > 0) {
39654
+ ops.push({ action: "style", target: itemIds[0], spacingBefore: 240 });
39655
+ }
39656
+ if (quoteLevel > 0) {
39657
+ for (const itemId of itemIds) {
39658
+ ops.push({ action: "namedStyle", operation: "apply", styleName: "Quote", target: itemId });
39659
+ ops.push({ action: "style", target: itemId, indentLeft: quoteLevel * QUOTE_INDENT + QUOTE_LIST_INDENT, indentHanging: QUOTE_LIST_INDENT });
39660
+ }
39661
+ }
39662
+ };
39663
+ const compileCode = (token, line2, quoteLevel = 0) => {
39664
+ const lang = (token.lang ?? "").trim();
39665
+ if (CHART_FENCE_LANGUAGES.has(lang.toLowerCase())) {
39666
+ fail(line2, `fenced code tagged '${lang}' requests a chart`, "describe the chart with a chart path reference instead");
39667
+ }
39668
+ const id = nextId();
39669
+ const anchor = insertAnchor();
39670
+ ops.push({
39671
+ action: "insert",
39672
+ type: "paragraph",
39673
+ anchor: anchor.anchor,
39674
+ position: anchor.position,
39675
+ text: token.text ?? "",
39676
+ id,
39677
+ params: { codeBlock: true, ...lang ? { codeLanguage: lang } : {} }
39678
+ });
39679
+ lastInsertId = id;
39680
+ if (lastBlockKind === "table") {
39681
+ ops.push({ action: "style", target: id, spacingBefore: 240 });
39682
+ }
39683
+ if (quoteLevel > 0) {
39684
+ ops.push({ action: "style", target: id, indentLeft: quoteLevel * QUOTE_INDENT + CODE_BLOCK_BASE_INDENT });
39685
+ }
39686
+ };
39687
+ const compileQuote = (token, line2, level) => {
39688
+ const effective = Math.min(level, QUOTE_MAX_LEVEL);
39689
+ if (level > QUOTE_MAX_LEVEL) {
39690
+ warnings.push(`line ${line2}: quote nesting deeper than ${QUOTE_MAX_LEVEL} levels was written at level ${QUOTE_MAX_LEVEL}.`);
39691
+ }
39692
+ const children2 = (token.tokens ?? []).filter((child) => child.type !== "space");
39693
+ if (children2.length === 0) {
39694
+ fail(line2, "blockquote is empty", "write the quote text after the > marker");
39695
+ }
39696
+ const strippedRaw = token.raw.split("\n").map((entry) => entry.replace(/^ {0,3}>[ ]?/, "")).join("\n");
39697
+ for (const child of children2) {
39698
+ const childRaw = child.raw ?? "";
39699
+ const childLine = subLine(strippedRaw, line2, childRaw.split("\n")[0] ?? "");
39700
+ switch (child.type) {
39701
+ case "paragraph": {
39702
+ const para = child;
39703
+ const paraText2 = para.text ?? "";
39704
+ const paraLines = childRaw.replace(/\n+$/, "").split("\n");
39705
+ if (paraLines.some((entry) => FOOTNOTE_DEF_PATTERN.test(entry) || CITATION_DEF_PATTERN.test(entry))) {
39706
+ fail(childLine, "note and citation definitions must stand alone as their own block", "move the definition out of the quote");
39707
+ }
39708
+ if (CONTAINER_FENCE_PATTERN.test(paraLines[0] ?? "")) {
39709
+ fail(childLine, "container fences (:::) are out of scope as custom styles", "use plain Markdown blocks");
39710
+ }
39711
+ const paraTokens = para.tokens ?? [];
39712
+ for (const it of paraTokens) {
39713
+ if (it.type === "image") {
39714
+ fail(childLine, "images are out of scope in a quote", "put images on their own block line after the quote");
39715
+ }
39716
+ }
39717
+ failOnImageOrHtml(paraText2, childLine, "a quote");
39718
+ const inline = parseInlineTokens(para.tokens, childLine, "a quote", citationDefs, citationOrder, citationNum, usedCitations);
39719
+ for (const key of inline.citationKeys) usedCitations.add(key);
39720
+ for (const label of inline.footnoteRefs) {
39721
+ if (!defs.has(label)) {
39722
+ fail(childLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39723
+ }
39724
+ }
39725
+ const id = pushInsertParagraph(inline.plainText, inline.segments);
39726
+ trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, childLine);
39727
+ ops.push({ action: "namedStyle", operation: "apply", styleName: "Quote", target: id });
39728
+ if (effective > 1) {
39729
+ ops.push({ action: "style", target: id, indentLeft: effective * QUOTE_INDENT });
39730
+ }
39731
+ if (lastBlockKind === "table") {
39732
+ ops.push({ action: "style", target: id, spacingBefore: 240 });
39733
+ }
39734
+ lastBlockKind = "quote";
39735
+ underTitle = false;
39736
+ break;
39737
+ }
39738
+ case "blockquote":
39739
+ compileQuote(child, childLine, level + 1);
39740
+ break;
39741
+ case "list":
39742
+ compileList(child, childLine, effective);
39743
+ lastBlockKind = "list";
39744
+ break;
39745
+ case "code":
39746
+ compileCode(child, childLine, effective);
39747
+ lastBlockKind = "code";
39748
+ break;
39749
+ case "table":
39750
+ fail(childLine, "tables inside a quote are out of scope", "Word cannot indent a table as a quote: put the table after the quote");
39751
+ break;
39752
+ case "heading":
39753
+ fail(childLine, "headings inside a quote are out of scope", "put the heading before the quote");
39754
+ break;
39755
+ default:
39756
+ fail(childLine, `${child.type} blocks inside a quote are out of scope`, "keep quotes to paragraphs, flat lists and code blocks");
39757
+ }
39758
+ }
39759
+ };
39760
+ const quoteOpensWithAlertMarker = (token, line2) => {
39761
+ const first = (token.tokens ?? []).find((child) => child.type !== "space");
39762
+ if (!first || first.type !== "paragraph") return false;
39763
+ const head = (first.text ?? "").split("\n")[0]?.trim() ?? "";
39764
+ if (ALERT_MARKER_PATTERN.test(head)) return true;
39765
+ const unknown = /^\[!([A-Za-z][A-Za-z0-9_-]*)\]/.exec(head);
39766
+ if (unknown) {
39767
+ fail(line2, `unknown alert type [!${unknown[1]}]`, "use NOTE, TIP, IMPORTANT, WARNING or CAUTION, or drop the [!...] marker for a plain quote");
39768
+ }
39769
+ return false;
39770
+ };
39091
39771
  for (const rawToken of tokens) {
39092
39772
  const token = rawToken;
39093
39773
  const line2 = lineOf.get(rawToken) ?? 1;
@@ -39197,60 +39877,7 @@ ${rest}` : sameLineBody;
39197
39877
  break;
39198
39878
  }
39199
39879
  case "list": {
39200
- const listType = token.ordered === true ? "numbered" : "bullet";
39201
- const items = token.items ?? [];
39202
- if (items.length === 0) {
39203
- fail(line2, "empty list block", "add at least one list item");
39204
- }
39205
- const itemIds = [];
39206
- const itemBaseLine = line2;
39207
- for (const item of items) {
39208
- const itemLine = subLine(token.raw, itemBaseLine, item.raw);
39209
- if (item.task === true) {
39210
- fail(itemLine, "task-list items are out of scope", "use a plain bullet or numbered item");
39211
- }
39212
- for (const child of item.tokens ?? []) {
39213
- if (child.type === "list") {
39214
- const nestedRaw = child.raw;
39215
- const nestedLine = typeof nestedRaw === "string" && nestedRaw ? subLine(token.raw, itemBaseLine, nestedRaw) : itemLine;
39216
- fail(nestedLine, "nested lists are out of scope", "flatten nested items into the top-level list");
39217
- }
39218
- if (child.type !== "text" && child.type !== "paragraph" && child.type !== "space") {
39219
- fail(itemLine, `list items carry ${child.type} blocks, out of scope`, "keep list items plain text");
39220
- }
39221
- }
39222
- if (item.text.split("\n").some((entry) => FOOTNOTE_DEF_PATTERN.test(entry))) {
39223
- fail(itemLine, "footnote definition must stand alone as its own block", "move the definition out of the list");
39224
- }
39225
- if (item.text.split("\n").some((entry) => CITATION_DEF_PATTERN.test(entry))) {
39226
- fail(itemLine, "citation definition must stand alone as its own block", "move the definition out of the list");
39227
- }
39228
- failOnImageOrHtml(item.text, itemLine, "a list item");
39229
- const firstChild = (item.tokens ?? [])[0];
39230
- const inlineTokens = firstChild?.tokens ?? [];
39231
- if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
39232
- fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39233
- }
39234
- for (const it of inlineTokens) {
39235
- if (it.type === "image") {
39236
- fail(itemLine, "images are out of scope in a list item", "put images on their own block line instead of inside a list");
39237
- }
39238
- }
39239
- const inline = parseInlineTokens(inlineTokens, itemLine, "a list item", citationDefs, citationOrder, citationNum, usedCitations);
39240
- for (const key of inline.citationKeys) usedCitations.add(key);
39241
- for (const label of inline.footnoteRefs) {
39242
- if (!defs.has(label)) {
39243
- fail(itemLine, `undefined footnote reference [^${label}]`, "add a matching [^label]: body definition");
39244
- }
39245
- }
39246
- const id = pushInsertParagraph(inline.plainText, inline.segments);
39247
- trackRefs(id, inline.footnoteEntries ?? inline.footnoteRefs, itemLine);
39248
- itemIds.push(id);
39249
- }
39250
- ops.push({ action: "list", operation: "create", target: itemIds, listType });
39251
- if (lastBlockKind === "table" && itemIds.length > 0) {
39252
- ops.push({ action: "style", target: itemIds[0], spacingBefore: 240 });
39253
- }
39880
+ compileList(token, line2);
39254
39881
  lastBlockKind = "list";
39255
39882
  break;
39256
39883
  }
@@ -39363,25 +39990,7 @@ ${rest}` : sameLineBody;
39363
39990
  break;
39364
39991
  }
39365
39992
  case "code": {
39366
- const lang = (token.lang ?? "").trim();
39367
- if (CHART_FENCE_LANGUAGES.has(lang.toLowerCase())) {
39368
- fail(line2, `fenced code tagged '${lang}' requests a chart`, "describe the chart with a chart path reference instead");
39369
- }
39370
- const id = nextId();
39371
- const anchor = insertAnchor();
39372
- ops.push({
39373
- action: "insert",
39374
- type: "paragraph",
39375
- anchor: anchor.anchor,
39376
- position: anchor.position,
39377
- text: token.text ?? "",
39378
- id,
39379
- params: { codeBlock: true, ...lang ? { codeLanguage: lang } : {} }
39380
- });
39381
- lastInsertId = id;
39382
- if (lastBlockKind === "table") {
39383
- ops.push({ action: "style", target: id, spacingBefore: 240 });
39384
- }
39993
+ compileCode(token, line2);
39385
39994
  lastBlockKind = "code";
39386
39995
  break;
39387
39996
  }
@@ -39400,8 +40009,12 @@ ${rest}` : sameLineBody;
39400
40009
  fail(line2, "raw HTML blocks are out of scope", "use plain Markdown blocks instead of HTML tags");
39401
40010
  break;
39402
40011
  case "blockquote":
39403
- handleAlertBlockquote(token, line2);
39404
- lastBlockKind = "callout";
40012
+ if (quoteOpensWithAlertMarker(token, line2)) {
40013
+ handleAlertBlockquote(token, line2);
40014
+ lastBlockKind = "callout";
40015
+ } else {
40016
+ compileQuote(token, line2, 1);
40017
+ }
39405
40018
  break;
39406
40019
  case "image": {
39407
40020
  const img = token;
@@ -39485,107 +40098,15 @@ ${rest}` : sameLineBody;
39485
40098
  }
39486
40099
 
39487
40100
  // ../engine/src/compose/markdown.ts
39488
- function pristineTemplateSeedIds(parser2, requestedTemplate) {
39489
- if (!parser2.hasPristineBuiltinSeed()) return null;
39490
- let summary;
39491
- try {
39492
- summary = parser2.getStructuralSummary();
39493
- } catch {
39494
- return null;
39495
- }
39496
- const nodes = summary.nodes ?? [];
39497
- if (![1, 2, 4, 5, 6].includes(nodes.length)) return null;
39498
- if (summary.hasTrackedChanges === true) return null;
39499
- if (Object.keys(summary.mediaFiles ?? {}).length > 0) return null;
39500
- if ((summary.headers ?? []).length > 0) return null;
39501
- if ((summary.footers ?? []).length > 0) return null;
39502
- if ((summary.charts ?? []).length > 0) return null;
39503
- try {
39504
- const zip = parser2.getZipAdapter?.();
39505
- if (typeof zip?.hasFile === "function") {
39506
- for (const part of [
39507
- "word/footnotes.xml",
39508
- "word/endnotes.xml",
39509
- "word/comments.xml",
39510
- "word/commentsExtended.xml"
39511
- ]) {
39512
- try {
39513
- if (zip.hasFile(part)) return null;
39514
- } catch {
39515
- return null;
39516
- }
39517
- }
39518
- } else {
39519
- return null;
39520
- }
39521
- } catch {
39522
- return null;
39523
- }
39524
- for (const n of nodes) {
39525
- if (n.hasTrackedChanges === true) return null;
39526
- const raw = n.rawXml ?? "";
39527
- if (raw.includes("w:footnoteReference") || raw.includes("w:endnoteReference") || raw.includes("w:commentReference") || raw.includes("w:commentRangeStart") || raw.includes("w:commentRangeEnd")) {
39528
- return null;
39529
- }
39530
- }
39531
- const ids = nodes.map((n) => n.id ?? "").filter((id) => id.length > 0);
39532
- if (ids.length !== nodes.length) return null;
39533
- if (nodes.length === 1) {
39534
- const node = nodes[0];
39535
- if (node.type !== "paragraph") return null;
39536
- if ((node.text ?? "").trim() !== "") return null;
39537
- return ids;
39538
- }
39539
- if (nodes.length === 2) {
39540
- const [first, second] = nodes;
39541
- const t0 = (first.text ?? "").trim();
39542
- const t1 = (second.text ?? "").trim();
39543
- if (t0 === BLANK_SEED_TITLE && t1 === BLANK_SEED_HINT) {
39544
- const firstRaw = first.rawXml ?? "";
39545
- const secondRaw = second.rawXml ?? "";
39546
- if (!firstRaw.includes('w:val="Title"') || !secondRaw.includes('w:val="Subtitle"')) return null;
39547
- return ids;
39548
- }
39549
- const tmpl = requestedTemplate?.toLowerCase();
39550
- if (t0 === "MEMORANDUM" && tmpl === "memo") return ids;
39551
- if (t0 === "Letter" && tmpl === "letter") return ids;
39552
- if (t0 === "Proposal" && tmpl === "proposal") return ids;
39553
- if (t0 === "INVOICE" && tmpl === "invoice") return ids;
39554
- return null;
39555
- }
39556
- if (nodes.length === 4) {
39557
- const t0 = (nodes[0].text ?? "").trim();
39558
- const tmpl = requestedTemplate?.toLowerCase();
39559
- if (t0 === "Research Benchmark Report" && tmpl === "research") return ids;
39560
- if (t0 === "Corporate Strategy Roadmap" && tmpl === "corporate") return ids;
39561
- if (t0 === "Velocity Audit & Performance" && tmpl === "bar-chart-3d") return ids;
39562
- return null;
39563
- }
39564
- if (nodes.length === 5) {
39565
- const t0 = (nodes[0].text ?? "").trim();
39566
- const tmpl = requestedTemplate?.toLowerCase();
39567
- if (t0 === "Financial Report & Audit" && tmpl === "financial") return ids;
39568
- return null;
39569
- }
39570
- if (nodes.length === 6) {
39571
- const t0 = (nodes[0].text ?? "").trim();
39572
- const t2 = (nodes[2].text ?? "").trim();
39573
- const tmpl = requestedTemplate?.toLowerCase();
39574
- if (t0 === "Q3 Executive Financial Performance Report" && t2 === "1. Executive Summary") {
39575
- if (tmpl === "report") return ids;
39576
- return null;
39577
- }
39578
- if (t0 === "Manuscript Title" && t2 === "Chapter 1: The Beginning" && tmpl === "novel") {
39579
- return ids;
39580
- }
39581
- if (t0 === "Neural Document Architectures: A Comparative Benchmark" && tmpl === "academic") {
39582
- return ids;
39583
- }
39584
- if (t0 === "TECH HORIZONS" && tmpl === "newsletter") return ids;
39585
- if (t0 === "Platform Core API Reference" && (tmpl === "api" || tmpl === "api-spec")) return ids;
39586
- return null;
39587
- }
39588
- return null;
40101
+ function nextComposeIdPrefix(ast) {
40102
+ const live = /* @__PURE__ */ new Set();
40103
+ for (const section of ast.sections ?? []) for (const node of section.nodes ?? []) live.add(node.id);
40104
+ const used = (prefix) => {
40105
+ for (const id of live) if (id.startsWith(`${prefix}_`)) return true;
40106
+ return false;
40107
+ };
40108
+ if (!used("md")) return "md";
40109
+ for (let n = 2; ; n++) if (!used(`md${n}`)) return `md${n}`;
39589
40110
  }
39590
40111
  var pristineBlankSeedIds = pristineTemplateSeedIds;
39591
40112
  function assertTemplateSeed(parser2, template) {
@@ -39629,15 +40150,26 @@ function assertTemplateSeed(parser2, template) {
39629
40150
  function applyInlineSpans(parser2, ast, operations) {
39630
40151
  const docParser = parser2;
39631
40152
  const liveAst = ast;
40153
+ const targets = [];
39632
40154
  for (const op of operations) {
39633
40155
  const rec = op;
39634
- if (rec["action"] !== "insert" || rec["type"] !== "paragraph") continue;
39635
40156
  const params = rec["params"];
39636
- const rawSpans = params?.["mdSpans"];
39637
- if (!Array.isArray(rawSpans) || !segmentsNeedSplit(rawSpans)) continue;
39638
- const spans = rawSpans;
39639
40157
  const id = String(rec["id"] ?? "");
39640
40158
  if (!id) continue;
40159
+ if (rec["action"] === "callout") {
40160
+ const titleSpans = params?.["mdTitleSpans"];
40161
+ const bodySpans = params?.["mdBodySpans"];
40162
+ if (Array.isArray(titleSpans) && segmentsNeedSplit(titleSpans)) targets.push({ id, spans: titleSpans, part: "calloutTitle" });
40163
+ if (Array.isArray(bodySpans) && segmentsNeedSplit(bodySpans)) targets.push({ id, spans: bodySpans, part: "calloutBody" });
40164
+ continue;
40165
+ }
40166
+ if (rec["action"] !== "insert" || rec["type"] !== "paragraph") continue;
40167
+ const rawSpans = params?.["mdSpans"];
40168
+ if (!Array.isArray(rawSpans) || !segmentsNeedSplit(rawSpans)) continue;
40169
+ targets.push({ id, spans: rawSpans, part: "paragraph" });
40170
+ }
40171
+ for (const target of targets) {
40172
+ const { id, spans, part } = target;
39641
40173
  let node;
39642
40174
  for (const section of liveAst.sections) {
39643
40175
  const found = section.nodes.find((n) => n.id === id);
@@ -39651,7 +40183,18 @@ function applyInlineSpans(parser2, ast, operations) {
39651
40183
  `E_VALIDATION: line 1: span target paragraph '${id}' is missing after compose. Remedy: retry the compose without manual edits in between.`
39652
40184
  );
39653
40185
  }
39654
- const para = node.xml;
40186
+ let para = node.xml;
40187
+ let baseRPr;
40188
+ if (part !== "paragraph") {
40189
+ const cellParas = Array.from(para.getElementsByTagName("w:p"));
40190
+ const hasTitle = cellParas.length > 1;
40191
+ const picked = part === "calloutTitle" ? hasTitle ? cellParas[0] : void 0 : cellParas[cellParas.length - 1];
40192
+ if (!picked) continue;
40193
+ para = picked;
40194
+ const firstRun = Array.from(para.childNodes).find((k) => k.nodeType === 1 && k.tagName === "w:r");
40195
+ const rPrEl = firstRun ? Array.from(firstRun.childNodes).find((k) => k.nodeType === 1 && k.tagName === "w:rPr") : void 0;
40196
+ baseRPr = rPrEl ? rPrEl : void 0;
40197
+ }
39655
40198
  const doc = para.ownerDocument;
39656
40199
  if (!doc) continue;
39657
40200
  const keptFootRefs = [];
@@ -39683,10 +40226,12 @@ function applyInlineSpans(parser2, ast, operations) {
39683
40226
  }
39684
40227
  const buildRun = (seg) => {
39685
40228
  const run2 = createWordElement(doc, "w:r");
39686
- const needsRPr = seg.bold || seg.italic || seg.strike || seg.code || seg.linkUrl !== void 0;
40229
+ const needsRPr = seg.bold || seg.italic || seg.strike || seg.code || seg.linkUrl !== void 0 || baseRPr !== void 0;
39687
40230
  if (needsRPr) {
39688
40231
  const rPr = createWordElement(doc, "w:rPr");
39689
- if (seg.bold) rPr.appendChild(createWordElement(doc, "w:b"));
40232
+ const baseBold = baseRPr !== void 0 && baseRPr.getElementsByTagName("w:b").length > 0;
40233
+ const baseColor = baseRPr?.getElementsByTagName("w:color")[0]?.getAttribute("w:val") ?? void 0;
40234
+ if (seg.bold || baseBold) rPr.appendChild(createWordElement(doc, "w:b"));
39690
40235
  if (seg.italic) rPr.appendChild(createWordElement(doc, "w:i"));
39691
40236
  if (seg.strike) rPr.appendChild(createWordElement(doc, "w:strike"));
39692
40237
  if (seg.code) {
@@ -39695,6 +40240,11 @@ function applyInlineSpans(parser2, ast, operations) {
39695
40240
  rFonts.setAttribute("w:hAnsi", COMPOSE_CODE_FONT);
39696
40241
  rPr.appendChild(rFonts);
39697
40242
  }
40243
+ if (seg.linkUrl === void 0 && baseColor) {
40244
+ const color = createWordElement(doc, "w:color");
40245
+ color.setAttribute("w:val", baseColor);
40246
+ rPr.appendChild(color);
40247
+ }
39698
40248
  if (seg.linkUrl !== void 0) {
39699
40249
  const color = createWordElement(doc, "w:color");
39700
40250
  color.setAttribute("w:val", "0066CC");
@@ -39780,35 +40330,76 @@ function applyInlineSpans(parser2, ast, operations) {
39780
40330
  );
39781
40331
  for (let i = 0; i < refSibs.length; i++) {
39782
40332
  const refEl = refSibs[i];
39783
- const op2 = noteOps[i];
39784
- insertRunAtOffset(para, refEl, op2?.offset);
40333
+ const op = noteOps[i];
40334
+ insertRunAtOffset(para, refEl, op?.offset);
39785
40335
  }
39786
40336
  node.version += 1;
39787
40337
  }
39788
40338
  }
40339
+ function validateComposedOperations(operations) {
40340
+ validateOperationsStrict(
40341
+ operations.map((op) => {
40342
+ const rec = op;
40343
+ const params = rec["params"];
40344
+ if (rec["action"] !== "callout" || !params) return op;
40345
+ const { mdTitleSpans: _t, mdBodySpans: _b, ...publicParams } = params;
40346
+ void _t;
40347
+ void _b;
40348
+ const { params: _p, ...rest } = rec;
40349
+ void _p;
40350
+ return Object.keys(publicParams).length > 0 ? { ...rest, params: publicParams } : rest;
40351
+ })
40352
+ );
40353
+ }
39789
40354
  async function composeDocument(parser2, markdown, options) {
39790
40355
  if (!parser2 || typeof parser2.getAST !== "function") {
39791
40356
  throw new Error(
39792
40357
  "E_VALIDATION: line 1: composeDocument needs an open document parser. Remedy: open a document first, then compose into it."
39793
40358
  );
39794
40359
  }
40360
+ const liveAst = await parser2.getAST();
40361
+ const idPrefix = options?.idPrefix ?? nextComposeIdPrefix(liveAst);
39795
40362
  if (options?.template !== void 0) {
40363
+ if (idPrefix !== "md") {
40364
+ throw new Error(
40365
+ "E_VALIDATION: line 1: template is accepted only on the first compose of a document. Remedy: omit template on this compose; the document already holds composed content."
40366
+ );
40367
+ }
39796
40368
  assertTemplateSeed(parser2, String(options.template));
39797
40369
  }
40370
+ if (options?.position !== void 0 && options.anchor === void 0) {
40371
+ throw new Error(
40372
+ "E_VALIDATION: line 1: position needs an anchor. Remedy: pass anchor (a node id, start or end) together with position, or omit position to append at the end."
40373
+ );
40374
+ }
40375
+ if (options?.anchor !== void 0 && anchorAliasOf(options.anchor) === null) {
40376
+ const known = liveAst.sections.some((section) => section.nodes.some((node) => node.id === options.anchor));
40377
+ if (!known) {
40378
+ throw new Error(
40379
+ `E_VALIDATION: line 1: anchor '${options.anchor}' is not a node of this document. Remedy: pass a node id from inspect_document, or start or end.`
40380
+ );
40381
+ }
40382
+ }
39798
40383
  const styleGuide = resolveComposeStyleGuide(options?.styleGuide);
39799
40384
  const { operations, warnings: compileWarnings } = compileMarkdownDetailed(markdown, {
40385
+ idPrefix,
40386
+ ...options?.anchor !== void 0 ? { anchor: options.anchor } : {},
40387
+ ...options?.position !== void 0 ? { position: options.position } : {},
39800
40388
  allowedDir: options?.allowedDir,
39801
40389
  ...options?.styleGuide !== void 0 ? { styleGuide: options.styleGuide } : {},
39802
40390
  ...options?.dollarMath ? { dollarMath: true } : {}
39803
40391
  });
39804
- validateOperationsStrict(operations);
40392
+ validateComposedOperations(operations);
40393
+ if (operations.some((op) => op.action === "namedStyle" && op.styleName === QUOTE_STYLE_ID)) {
40394
+ ensureQuoteStyle(parser2.getStylesDom());
40395
+ }
39805
40396
  const chunkSize = options?.chunkSize !== void 0 ? Math.floor(options.chunkSize) : ENGINE_MAX_OPS;
39806
40397
  if (!Number.isInteger(chunkSize) || chunkSize < 1 || chunkSize > ENGINE_MAX_OPS) {
39807
40398
  throw new Error(
39808
40399
  `E_VALIDATION: line 1: chunkSize must be an integer in [1, ${ENGINE_MAX_OPS}]. Remedy: omit chunkSize or pass a value inside that range.`
39809
40400
  );
39810
40401
  }
39811
- const ast = await parser2.getAST();
40402
+ const ast = liveAst;
39812
40403
  const blankSeedIds = pristineBlankSeedIds(parser2, options?.template);
39813
40404
  let preBatchSnapshot = null;
39814
40405
  try {
@@ -39861,7 +40452,17 @@ async function composeDocument(parser2, markdown, options) {
39861
40452
  }
39862
40453
  base += chunk.length;
39863
40454
  }
39864
- if (!aggregate.success) return aggregate;
40455
+ if (!aggregate.success) {
40456
+ if (aggregate.appliedCount > 0 && preBatchSnapshot !== null) {
40457
+ try {
40458
+ parser2.restorePackageSnapshot(preBatchSnapshot);
40459
+ aggregate.appliedCount = 0;
40460
+ aggregate.rollbackTriggered = true;
40461
+ } catch {
40462
+ }
40463
+ }
40464
+ return aggregate;
40465
+ }
39865
40466
  if (blankSeedIds !== null) {
39866
40467
  const seedCleanup = await engine.executeBatch({
39867
40468
  operations: [{ action: "delete", target: blankSeedIds }],
@@ -39932,119 +40533,19 @@ async function composeDocument(parser2, markdown, options) {
39932
40533
  if (/^E_VALIDATION/.test(detail)) throw err;
39933
40534
  throw new Error(`E_VALIDATION: line 1: inline formatting failed (${detail}). Remedy: simplify bold, italic, code, or link spans and retry.`);
39934
40535
  }
39935
- return aggregate;
39936
- }
39937
-
39938
- // ../engine/src/stats.ts
39939
- function nearestOwner(el, tagName) {
39940
- let cur = el.parentNode;
39941
- while (cur && typeof cur === "object") {
39942
- const e = cur;
39943
- if (e.tagName === tagName) return e;
39944
- cur = e.parentNode;
39945
- }
39946
- return null;
39947
- }
39948
- function tableCellTextSpaced(node) {
39949
- try {
39950
- const xml = node.xml;
39951
- if (!xml || typeof xml.getElementsByTagName !== "function") return void 0;
39952
- const tcs = xml.getElementsByTagName("w:tc");
39953
- if (!tcs || tcs.length === 0) return void 0;
39954
- const cells = [];
39955
- for (let i = 0; i < tcs.length; i++) {
39956
- const tc = tcs[i];
39957
- const paras = tc.getElementsByTagName("w:p");
39958
- if (paras.length > 0) {
39959
- const parts = [];
39960
- for (let p = 0; p < paras.length; p++) {
39961
- const para = paras[p];
39962
- if (nearestOwner(para, "w:tc") !== tc) continue;
39963
- const runs = para.getElementsByTagName("w:t");
39964
- let t = "";
39965
- for (let r = 0; r < runs.length; r++) {
39966
- const run2 = runs[r];
39967
- if (nearestOwner(run2, "w:tc") !== tc) continue;
39968
- if (nearestOwner(run2, "w:p") !== para) continue;
39969
- t += run2.textContent || "";
39970
- }
39971
- if (t.trim().length > 0) parts.push(t.trim());
39972
- }
39973
- if (parts.length > 0) cells.push(parts.join(" "));
39974
- } else {
39975
- const runs = tc.getElementsByTagName("w:t");
39976
- let t = "";
39977
- for (let r = 0; r < runs.length; r++) {
39978
- const run2 = runs[r];
39979
- if (nearestOwner(run2, "w:tc") !== tc) continue;
39980
- t += run2.textContent || "";
39981
- }
39982
- if (t.trim().length > 0) cells.push(t.trim());
39983
- }
39984
- }
39985
- if (cells.length === 0) return "";
39986
- return cells.join(" ");
39987
- } catch {
39988
- return void 0;
39989
- }
39990
- }
39991
- function nestedTableCount(node) {
39992
40536
  try {
39993
- const xml = node.xml;
39994
- if (!xml || typeof xml.getElementsByTagName !== "function") return 0;
39995
- return xml.getElementsByTagName("w:tbl").length;
39996
- } catch {
39997
- return 0;
39998
- }
39999
- }
40000
- function countExplicitPageBreaks(node) {
40001
- if (node.type === "pageBreak") return 1;
40002
- const text2 = node.metadata?.text || "";
40003
- let count = text2 === "[Page Break]" ? 1 : 0;
40004
- try {
40005
- const xml = node.xml;
40006
- if (xml && typeof xml.getElementsByTagName === "function") {
40007
- const brs = xml.getElementsByTagName("w:br");
40008
- let brCount = 0;
40009
- for (let i = 0; i < brs.length; i++) {
40010
- if (brs[i].getAttribute("w:type") === "page") brCount++;
40011
- }
40012
- if (brCount > count) count = brCount;
40013
- }
40537
+ const identity = applyDocumentIdentity(ast, parser2);
40538
+ if (identity.titleSetFrom) aggregate.titleSetFrom = identity.titleSetFrom;
40014
40539
  } catch {
40015
40540
  }
40016
- return count;
40017
- }
40018
- function computeDocStats(ast) {
40019
- let words = 0;
40020
- let paragraphs = 0;
40021
- let tables = 0;
40022
- let images = 0;
40023
- let pageBreaks = 0;
40024
- for (const sec of ast.sections) {
40025
- for (const node of sec.nodes) {
40026
- if (node.type === "paragraph") paragraphs += 1;
40027
- if (node.type === "table") tables += 1 + nestedTableCount(node);
40028
- if (node.type === "image") images += 1;
40029
- pageBreaks += countExplicitPageBreaks(node);
40030
- let text2 = node.metadata?.text ?? "";
40031
- if (node.type === "table") {
40032
- const spaced = tableCellTextSpaced(node);
40033
- if (spaced !== void 0) text2 = spaced;
40034
- }
40035
- const hits = text2.match(/\S+/g);
40036
- if (hits) words += hits.length;
40037
- }
40038
- }
40039
- const pageBudget = words / 450 + tables * 0.45 + images * 0.5 + pageBreaks;
40040
- return { words, paragraphs, pages: Math.max(1, Math.ceil(pageBudget)), tables, images };
40541
+ return aggregate;
40041
40542
  }
40042
40543
 
40043
40544
  // ../engine/src/style-guide/resolve.ts
40044
40545
  var import_fs8 = __toESM(require("fs"));
40045
40546
  var import_path7 = __toESM(require("path"));
40046
40547
  function hasScheme(ref) {
40047
- return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(ref);
40548
+ return hasUriScheme(ref);
40048
40549
  }
40049
40550
  function failValidation(message, remedy) {
40050
40551
  throw toError("E_VALIDATION", message, remedy);
@@ -40217,7 +40718,7 @@ function summarizeStyleGuide(guide) {
40217
40718
  // package.json
40218
40719
  var package_default = {
40219
40720
  name: "@attentionform/astra",
40220
- version: "0.3.0",
40721
+ version: "0.4.1",
40221
40722
  description: "Command-line tool for OOXML (.docx) document engineering",
40222
40723
  main: "dist/cli.js",
40223
40724
  types: "dist/packages/cli/src/cli.d.ts",
@@ -40260,8 +40761,8 @@ var package_default = {
40260
40761
  "adm-zip-gate": "node ../../scripts/check-adm-zip-imports.mjs"
40261
40762
  },
40262
40763
  dependencies: {
40263
- "@attentionform/astra-engine": "^0.3.0",
40264
- "@attentionform/ooxml-validator": "^0.3.0",
40764
+ "@attentionform/astra-engine": "^0.4.0",
40765
+ "@attentionform/ooxml-validator": "^0.4.1",
40265
40766
  "@xmldom/xmldom": "0.8.15",
40266
40767
  "adm-zip": "0.6.1",
40267
40768
  "isomorphic-git": "^1.25.0",
@@ -40922,9 +41423,9 @@ function commandHelpText(cmd) {
40922
41423
  " Supported input: #/##/### headings, blank-line paragraphs, flat bullet/ordered lists,",
40923
41424
  " header-row tables, --- dividers, fenced code, [^label] footnotes, inline spans (**bold**,",
40924
41425
  " *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, `> [!NOTE]`-style",
40925
- " alert callouts (NOTE|TIP|IMPORTANT|WARNING|CAUTION compile to callout containers), and standalone",
41426
+ " alert callouts (NOTE|TIP|IMPORTANT|WARNING|CAUTION compile to callout containers that keep inline formatting), `> quote` blocks (Quote style, nested up to 3 levels), and standalone",
40926
41427
  " block images/charts (![alt](path)) resolved from --allowed-dir. Out-of-scope input",
40927
- " (raw HTML, custom styles, plain blockquotes, deeper headings, nested lists/tables,",
41428
+ " (raw HTML, custom styles, deeper headings, nested lists/tables,",
40928
41429
  " task items) fails closed with E_VALIDATION plus a line number and writes nothing.",
40929
41430
  " Subtitle: a line that is only italic (*text*), directly under the opening # title, gets the",
40930
41431
  " Subtitle style (the style is already italic, so the emphasis is dropped). For italic body",
@@ -40956,10 +41457,10 @@ function commandHelpText(cmd) {
40956
41457
  " Same skinning as create --from-md: tables take headerFill, `> [!TAG]` alerts take the guide",
40957
41458
  " defaultTheme, plus chart palette, callout, and code-block defaults where content omits styling.",
40958
41459
  "Supported input: #/##/### headings, blank-line paragraphs, flat bullet/ordered lists,",
40959
- " header-row tables, --- dividers, fenced code, [^label] footnotes, `> [!NOTE]`-style alert callouts, inline spans (**bold**,",
41460
+ " header-row tables, --- dividers, fenced code, [^label] footnotes, `> [!NOTE]`-style alert callouts (inline formatting kept), `> quote` blocks (Quote style, up to 3 levels), inline spans (**bold**,",
40960
41461
  " *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, and standalone",
40961
41462
  " block images/charts (![alt](path)) resolved from --allowed-dir. Out-of-scope input",
40962
- " (raw HTML, custom styles, plain blockquotes, deeper headings, nested lists/tables,",
41463
+ " (raw HTML, custom styles, deeper headings, nested lists/tables,",
40963
41464
  " task items) fails closed with E_VALIDATION plus a line number and writes nothing.",
40964
41465
  " Subtitle: a line that is only italic (*text*), directly under the opening # title, gets the Subtitle style",
40965
41466
  " (the style is already italic, so the emphasis is dropped). For italic body text there, put another",
@@ -40970,6 +41471,7 @@ function commandHelpText(cmd) {
40970
41471
  `Usage: ${CLI_NAME} stats <docx> [--allow-macros] [--strip-macros]`,
40971
41472
  "",
40972
41473
  "Print word/paragraph counts plus estimated pages: `STATS <file>: words=N paragraphs=M pages~P (est, 500 wpp)`.",
41474
+ "Also prints `tables=N callouts=N images=N`; a callout (a one-cell filled table, such as a `> [!NOTE]` alert) counts in callouts, not in tables.",
40973
41475
  " --allow-macros preserve macro/active-content parts byte-identical (default refuses E_VALIDATION naming the parts)",
40974
41476
  " --strip-macros remove macro/active-content parts and continue (never with --allow-macros)",
40975
41477
  "Recipe: words count body paragraph text (non-whitespace tokens) plus table cell text (cells joined with spaces).",
@@ -42373,6 +42875,10 @@ async function runBatchJob(out, ctx) {
42373
42875
  const backupCode = await ensureBackupCopy(out, file, docx, target, ctx.backupState);
42374
42876
  if (backupCode !== void 0) return backupCode;
42375
42877
  const gateNormalized = skipValidate ? [] : readParserNormalizations(parser2);
42878
+ try {
42879
+ applyDocumentIdentity(ast, parser2);
42880
+ } catch {
42881
+ }
42376
42882
  const finalBytes = parser2.rezip({ reproducible: ctx.reproducible });
42377
42883
  const batchNormalized = mergeNormalized(readEngineNormalized(res), gateNormalized, readParserNormalizations(parser2));
42378
42884
  if (dupPreState && batchNormalized.some((l) => l.startsWith("Renumbered duplicate"))) {
@@ -42630,6 +43136,10 @@ async function runStreamSession(out, docx, opts, lines, emit) {
42630
43136
  remedy: `name the output "${path13.basename(target)}" with a .docm extension, or add --strip-macros to remove the macro parts`
42631
43137
  };
42632
43138
  }
43139
+ try {
43140
+ applyDocumentIdentity(await parser2.getAST(), parser2);
43141
+ } catch {
43142
+ }
42633
43143
  const finalBytes = parser2.rezip({ reproducible });
42634
43144
  await writeDocx(target, finalBytes);
42635
43145
  lastWritten = finalBytes;
@@ -44790,8 +45300,9 @@ async function cmdStats(out, args) {
44790
45300
  const ast = await parser2.getAST();
44791
45301
  const stats = computeDocStats(ast);
44792
45302
  print(out, `STATS ${file}: words=${stats.words} paragraphs=${stats.paragraphs} pages~${stats.pages} (est, 500 wpp)`);
45303
+ print(out, `tables=${stats.tables} callouts=${stats.callouts} images=${stats.images} (a callout is a one-cell filled table and is not counted in tables)`);
44793
45304
  print(out, `recipe: body paragraphs + table cells; excludes headers/footers/footnotes/comments; pages~P rough estimate = max(1, ceil(words/500))`);
44794
- printJson(out, { ok: true, file, words: stats.words, paragraphs: stats.paragraphs, pages: stats.pages });
45305
+ printJson(out, { ok: true, file, words: stats.words, paragraphs: stats.paragraphs, pages: stats.pages, tables: stats.tables, callouts: stats.callouts, images: stats.images });
44795
45306
  return EXIT_OK;
44796
45307
  } catch (err) {
44797
45308
  return failHuman(out, err, { file, fallbackCode: "E_VALIDATION" });
@@ -45323,6 +45834,10 @@ async function run(argv) {
45323
45834
 
45324
45835
  // src/cli.ts
45325
45836
  function isDirectEntry() {
45837
+ try {
45838
+ if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) return true;
45839
+ } catch {
45840
+ }
45326
45841
  const entry = process.argv?.[1] ?? "";
45327
45842
  return entry.endsWith("packages/cli/src/cli.ts") || entry.includes("packages/cli/dist/") || /(^|[/\\])astra$/.test(entry);
45328
45843
  }