document-cli 1.9.0 → 1.11.0

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.
@@ -1,6 +1,6 @@
1
1
  import { c as formatDocxExtrasLines, f as loadProvidedFonts, i as formatOdbReportLines, l as readInput, m as inferFormatFromExtension, n as describeOdbReport, o as formatMetadataLines, p as formatToExtension, r as formatOdbFormLines, t as describeOdbForm } from "./odb-structure-Du1w_hOU.js";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
- import { buildDocxPackage, buildOdtPackage, bytesToBase64, convertWordprocessingToLayout, createDocx, createFontMeasurer, createFontRegistry, createOdg, createOdp, createOds, createOdt, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openOdg, openOdp, openOds, openOdt, openPptx, parseXml, pptxToPdf, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbReportContent, readOdbReports, readOdbTables, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, rgbHexToColor, writePdf, xlsxToPdf } from "documents.js";
3
+ import { buildDocxPackage, buildOdtPackage, bytesToBase64, convertWordprocessingToLayout, createDocx, createFontMeasurer, createFontRegistry, createOdg, createOdp, createOds, createOdt, createPdf, createPptx, decodeMarkdownText, docxToPdf, encodeMarkdownText, encodePackage, hsqldbCellDisplayText, markdownToPdf, odgToPdf, odpToPdf, odsToPdf, odtToPdf, openDocx, openMarkdown, openOdg, openOdp, openOds, openOdt, openPdf, openPptx, parseXml, pptxToPdf, readDocxContent, readDocxExtras, readMarkdownContent, readOdbForms, readOdbReportContent, readOdbReports, readOdbTables, readOdgContent, readOdpContent, readOdsContent, readOdtContent, readPdf, readPptxContent, rgbHexToColor, writePdf, xlsxToPdf } from "documents.js";
4
4
  import { basename, dirname, extname, join } from "node:path";
5
5
  import { cellReference, columnIndexToLetters, decodePackage as decodePackage$1, encodePackage as encodePackage$1 } from "odf.js";
6
6
  import { readdirSync } from "node:fs";
@@ -36,10 +36,10 @@ function toPdfOptions(options, fonts) {
36
36
  };
37
37
  }
38
38
  async function exportToPdf(openDocument, destinationPath, options) {
39
- if (openDocument.format === "odb" || openDocument.format === "pdf") throw new Error(`A ${openDocument.format} document is already a read-only source; there is no export-to-PDF path for it`);
39
+ if (openDocument.format === "odb" || openDocument.format === "pdf") throw new Error(`A ${openDocument.format} document has no export-to-PDF path -- ${openDocument.format === "pdf" ? "save it directly instead" : "it is a read-only source with nothing to convert"}`);
40
40
  const pdfOptions = toPdfOptions(options, await loadProvidedFonts(options.fontFiles ?? [], { signal: options.signal }));
41
41
  if (openDocument.format === "markdown") {
42
- const pdfBytes = markdownToPdf(encodeMarkdownText(openDocument.source), pdfOptions);
42
+ const pdfBytes = markdownToPdf(encodeMarkdownText(openDocument.editor.toMarkdownText()), pdfOptions);
43
43
  await writeFile(destinationPath, pdfBytes);
44
44
  return;
45
45
  }
@@ -75,7 +75,7 @@ function detectFormat(path) {
75
75
  //#endregion
76
76
  //#region src/tui/format/open-document.ts
77
77
  const ODB_EXTENSION = ".odb";
78
- async function openDocumentAtPath(path) {
78
+ async function openDocumentAtPath(path, options = {}) {
79
79
  const bytes = new Uint8Array(await readFile(path));
80
80
  if (path.toLowerCase().endsWith(ODB_EXTENSION)) {
81
81
  const pkg = decodePackage$1(bytes);
@@ -120,16 +120,29 @@ async function openDocumentAtPath(path) {
120
120
  editor: openOdg(bytes),
121
121
  path
122
122
  };
123
- case "pdf": return {
124
- format,
125
- layout: readPdf(bytes),
126
- path
127
- };
128
- case "markdown": return {
129
- format,
130
- source: decodeMarkdownText(bytes),
131
- path
132
- };
123
+ case "pdf": {
124
+ const editor = openPdf(bytes);
125
+ return {
126
+ format,
127
+ editor,
128
+ layout: editor.toLayoutDocument(),
129
+ path
130
+ };
131
+ }
132
+ case "markdown": {
133
+ const text = decodeMarkdownText(bytes);
134
+ return {
135
+ format,
136
+ editor: openMarkdown(text, { sink: (diagnostic) => {
137
+ options.onDiagnostic?.({
138
+ severity: diagnostic.severity,
139
+ message: diagnostic.message
140
+ });
141
+ } }),
142
+ originalText: text,
143
+ path
144
+ };
145
+ }
133
146
  case "xlsx": return {
134
147
  format,
135
148
  layout: readPdf(xlsxToPdf(bytes)),
@@ -171,12 +184,21 @@ function createNewDocument(format) {
171
184
  editor: createOdg(),
172
185
  path: void 0
173
186
  };
187
+ case "pdf": {
188
+ const editor = createPdf();
189
+ return {
190
+ format,
191
+ editor,
192
+ layout: editor.toLayoutDocument(),
193
+ path: void 0
194
+ };
195
+ }
174
196
  }
175
197
  }
176
198
  async function saveDocumentTo(openDocument, path) {
177
- if (openDocument.format === "odb" || openDocument.format === "pdf" || openDocument.format === "xlsx") throw new Error(`A ${openDocument.format} document is opened read-only and cannot be written back`);
199
+ if (openDocument.format === "odb" || openDocument.format === "xlsx") throw new Error(`A ${openDocument.format} document is opened read-only and cannot be written back`);
178
200
  if (openDocument.format === "markdown") {
179
- await writeFile(path, encodeMarkdownText(openDocument.source));
201
+ await writeFile(path, encodeMarkdownText(openDocument.editor.toMarkdownText()));
180
202
  return;
181
203
  }
182
204
  await writeFile(path, openDocument.editor.toBytes());
@@ -189,7 +211,8 @@ const EDITABLE_FORMATS = {
189
211
  odt: true,
190
212
  odp: true,
191
213
  ods: true,
192
- odg: true
214
+ odg: true,
215
+ pdf: true
193
216
  };
194
217
  const WRITABLE_FORMATS = {
195
218
  ...EDITABLE_FORMATS,
@@ -219,7 +242,7 @@ function selectionKeyFor(screen) {
219
242
  case "pdfPageList":
220
243
  case "exportOptions":
221
244
  case "saveAsPrompt":
222
- case "markdownLineList":
245
+ case "viewSource":
223
246
  case "metadata": return screen.kind;
224
247
  case "filePicker": return `filePicker:${screen.purpose}:${screen.cwd}`;
225
248
  case "paragraphDetail":
@@ -242,19 +265,18 @@ function selectionKeyFor(screen) {
242
265
  case "odbFormDetail": return `odbFormDetail:${screen.formName}`;
243
266
  case "odbReportDetail": return `odbReportDetail:${screen.reportName}`;
244
267
  case "odbReportRender": return `odbReportRender:${screen.reportName}`;
245
- case "markdownLineEditor": return `markdownLineEditor:${screen.lineIndex}`;
246
268
  }
247
269
  }
248
270
  function rootScreenForFormat(format) {
249
271
  switch (format) {
250
272
  case "docx":
251
- case "odt": return { kind: "bodyList" };
273
+ case "odt":
274
+ case "markdown": return { kind: "bodyList" };
252
275
  case "pptx":
253
276
  case "odp": return { kind: "slideList" };
254
277
  case "ods": return { kind: "sheetList" };
255
278
  case "odg": return { kind: "pageList" };
256
279
  case "odb": return { kind: "odbTableList" };
257
- case "markdown": return { kind: "markdownLineList" };
258
280
  case "pdf":
259
281
  case "xlsx": return { kind: "pdfPageList" };
260
282
  }
@@ -368,6 +390,12 @@ function documentWithPath(doc, path) {
368
390
  editor: doc.editor,
369
391
  path
370
392
  };
393
+ case "pdf": return {
394
+ format: "pdf",
395
+ editor: doc.editor,
396
+ layout: doc.layout,
397
+ path
398
+ };
371
399
  case "odb": return {
372
400
  format: "odb",
373
401
  tables: doc.tables,
@@ -377,12 +405,8 @@ function documentWithPath(doc, path) {
377
405
  };
378
406
  case "markdown": return {
379
407
  format: "markdown",
380
- source: doc.source,
381
- path
382
- };
383
- case "pdf": return {
384
- format: "pdf",
385
- layout: doc.layout,
408
+ editor: doc.editor,
409
+ originalText: doc.originalText,
386
410
  path
387
411
  };
388
412
  case "xlsx": return {
@@ -425,10 +449,22 @@ function reopenEditable(doc, bytes) {
425
449
  editor: openOdg(bytes),
426
450
  path: doc.path
427
451
  };
452
+ case "pdf": {
453
+ const editor = openPdf(bytes);
454
+ return {
455
+ format: "pdf",
456
+ editor,
457
+ layout: editor.toLayoutDocument(),
458
+ path: doc.path
459
+ };
460
+ }
428
461
  }
429
462
  }
463
+ function toUndoSnapshot(doc) {
464
+ return doc.format === "markdown" ? encodeMarkdownText(doc.editor.toMarkdownText()) : doc.editor.toBytes();
465
+ }
430
466
  function mutate(state, doc, apply) {
431
- const snapshot = doc.editor.toBytes();
467
+ const snapshot = toUndoSnapshot(doc);
432
468
  apply();
433
469
  return {
434
470
  ...state,
@@ -468,22 +504,15 @@ function mergePptxTableCells(table, startRow, startColumn, rowSpan, colSpan) {
468
504
  }
469
505
  }
470
506
  }
471
- function mutateMarkdown(state, doc, source) {
472
- const snapshot = encodeMarkdownText(doc.source);
473
- return {
474
- ...state,
475
- openDocument: {
476
- ...doc,
477
- source
478
- },
479
- hasUnsavedChanges: true,
480
- undoStack: pushSnapshot(state.undoStack, snapshot)
481
- };
482
- }
483
507
  function wrongDocument(state, expected) {
484
508
  return withStatus(state, "warning", `That action needs ${expected}; the open document is ${state.openDocument === void 0 ? "no document" : state.openDocument.format}`);
485
509
  }
486
510
  function wordprocessingDocument(state) {
511
+ const doc = state.openDocument;
512
+ if (doc === void 0) return;
513
+ return doc.format === "docx" || doc.format === "odt" || doc.format === "markdown" ? doc : void 0;
514
+ }
515
+ function styledWordprocessingDocument(state) {
487
516
  const doc = state.openDocument;
488
517
  if (doc === void 0) return;
489
518
  return doc.format === "docx" || doc.format === "odt" ? doc : void 0;
@@ -508,15 +537,44 @@ function drawingDocument(state) {
508
537
  if (doc === void 0) return;
509
538
  return doc.format === "odg" ? doc : void 0;
510
539
  }
511
- function vectorHostDocument(state) {
540
+ function pdfDocument(state) {
512
541
  const doc = state.openDocument;
513
542
  if (doc === void 0) return;
514
- return doc.format === "odg" || doc.format === "odp" ? doc : void 0;
543
+ return doc.format === "pdf" ? doc : void 0;
544
+ }
545
+ function pdfItemAt(doc, pageIndex, itemIndex) {
546
+ return doc.editor.page(pageIndex)?.items()[itemIndex];
515
547
  }
516
- function markdownDocument(state) {
548
+ function withPdfPage(state, pageIndex, apply) {
549
+ const doc = pdfDocument(state);
550
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
551
+ const page = doc.editor.page(pageIndex);
552
+ if (page === void 0) return withStatus(state, "warning", `There is no page at index ${pageIndex}`);
553
+ return mutate(state, doc, () => {
554
+ apply(page);
555
+ });
556
+ }
557
+ function withPdfItemMatching(state, pageIndex, itemIndex, guard, kindLabel, apply) {
558
+ const doc = pdfDocument(state);
559
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
560
+ const item = pdfItemAt(doc, pageIndex, itemIndex);
561
+ if (item === void 0) return withStatus(state, "warning", `Page ${pageIndex} has no item at index ${itemIndex}`);
562
+ if (!guard(item)) return withStatus(state, "warning", `Item ${itemIndex} on page ${pageIndex} is a ${item.kind} item, not ${kindLabel}`);
563
+ return mutate(state, doc, () => {
564
+ apply(item);
565
+ });
566
+ }
567
+ const isPdfTextItem = (item) => item.kind === "text";
568
+ const isPdfRectItem = (item) => item.kind === "rect";
569
+ const isPdfEllipseItem = (item) => item.kind === "ellipse";
570
+ const isPdfLineItem = (item) => item.kind === "line";
571
+ const isPdfPathItem = (item) => item.kind === "path";
572
+ const isPdfImageItem = (item) => item.kind === "image";
573
+ const isPdfLinkItem = (item) => item.kind === "link";
574
+ function vectorHostDocument(state) {
517
575
  const doc = state.openDocument;
518
576
  if (doc === void 0) return;
519
- return doc.format === "markdown" ? doc : void 0;
577
+ return doc.format === "odg" || doc.format === "odp" ? doc : void 0;
520
578
  }
521
579
  function paragraphAt(doc, blockIndex) {
522
580
  return doc.editor.paragraphs()[blockIndex];
@@ -524,6 +582,9 @@ function paragraphAt(doc, blockIndex) {
524
582
  function tableAt(doc, tableIndex) {
525
583
  return doc.editor.tables()[tableIndex];
526
584
  }
585
+ function tableCellAt(table, row, column) {
586
+ return table.rows()[row]?.cells()[column];
587
+ }
527
588
  function shapeAt(doc, containerIndex, shapeIndex) {
528
589
  if (doc.format === "odg") return doc.editor.pages()[containerIndex]?.shapes()[shapeIndex];
529
590
  return doc.editor.slides()[containerIndex]?.shapes()[shapeIndex];
@@ -533,7 +594,7 @@ function sheetAt(doc, sheetIndex) {
533
594
  }
534
595
  function withRun(state, blockIndex, runIndex, apply) {
535
596
  const doc = wordprocessingDocument(state);
536
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
597
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
537
598
  const paragraph = paragraphAt(doc, blockIndex);
538
599
  if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${blockIndex}`);
539
600
  const run = paragraph.runs()[runIndex];
@@ -542,6 +603,17 @@ function withRun(state, blockIndex, runIndex, apply) {
542
603
  apply(run);
543
604
  });
544
605
  }
606
+ function withStyledRun(state, blockIndex, runIndex, apply) {
607
+ const doc = styledWordprocessingDocument(state);
608
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
609
+ const paragraph = doc.editor.paragraphs()[blockIndex];
610
+ if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${blockIndex}`);
611
+ const run = paragraph.runs()[runIndex];
612
+ if (run === void 0) return withStatus(state, "warning", `Paragraph ${blockIndex} has no run at index ${runIndex}`);
613
+ return mutate(state, doc, () => {
614
+ apply(run);
615
+ });
616
+ }
545
617
  function withShape(state, containerIndex, shapeIndex, apply) {
546
618
  const doc = shapeHostDocument(state);
547
619
  if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
@@ -703,8 +775,15 @@ function appReducer(state, action) {
703
775
  };
704
776
  case "APPEND_PARAGRAPH": {
705
777
  const doc = wordprocessingDocument(state);
706
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
778
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
707
779
  return mutate(state, doc, () => {
780
+ if (doc.format === "markdown") {
781
+ doc.editor.body.appendParagraph({
782
+ text: action.text,
783
+ styleId: action.styleId
784
+ });
785
+ return;
786
+ }
708
787
  doc.editor.body.appendParagraph({
709
788
  text: action.text,
710
789
  styleId: action.styleId,
@@ -713,9 +792,9 @@ function appReducer(state, action) {
713
792
  });
714
793
  }
715
794
  case "SET_PARAGRAPH_ALIGNMENT": {
716
- const doc = wordprocessingDocument(state);
795
+ const doc = styledWordprocessingDocument(state);
717
796
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
718
- const paragraph = paragraphAt(doc, action.blockIndex);
797
+ const paragraph = doc.editor.paragraphs()[action.blockIndex];
719
798
  if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
720
799
  return mutate(state, doc, () => {
721
800
  paragraph.alignment = action.alignment;
@@ -723,7 +802,7 @@ function appReducer(state, action) {
723
802
  }
724
803
  case "APPEND_RUN": {
725
804
  const doc = wordprocessingDocument(state);
726
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
805
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
727
806
  const paragraph = paragraphAt(doc, action.blockIndex);
728
807
  if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
729
808
  return mutate(state, doc, () => {
@@ -739,50 +818,60 @@ function appReducer(state, action) {
739
818
  case "TOGGLE_RUN_ITALIC": return withRun(state, action.blockIndex, action.runIndex, (run) => {
740
819
  run.italic = !run.italic;
741
820
  });
742
- case "TOGGLE_RUN_UNDERLINE": return withRun(state, action.blockIndex, action.runIndex, (run) => {
821
+ case "TOGGLE_RUN_UNDERLINE": return withStyledRun(state, action.blockIndex, action.runIndex, (run) => {
743
822
  run.underline = !run.underline;
744
823
  });
745
- case "SET_RUN_COLOR": return withRun(state, action.blockIndex, action.runIndex, (run) => {
824
+ case "SET_RUN_COLOR": return withStyledRun(state, action.blockIndex, action.runIndex, (run) => {
746
825
  run.color = action.color;
747
826
  });
748
- case "SET_RUN_FONT_FAMILY": return withRun(state, action.blockIndex, action.runIndex, (run) => {
827
+ case "SET_RUN_FONT_FAMILY": return withStyledRun(state, action.blockIndex, action.runIndex, (run) => {
749
828
  run.fontFamily = action.fontFamily;
750
829
  });
751
- case "SET_RUN_FONT_SIZE": return withRun(state, action.blockIndex, action.runIndex, (run) => {
830
+ case "SET_RUN_FONT_SIZE": return withStyledRun(state, action.blockIndex, action.runIndex, (run) => {
752
831
  run.sizePt = action.sizePt;
753
832
  });
754
833
  case "APPEND_TABLE": {
755
834
  const doc = wordprocessingDocument(state);
756
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
757
- return mutateGuarded(state, doc, () => {
835
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
836
+ let mergeUnsupported = false;
837
+ const nextState = mutateGuarded(state, doc, () => {
758
838
  const table = doc.editor.body.appendTable({
759
839
  rows: action.rows,
760
840
  columns: action.columns
761
841
  });
762
- if (action.merge !== void 0) table.mergeCells(action.merge.startRow, action.merge.startColumn, action.merge.rowSpan, action.merge.colSpan);
842
+ if (action.merge === void 0) return;
843
+ if (!("mergeCells" in table)) {
844
+ mergeUnsupported = true;
845
+ return;
846
+ }
847
+ table.mergeCells(action.merge.startRow, action.merge.startColumn, action.merge.rowSpan, action.merge.colSpan);
763
848
  });
849
+ return mergeUnsupported ? withStatus(nextState, "warning", "Markdown tables do not support merged cells -- the table was created without merging") : nextState;
764
850
  }
765
851
  case "MERGE_TABLE_CELLS": {
766
852
  const doc = wordprocessingDocument(state);
767
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
853
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
768
854
  const table = tableAt(doc, action.tableIndex);
769
855
  if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex}`);
856
+ if (!("mergeCells" in table)) return withStatus(state, "warning", "Markdown tables do not support merged cells");
770
857
  return mutateGuarded(state, doc, () => {
771
858
  table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
772
859
  });
773
860
  }
774
861
  case "SET_TABLE_CELL_TEXT": {
775
862
  const doc = wordprocessingDocument(state);
776
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
863
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
777
864
  const table = tableAt(doc, action.tableIndex);
778
865
  if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex}`);
866
+ const cell = tableCellAt(table, action.row, action.column);
867
+ if (cell === void 0) return withStatus(state, "warning", `There is no cell at row ${action.row}, column ${action.column} of table ${action.tableIndex}`);
779
868
  return mutate(state, doc, () => {
780
- setCellText(table.cell(action.row, action.column), action.text);
869
+ setCellText(cell, action.text);
781
870
  });
782
871
  }
783
872
  case "ADD_LIST_ITEM": {
784
873
  const doc = wordprocessingDocument(state);
785
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
874
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
786
875
  if (doc.format === "odt") {
787
876
  const list = doc.editor.lists()[action.blockIndex];
788
877
  if (list === void 0) return withStatus(state, "warning", `There is no list at index ${action.blockIndex}`);
@@ -801,7 +890,7 @@ function appReducer(state, action) {
801
890
  }
802
891
  case "SET_LIST_ITEM_TEXT": {
803
892
  const doc = wordprocessingDocument(state);
804
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
893
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
805
894
  if (doc.format !== "odt") return wrongDocument(state, "an odt document (lists are an odt-only concept)");
806
895
  const list = doc.editor.lists()[action.blockIndex];
807
896
  if (list === void 0) return withStatus(state, "warning", `There is no list at index ${action.blockIndex}`);
@@ -813,16 +902,16 @@ function appReducer(state, action) {
813
902
  }
814
903
  case "ADD_LIST": {
815
904
  const doc = wordprocessingDocument(state);
816
- if (doc === void 0) return wrongDocument(state, "a docx or odt document");
905
+ if (doc === void 0) return wrongDocument(state, "a docx, odt or markdown document");
817
906
  if (doc.format !== "odt") return wrongDocument(state, "an odt document (lists are an odt-only concept)");
818
907
  return mutate(state, doc, () => {
819
908
  doc.editor.body.appendList();
820
909
  });
821
910
  }
822
911
  case "INSERT_PARAGRAPH_IMAGE": {
823
- const doc = wordprocessingDocument(state);
912
+ const doc = styledWordprocessingDocument(state);
824
913
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
825
- const paragraph = paragraphAt(doc, action.blockIndex);
914
+ const paragraph = doc.editor.paragraphs()[action.blockIndex];
826
915
  if (paragraph === void 0) return withStatus(state, "warning", `There is no paragraph at index ${action.blockIndex}`);
827
916
  return mutate(state, doc, () => {
828
917
  paragraph.insertImageAfter({
@@ -1078,11 +1167,129 @@ function appReducer(state, action) {
1078
1167
  action.vector.stroke = action.stroke;
1079
1168
  });
1080
1169
  }
1081
- case "SET_MARKDOWN_SOURCE": {
1082
- const doc = markdownDocument(state);
1083
- if (doc === void 0) return wrongDocument(state, "a markdown document");
1084
- return mutateMarkdown(state, doc, action.source);
1170
+ case "ADD_PDF_TEXT": return withPdfPage(state, action.pageIndex, (page) => {
1171
+ page.appendText(action.init);
1172
+ });
1173
+ case "ADD_PDF_RECT": return withPdfPage(state, action.pageIndex, (page) => {
1174
+ page.appendRect(action.init);
1175
+ });
1176
+ case "ADD_PDF_ELLIPSE": return withPdfPage(state, action.pageIndex, (page) => {
1177
+ page.appendEllipse(action.init);
1178
+ });
1179
+ case "ADD_PDF_LINE": return withPdfPage(state, action.pageIndex, (page) => {
1180
+ page.appendLine(action.init);
1181
+ });
1182
+ case "ADD_PDF_PATH": return withPdfPage(state, action.pageIndex, (page) => {
1183
+ page.appendPath(action.init);
1184
+ });
1185
+ case "ADD_PDF_IMAGE": return withPdfPage(state, action.pageIndex, (page) => {
1186
+ page.appendImage(action.init);
1187
+ });
1188
+ case "ADD_PDF_LINK": return withPdfPage(state, action.pageIndex, (page) => {
1189
+ page.appendLink(action.init);
1190
+ });
1191
+ case "REMOVE_PDF_ITEM": {
1192
+ const doc = pdfDocument(state);
1193
+ if (doc === void 0) return wrongDocument(state, "a pdf document");
1194
+ const item = pdfItemAt(doc, action.pageIndex, action.itemIndex);
1195
+ if (item === void 0) return withStatus(state, "warning", `Page ${action.pageIndex} has no item at index ${action.itemIndex}`);
1196
+ return mutate(state, doc, () => {
1197
+ item.remove();
1198
+ });
1085
1199
  }
1200
+ case "SET_PDF_TEXT_TEXT": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1201
+ item.text = action.text;
1202
+ });
1203
+ case "SET_PDF_TEXT_POSITION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1204
+ item.xPt = action.xPt;
1205
+ item.yPt = action.yPt;
1206
+ });
1207
+ case "SET_PDF_TEXT_FONT": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1208
+ item.font = action.font;
1209
+ });
1210
+ case "SET_PDF_TEXT_SIZE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1211
+ item.sizePt = action.sizePt;
1212
+ });
1213
+ case "SET_PDF_TEXT_COLOR": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1214
+ item.color = action.color;
1215
+ });
1216
+ case "SET_PDF_TEXT_ROTATION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1217
+ item.rotationDeg = action.rotationDeg;
1218
+ });
1219
+ case "SET_PDF_TEXT_WIDTH": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1220
+ item.widthPt = action.widthPt;
1221
+ });
1222
+ case "TOGGLE_PDF_TEXT_UNDERLINE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfTextItem, "text", (item) => {
1223
+ item.underline = !(item.underline ?? false);
1224
+ });
1225
+ case "SET_PDF_RECT_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1226
+ item.xPt = action.xPt;
1227
+ item.yPt = action.yPt;
1228
+ item.widthPt = action.widthPt;
1229
+ item.heightPt = action.heightPt;
1230
+ });
1231
+ case "SET_PDF_RECT_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1232
+ item.fill = action.fill;
1233
+ });
1234
+ case "SET_PDF_RECT_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfRectItem, "rect", (item) => {
1235
+ item.stroke = action.stroke;
1236
+ });
1237
+ case "SET_PDF_ELLIPSE_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1238
+ item.xPt = action.xPt;
1239
+ item.yPt = action.yPt;
1240
+ item.widthPt = action.widthPt;
1241
+ item.heightPt = action.heightPt;
1242
+ });
1243
+ case "SET_PDF_ELLIPSE_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1244
+ item.fill = action.fill;
1245
+ });
1246
+ case "SET_PDF_ELLIPSE_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfEllipseItem, "ellipse", (item) => {
1247
+ item.stroke = action.stroke;
1248
+ });
1249
+ case "SET_PDF_LINE_FROM": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1250
+ item.x1Pt = action.x1Pt;
1251
+ item.y1Pt = action.y1Pt;
1252
+ });
1253
+ case "SET_PDF_LINE_TO": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1254
+ item.x2Pt = action.x2Pt;
1255
+ item.y2Pt = action.y2Pt;
1256
+ });
1257
+ case "SET_PDF_LINE_COLOR": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1258
+ item.color = action.color;
1259
+ });
1260
+ case "SET_PDF_LINE_WIDTH": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLineItem, "line", (item) => {
1261
+ item.widthPt = action.widthPt;
1262
+ });
1263
+ case "SET_PDF_PATH_FILL": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1264
+ item.fill = action.fill;
1265
+ });
1266
+ case "SET_PDF_PATH_FILL_RULE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1267
+ item.fillRule = action.fillRule;
1268
+ });
1269
+ case "SET_PDF_PATH_STROKE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfPathItem, "path", (item) => {
1270
+ item.stroke = action.stroke;
1271
+ });
1272
+ case "SET_PDF_IMAGE_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1273
+ item.xPt = action.xPt;
1274
+ item.yPt = action.yPt;
1275
+ item.widthPt = action.widthPt;
1276
+ item.heightPt = action.heightPt;
1277
+ });
1278
+ case "SET_PDF_IMAGE_ROTATION": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1279
+ item.rotationDeg = action.rotationDeg;
1280
+ });
1281
+ case "SET_PDF_IMAGE_SOURCE": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfImageItem, "image", (item) => {
1282
+ item.setImage(action.bytes, action.format);
1283
+ });
1284
+ case "SET_PDF_LINK_URI": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLinkItem, "link", (item) => {
1285
+ item.uri = action.uri;
1286
+ });
1287
+ case "SET_PDF_LINK_FRAME": return withPdfItemMatching(state, action.pageIndex, action.itemIndex, isPdfLinkItem, "link", (item) => {
1288
+ item.xPt = action.xPt;
1289
+ item.yPt = action.yPt;
1290
+ item.widthPt = action.widthPt;
1291
+ item.heightPt = action.heightPt;
1292
+ });
1086
1293
  case "APPEND_DIAGNOSTIC": return {
1087
1294
  ...state,
1088
1295
  diagnostics: [...state.diagnostics, action.diagnostic]
@@ -1111,12 +1318,12 @@ function appReducer(state, action) {
1111
1318
  case "UNDO": {
1112
1319
  const doc = state.openDocument;
1113
1320
  if (doc === void 0) return withStatus(state, "info", "There is nothing to undo");
1114
- if (doc.format === "odb" || doc.format === "pdf" || doc.format === "xlsx") return withStatus(state, "warning", `A ${doc.format} document is read-only, so it has no history to undo`);
1321
+ if (doc.format === "odb" || doc.format === "xlsx") return withStatus(state, "warning", `A ${doc.format} document is read-only, so it has no history to undo`);
1115
1322
  const snapshot = state.undoStack.at(-1);
1116
1323
  if (snapshot === void 0) return withStatus(state, "info", "There is nothing to undo");
1117
1324
  const restored = doc.format === "markdown" ? {
1118
1325
  ...doc,
1119
- source: decodeMarkdownText(snapshot)
1326
+ editor: openMarkdown(decodeMarkdownText(snapshot))
1120
1327
  } : reopenEditable(doc, snapshot);
1121
1328
  return withStatus({
1122
1329
  ...state,
@@ -1195,7 +1402,7 @@ function TextField(props) {
1195
1402
  }
1196
1403
  //#endregion
1197
1404
  //#region src/tui/components/command-palette.tsx
1198
- const USAGE_COLUMN_WIDTH = 32;
1405
+ const USAGE_COLUMN_WIDTH = 36;
1199
1406
  const COMMANDS = [
1200
1407
  {
1201
1408
  name: "save",
@@ -1214,7 +1421,7 @@ const COMMANDS = [
1214
1421
  },
1215
1422
  {
1216
1423
  name: "new",
1217
- usage: ":new docx|pptx|odt|odp|ods|odg",
1424
+ usage: ":new docx|pptx|odt|odp|ods|odg|pdf",
1218
1425
  description: "Create an empty document"
1219
1426
  },
1220
1427
  {
@@ -1232,6 +1439,11 @@ const COMMANDS = [
1232
1439
  usage: ":undo",
1233
1440
  description: "Undo the last change"
1234
1441
  },
1442
+ {
1443
+ name: "view-source",
1444
+ usage: ":view-source",
1445
+ description: "Compare a markdown document as opened vs. as it will save now"
1446
+ },
1235
1447
  {
1236
1448
  name: "help",
1237
1449
  usage: ":help",
@@ -1331,7 +1543,7 @@ async function runCommand(line, state, dispatch) {
1331
1543
  case "new": {
1332
1544
  const format = args[0];
1333
1545
  if (format === void 0 || !isEditableFormat(format)) {
1334
- warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg");
1546
+ warn(dispatch, "Usage: :new docx|pptx|odt|odp|ods|odg|pdf");
1335
1547
  return;
1336
1548
  }
1337
1549
  dispatch({
@@ -1350,7 +1562,12 @@ async function runCommand(line, state, dispatch) {
1350
1562
  dispatch({
1351
1563
  type: "OPEN_FILE_SUCCESS",
1352
1564
  path,
1353
- doc: await openDocumentAtPath(path)
1565
+ doc: await openDocumentAtPath(path, { onDiagnostic: (diagnostic) => {
1566
+ dispatch({
1567
+ type: "APPEND_DIAGNOSTIC",
1568
+ diagnostic
1569
+ });
1570
+ } })
1354
1571
  });
1355
1572
  } catch (error) {
1356
1573
  dispatch({
@@ -1367,6 +1584,16 @@ async function runCommand(line, state, dispatch) {
1367
1584
  case "undo":
1368
1585
  dispatch({ type: "UNDO" });
1369
1586
  return;
1587
+ case "view-source":
1588
+ if (doc?.format !== "markdown") {
1589
+ warn(dispatch, ":view-source only applies to an open markdown document");
1590
+ return;
1591
+ }
1592
+ dispatch({
1593
+ type: "PUSH_SCREEN",
1594
+ screen: { kind: "viewSource" }
1595
+ });
1596
+ return;
1370
1597
  case "help":
1371
1598
  dispatch({
1372
1599
  type: "OPEN_OVERLAY",
@@ -2037,9 +2264,12 @@ function parseNumberField(raw, fallback) {
2037
2264
  }
2038
2265
  //#endregion
2039
2266
  //#region src/tui/screens/shared/paragraph-family.tsx
2267
+ function supportsRunStyleExtras(run) {
2268
+ return "underline" in run;
2269
+ }
2040
2270
  function paragraphFamilyDocument(openDocument) {
2041
2271
  if (openDocument === void 0) return;
2042
- return openDocument.format === "docx" || openDocument.format === "odt" ? openDocument : void 0;
2272
+ return openDocument.format === "docx" || openDocument.format === "odt" || openDocument.format === "markdown" ? openDocument : void 0;
2043
2273
  }
2044
2274
  function liveParagraphAt(doc, blockIndex) {
2045
2275
  return doc.editor.paragraphs()[blockIndex];
@@ -2522,14 +2752,14 @@ const IMAGE_FIELDS = [
2522
2752
  defaultValue: ""
2523
2753
  }
2524
2754
  ];
2525
- function inferImageFormat$1(path) {
2755
+ function inferImageFormat$2(path) {
2526
2756
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
2527
2757
  if (extension === "png") return "png";
2528
2758
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
2529
2759
  }
2530
2760
  async function applyInsertImage(blockIndex, values, dispatch) {
2531
2761
  const path = requireFieldValue(values, "path");
2532
- const format = inferImageFormat$1(path);
2762
+ const format = inferImageFormat$2(path);
2533
2763
  if (format === void 0) {
2534
2764
  dispatch({
2535
2765
  type: "SET_STATUS",
@@ -2565,14 +2795,17 @@ function ParagraphRunsView(props) {
2565
2795
  dimColor: true,
2566
2796
  children: "(no runs -- press 'a' to append one)"
2567
2797
  });
2568
- return /* @__PURE__ */ jsx(Box, { children: props.runs.map((run, index) => /* @__PURE__ */ jsx(Text, {
2569
- bold: run.bold,
2570
- italic: run.italic,
2571
- underline: run.underline,
2572
- color: run.color === void 0 ? void 0 : layoutColorToHex(run.color),
2573
- inverse: index === props.selectedRunIndex,
2574
- children: run.text.length === 0 ? "<empty run>" : run.text
2575
- }, index)) });
2798
+ return /* @__PURE__ */ jsx(Box, { children: props.runs.map((run, index) => {
2799
+ const styled = supportsRunStyleExtras(run);
2800
+ return /* @__PURE__ */ jsx(Text, {
2801
+ bold: run.bold,
2802
+ italic: run.italic,
2803
+ underline: styled && run.underline,
2804
+ color: styled && run.color !== void 0 ? layoutColorToHex(run.color) : void 0,
2805
+ inverse: index === props.selectedRunIndex,
2806
+ children: run.text.length === 0 ? "<empty run>" : run.text
2807
+ }, index);
2808
+ }) });
2576
2809
  }
2577
2810
  function ParagraphDetailScreen() {
2578
2811
  const state = useAppState();
@@ -2645,7 +2878,7 @@ function ParagraphDetailScreen() {
2645
2878
  });
2646
2879
  return;
2647
2880
  }
2648
- if (input === "I") {
2881
+ if (input === "I" && doc?.format !== "markdown") {
2649
2882
  setImageWizardOpen(true);
2650
2883
  return;
2651
2884
  }
@@ -2670,6 +2903,7 @@ function ParagraphDetailScreen() {
2670
2903
  });
2671
2904
  return;
2672
2905
  }
2906
+ if (!supportsRunStyleExtras(selectedRun)) return;
2673
2907
  if (input === "u") {
2674
2908
  dispatch({
2675
2909
  type: "TOGGLE_RUN_UNDERLINE",
@@ -2694,7 +2928,7 @@ function ParagraphDetailScreen() {
2694
2928
  });
2695
2929
  if (doc === void 0) return /* @__PURE__ */ jsx(Text, {
2696
2930
  color: "red",
2697
- children: "ParagraphDetailScreen requires an open docx or odt document."
2931
+ children: "ParagraphDetailScreen requires an open docx, odt or markdown document."
2698
2932
  });
2699
2933
  if (paragraph === void 0) return /* @__PURE__ */ jsxs(Text, {
2700
2934
  color: "red",
@@ -2781,7 +3015,7 @@ function ParagraphDetailScreen() {
2781
3015
  placeholder: "e.g. 12",
2782
3016
  onChange: setFontSizeInput,
2783
3017
  onSubmit: (value) => {
2784
- const sizePt = parseNumberField(value, selectedRun?.sizePt ?? DEFAULT_RUN_SIZE_PT);
3018
+ const sizePt = parseNumberField(value, selectedRun !== void 0 && supportsRunStyleExtras(selectedRun) ? selectedRun.sizePt ?? DEFAULT_RUN_SIZE_PT : DEFAULT_RUN_SIZE_PT);
2785
3019
  if (sizePt <= 0) dispatch({
2786
3020
  type: "SET_STATUS",
2787
3021
  severity: "warning",
@@ -2834,7 +3068,12 @@ function ParagraphDetailScreen() {
2834
3068
  /* @__PURE__ */ jsxs(Text, {
2835
3069
  dimColor: true,
2836
3070
  children: [
2837
- "<- / -> move, Enter edit text, b/i/u toggle, c colour, f font, s size, a append run, I image",
3071
+ "<- / -> move, Enter edit text, b/i",
3072
+ doc.format === "markdown" ? "" : "/u",
3073
+ " toggle",
3074
+ doc.format === "markdown" ? "" : ", c colour, f font, s size",
3075
+ ", a append run",
3076
+ doc.format === "markdown" ? "" : ", I image",
2838
3077
  doc.format === "docx" ? ", m formula" : "",
2839
3078
  ", Esc back"
2840
3079
  ]
@@ -2869,7 +3108,7 @@ function RunEditorScreen() {
2869
3108
  });
2870
3109
  if (doc === void 0) return /* @__PURE__ */ jsx(Text, {
2871
3110
  color: "red",
2872
- children: "RunEditorScreen requires an open docx or odt document."
3111
+ children: "RunEditorScreen requires an open docx, odt or markdown document."
2873
3112
  });
2874
3113
  const paragraph = liveParagraphAt(doc, screen.blockIndex);
2875
3114
  if (paragraph === void 0) return /* @__PURE__ */ jsxs(Text, {
@@ -3028,7 +3267,7 @@ function TableViewScreen() {
3028
3267
  });
3029
3268
  if (doc === void 0) return /* @__PURE__ */ jsx(Text, {
3030
3269
  color: "red",
3031
- children: "TableViewScreen requires an open docx or odt document."
3270
+ children: "TableViewScreen requires an open docx, odt or markdown document."
3032
3271
  });
3033
3272
  if (table === void 0) return /* @__PURE__ */ jsxs(Text, {
3034
3273
  color: "red",
@@ -3102,7 +3341,7 @@ function TableCellDetailScreen() {
3102
3341
  });
3103
3342
  if (doc === void 0) return /* @__PURE__ */ jsx(Text, {
3104
3343
  color: "red",
3105
- children: "TableCellDetailScreen requires an open docx or odt document."
3344
+ children: "TableCellDetailScreen requires an open docx, odt or markdown document."
3106
3345
  });
3107
3346
  if (cell === void 0) return /* @__PURE__ */ jsxs(Text, {
3108
3347
  color: "red",
@@ -3249,123 +3488,67 @@ function DocxExtrasScreen() {
3249
3488
  });
3250
3489
  }
3251
3490
  //#endregion
3252
- //#region src/tui/screens/editors/markdown/shared.ts
3253
- function requireMarkdownDocument(openDocument) {
3254
- if (openDocument?.format !== "markdown") throw new Error("A markdown editing screen rendered without an open markdown document; the app router only reaches this screen group from markdownLineList, which is only ever the root screen of an open markdown document.");
3255
- return openDocument;
3256
- }
3257
- //#endregion
3258
- //#region src/tui/screens/editors/markdown/line-list.tsx
3259
- function MarkdownLineListScreen() {
3260
- const state = useAppState();
3261
- const dispatch = useAppDispatch();
3262
- const doc = requireMarkdownDocument(state.openDocument);
3263
- const query = state.searchQuery.trim().toLowerCase();
3264
- const allLines = doc.source.split("\n").map((line, lineIndex) => ({
3265
- line,
3266
- lineIndex
3267
- }));
3268
- const lines = query === "" ? allLines : allLines.filter((entry) => entry.line.toLowerCase().includes(query));
3269
- const { selectedIndex } = useNavigationInput({
3270
- itemCount: lines.length,
3271
- onSelect: (index) => {
3272
- const entry = lines[index];
3273
- if (entry === void 0) return;
3274
- dispatch({
3275
- type: "PUSH_SCREEN",
3276
- screen: {
3277
- kind: "markdownLineEditor",
3278
- lineIndex: entry.lineIndex
3279
- }
3280
- });
3281
- },
3282
- onBack: () => {
3283
- dispatch({ type: "POP_SCREEN" });
3284
- },
3285
- isActive: !anyOverlayOpen(state)
3286
- });
3287
- return /* @__PURE__ */ jsxs(Box, {
3288
- flexDirection: "column",
3289
- children: [/* @__PURE__ */ jsxs(Text, {
3290
- bold: true,
3291
- children: [
3292
- "Lines (",
3293
- lines.length,
3294
- " of ",
3295
- allLines.length,
3296
- ")"
3297
- ]
3298
- }), /* @__PURE__ */ jsx(ListView, {
3299
- items: lines,
3300
- selectedIndex,
3301
- emptyMessage: query === "" ? "This document has no lines." : `No lines match "${state.searchQuery}".`,
3302
- renderItem: ({ line, lineIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
3303
- color: isSelected ? "cyan" : void 0,
3304
- inverse: isSelected,
3305
- children: [
3306
- lineIndex + 1,
3307
- ": ",
3308
- line === "" ? "(blank)" : line
3309
- ]
3310
- })
3311
- })]
3312
- });
3313
- }
3314
- //#endregion
3315
- //#region src/tui/screens/editors/markdown/line-editor.tsx
3316
- function MarkdownLineEditorScreen() {
3491
+ //#region src/tui/screens/editors/markdown/view-source.tsx
3492
+ function MarkdownViewSourceScreen() {
3317
3493
  const state = useAppState();
3318
3494
  const dispatch = useAppDispatch();
3319
3495
  const screen = currentScreen(state);
3320
3496
  const doc = state.openDocument;
3321
- if (screen.kind !== "markdownLineEditor") return /* @__PURE__ */ jsx(Text, {
3497
+ useInput((_input, key) => {
3498
+ if (key.escape) dispatch({ type: "POP_SCREEN" });
3499
+ }, { isActive: !anyOverlayOpen(state) && screen.kind === "viewSource" });
3500
+ if (screen.kind !== "viewSource") return /* @__PURE__ */ jsx(Text, {
3322
3501
  color: "red",
3323
- children: "MarkdownLineEditorScreen rendered outside a markdownLineEditor screen."
3502
+ children: "MarkdownViewSourceScreen rendered outside a viewSource screen."
3324
3503
  });
3325
3504
  if (doc?.format !== "markdown") return /* @__PURE__ */ jsx(Text, {
3326
3505
  color: "red",
3327
- children: "MarkdownLineEditorScreen requires an open markdown document."
3328
- });
3329
- const lines = doc.source.split("\n");
3330
- const line = lines[screen.lineIndex];
3331
- if (line === void 0) return /* @__PURE__ */ jsxs(Text, {
3332
- color: "red",
3333
- children: [
3334
- "There is no line at index ",
3335
- screen.lineIndex,
3336
- "."
3337
- ]
3506
+ children: "view-source requires an open markdown document."
3338
3507
  });
3508
+ const asItWillSaveNow = doc.editor.toMarkdownText();
3339
3509
  return /* @__PURE__ */ jsxs(Box, {
3340
3510
  flexDirection: "column",
3341
3511
  children: [
3342
- /* @__PURE__ */ jsxs(Text, {
3512
+ /* @__PURE__ */ jsx(Text, {
3343
3513
  bold: true,
3344
- children: ["Edit line ", screen.lineIndex + 1]
3514
+ children: "As opened"
3345
3515
  }),
3346
- /* @__PURE__ */ jsx(RunTextEditor, {
3347
- initialText: line,
3348
- onCommit: (text) => {
3349
- const nextLines = [...lines];
3350
- nextLines[screen.lineIndex] = text;
3351
- dispatch({
3352
- type: "SET_MARKDOWN_SOURCE",
3353
- source: nextLines.join("\n")
3354
- });
3355
- dispatch({ type: "POP_SCREEN" });
3356
- },
3357
- onCancel: () => {
3358
- dispatch({ type: "POP_SCREEN" });
3359
- }
3516
+ /* @__PURE__ */ jsx(Text, { children: doc.originalText ?? "(this document was created fresh, with no original text to compare against)" }),
3517
+ /* @__PURE__ */ jsx(Text, {
3518
+ bold: true,
3519
+ children: "As it will save right now"
3360
3520
  }),
3521
+ /* @__PURE__ */ jsx(Text, { children: asItWillSaveNow }),
3361
3522
  /* @__PURE__ */ jsx(Text, {
3362
3523
  dimColor: true,
3363
- children: "Enter to commit, Esc to discard"
3524
+ children: "Esc back"
3364
3525
  })
3365
3526
  ]
3366
3527
  });
3367
3528
  }
3368
3529
  //#endregion
3530
+ //#region src/tui/screens/editors/markdown/index.tsx
3531
+ function MarkdownBodyListScreen() {
3532
+ const state = useAppState();
3533
+ const dispatch = useAppDispatch();
3534
+ const doc = state.openDocument;
3535
+ if (doc?.format !== "markdown") return /* @__PURE__ */ jsxs(Text, {
3536
+ color: "red",
3537
+ children: [
3538
+ "MarkdownBodyListScreen requires an open markdown document, found ",
3539
+ doc === void 0 ? "no open document" : doc.format,
3540
+ "."
3541
+ ]
3542
+ });
3543
+ const adapter = createParagraphFamilyAdapter({
3544
+ formatLabel: "markdown",
3545
+ paragraphs: () => doc.editor.paragraphs(),
3546
+ tables: () => doc.editor.tables(),
3547
+ dispatch
3548
+ });
3549
+ return /* @__PURE__ */ jsx(ParagraphFamilyBodyList, { adapter });
3550
+ }
3551
+ //#endregion
3369
3552
  //#region src/tui/screens/editors/odb/shared.ts
3370
3553
  function requireOdbDocument(openDocument) {
3371
3554
  if (openDocument?.format !== "odb") throw new Error("An .odb browsing screen rendered without an open .odb document; the app router only reaches this screen group from odbTableList, which is only ever the root screen of an open .odb document.");
@@ -4012,14 +4195,14 @@ function vectorKindLabel(kind) {
4012
4195
  case "path": return "Path";
4013
4196
  }
4014
4197
  }
4015
- function formatPt(value) {
4198
+ function formatPt$1(value) {
4016
4199
  return value.toFixed(1);
4017
4200
  }
4018
4201
  function formatFrame(box) {
4019
- return `${formatPt(box.xPt)},${formatPt(box.yPt)} ${formatPt(box.widthPt)}x${formatPt(box.heightPt)}pt`;
4202
+ return `${formatPt$1(box.xPt)},${formatPt$1(box.yPt)} ${formatPt$1(box.widthPt)}x${formatPt$1(box.heightPt)}pt`;
4020
4203
  }
4021
4204
  function formatPoint$1(point) {
4022
- return `${formatPt(point.xPt)},${formatPt(point.yPt)}`;
4205
+ return `${formatPt$1(point.xPt)},${formatPt$1(point.yPt)}`;
4023
4206
  }
4024
4207
  function formatColor$1(color) {
4025
4208
  return `rgb(${color.r.toFixed(2)}, ${color.g.toFixed(2)}, ${color.b.toFixed(2)})`;
@@ -4031,7 +4214,7 @@ function describeVectorGeometry(vector) {
4031
4214
  function describeFillStroke(vector) {
4032
4215
  const parts = [];
4033
4216
  if (vector.kind !== "line" && vector.fill !== void 0) parts.push(`fill ${formatColor$1(vector.fill)}`);
4034
- if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`);
4217
+ if (vector.stroke !== void 0) parts.push(`stroke ${formatColor$1(vector.stroke.color)} ${formatPt$1(vector.stroke.widthPt)}pt`);
4035
4218
  return parts.length === 0 ? "no fill or stroke" : parts.join(", ");
4036
4219
  }
4037
4220
  //#endregion
@@ -4098,7 +4281,7 @@ function OdgPageListScreen() {
4098
4281
  }
4099
4282
  //#endregion
4100
4283
  //#region src/tui/screens/editors/odg/page-detail.tsx
4101
- const ADD_KIND_OPTIONS = [
4284
+ const ADD_KIND_OPTIONS$1 = [
4102
4285
  {
4103
4286
  kind: "rect",
4104
4287
  label: "Rectangle"
@@ -4124,7 +4307,7 @@ const ADD_KIND_OPTIONS = [
4124
4307
  label: "Image"
4125
4308
  }
4126
4309
  ];
4127
- const GEOMETRY_FIELDS = [
4310
+ const GEOMETRY_FIELDS$1 = [
4128
4311
  {
4129
4312
  key: "xPt",
4130
4313
  label: "X (pt)",
@@ -4146,24 +4329,24 @@ const GEOMETRY_FIELDS = [
4146
4329
  defaultValue: "100"
4147
4330
  }
4148
4331
  ];
4149
- const FILL_FIELD = {
4332
+ const FILL_FIELD$1 = {
4150
4333
  key: "fill",
4151
4334
  label: "Fill \"r g b\" (0-1 each), blank for none",
4152
4335
  defaultValue: "0.8 0.8 0.8"
4153
4336
  };
4154
- const STROKE_FIELD = {
4337
+ const STROKE_FIELD$1 = {
4155
4338
  key: "stroke",
4156
4339
  label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
4157
4340
  defaultValue: "0 0 0 1"
4158
4341
  };
4159
- function fieldsForAddKind(kind) {
4342
+ function fieldsForAddKind$1(kind) {
4160
4343
  switch (kind) {
4161
4344
  case "rect":
4162
4345
  case "ellipse":
4163
4346
  case "path": return [
4164
- ...GEOMETRY_FIELDS,
4165
- FILL_FIELD,
4166
- STROKE_FIELD
4347
+ ...GEOMETRY_FIELDS$1,
4348
+ FILL_FIELD$1,
4349
+ STROKE_FIELD$1
4167
4350
  ];
4168
4351
  case "line": return [
4169
4352
  {
@@ -4186,15 +4369,15 @@ function fieldsForAddKind(kind) {
4186
4369
  label: "To Y (pt)",
4187
4370
  defaultValue: "40"
4188
4371
  },
4189
- STROKE_FIELD
4372
+ STROKE_FIELD$1
4190
4373
  ];
4191
- case "textbox": return [...GEOMETRY_FIELDS, {
4374
+ case "textbox": return [...GEOMETRY_FIELDS$1, {
4192
4375
  key: "text",
4193
4376
  label: "Text",
4194
4377
  defaultValue: "Text"
4195
4378
  }];
4196
4379
  case "image": return [
4197
- ...GEOMETRY_FIELDS,
4380
+ ...GEOMETRY_FIELDS$1,
4198
4381
  {
4199
4382
  key: "path",
4200
4383
  label: "Image file path (.png/.jpg/.jpeg)",
@@ -4208,7 +4391,7 @@ function fieldsForAddKind(kind) {
4208
4391
  ];
4209
4392
  }
4210
4393
  }
4211
- function readFrame(values) {
4394
+ function readFrame$1(values) {
4212
4395
  return {
4213
4396
  xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
4214
4397
  yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
@@ -4227,19 +4410,19 @@ function warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, label) {
4227
4410
  const added = buildPageItems(doc, pageIndex).filter((item) => item.kind === "vector").at(-1);
4228
4411
  if (added !== void 0 && added.liveVector === void 0) warnVectorIsViewOnly(dispatch, label);
4229
4412
  }
4230
- function inferImageFormat(path) {
4413
+ function inferImageFormat$1(path) {
4231
4414
  const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
4232
4415
  if (extension === "png") return "png";
4233
4416
  if (extension === "jpg" || extension === "jpeg") return "jpeg";
4234
4417
  }
4235
- async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4418
+ async function applyAddKind$1(kind, pageIndex, doc, values, dispatch) {
4236
4419
  switch (kind) {
4237
4420
  case "rect":
4238
4421
  dispatch({
4239
4422
  type: "ADD_RECT",
4240
4423
  containerIndex: pageIndex,
4241
4424
  init: {
4242
- frame: readFrame(values),
4425
+ frame: readFrame$1(values),
4243
4426
  fill: parseColorField(requireFieldValue(values, "fill")),
4244
4427
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4245
4428
  }
@@ -4251,7 +4434,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4251
4434
  type: "ADD_ELLIPSE",
4252
4435
  containerIndex: pageIndex,
4253
4436
  init: {
4254
- frame: readFrame(values),
4437
+ frame: readFrame$1(values),
4255
4438
  fill: parseColorField(requireFieldValue(values, "fill")),
4256
4439
  stroke: parseStrokeField(requireFieldValue(values, "stroke"))
4257
4440
  }
@@ -4284,7 +4467,7 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4284
4467
  warnIfVectorAddedReadOnly(doc, pageIndex, dispatch, "Line");
4285
4468
  return;
4286
4469
  case "path": {
4287
- const frame = readFrame(values);
4470
+ const frame = readFrame$1(values);
4288
4471
  dispatch({
4289
4472
  type: "ADD_PATH",
4290
4473
  containerIndex: pageIndex,
@@ -4302,14 +4485,14 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4302
4485
  dispatch({
4303
4486
  type: "ADD_TEXTBOX",
4304
4487
  containerIndex: pageIndex,
4305
- frame: readFrame(values),
4488
+ frame: readFrame$1(values),
4306
4489
  text: requireFieldValue(values, "text")
4307
4490
  });
4308
4491
  return;
4309
4492
  case "image": {
4310
- const frame = readFrame(values);
4493
+ const frame = readFrame$1(values);
4311
4494
  const path = requireFieldValue(values, "path");
4312
- const format = inferImageFormat(path);
4495
+ const format = inferImageFormat$1(path);
4313
4496
  if (format === void 0) {
4314
4497
  dispatch({
4315
4498
  type: "SET_STATUS",
@@ -4340,15 +4523,15 @@ async function applyAddKind(kind, pageIndex, doc, values, dispatch) {
4340
4523
  }
4341
4524
  }
4342
4525
  }
4343
- function AddItemFlow(props) {
4526
+ function AddItemFlow$1(props) {
4344
4527
  const dispatch = useAppDispatch();
4345
4528
  const [kind, setKind] = useState(void 0);
4346
4529
  const { selectedIndex } = useNavigationInput({
4347
- itemCount: ADD_KIND_OPTIONS.length,
4530
+ itemCount: ADD_KIND_OPTIONS$1.length,
4348
4531
  isActive: props.isActive && kind === void 0,
4349
4532
  onBack: props.onCancel,
4350
4533
  onSelect: (index) => {
4351
- const option = ADD_KIND_OPTIONS[index];
4534
+ const option = ADD_KIND_OPTIONS$1[index];
4352
4535
  if (option === void 0) return;
4353
4536
  setKind(option.kind);
4354
4537
  }
@@ -4361,7 +4544,7 @@ function AddItemFlow(props) {
4361
4544
  bold: true,
4362
4545
  children: "Add item -- choose a kind"
4363
4546
  }), /* @__PURE__ */ jsx(ListView, {
4364
- items: ADD_KIND_OPTIONS,
4547
+ items: ADD_KIND_OPTIONS$1,
4365
4548
  selectedIndex,
4366
4549
  reservedRows: 6,
4367
4550
  renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
@@ -4371,10 +4554,10 @@ function AddItemFlow(props) {
4371
4554
  })]
4372
4555
  });
4373
4556
  return /* @__PURE__ */ jsx(FieldWizard, {
4374
- fields: fieldsForAddKind(kind),
4557
+ fields: fieldsForAddKind$1(kind),
4375
4558
  onCancel: props.onCancel,
4376
4559
  onComplete: (values) => {
4377
- applyAddKind(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4560
+ applyAddKind$1(kind, props.pageIndex, props.doc, values, dispatch).then(props.onCreated);
4378
4561
  }
4379
4562
  });
4380
4563
  }
@@ -4426,7 +4609,7 @@ function OdgPageDetailScreen() {
4426
4609
  setIsAdding(true);
4427
4610
  }
4428
4611
  });
4429
- if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
4612
+ if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow$1, {
4430
4613
  pageIndex,
4431
4614
  doc,
4432
4615
  isActive: !overlayOpen,
@@ -4479,7 +4662,7 @@ function buildVectorRows(vector, liveVector, dispatch) {
4479
4662
  });
4480
4663
  }
4481
4664
  rows.push({
4482
- label: `Stroke: ${vector.stroke === void 0 ? "none" : `${formatColor$1(vector.stroke.color)} ${formatPt(vector.stroke.widthPt)}pt`}`,
4665
+ label: `Stroke: ${vector.stroke === void 0 ? "none" : `${formatColor$1(vector.stroke.color)} ${formatPt$1(vector.stroke.widthPt)}pt`}`,
4483
4666
  currentValue: vector.stroke === void 0 ? "" : `${vector.stroke.color.r} ${vector.stroke.color.g} ${vector.stroke.color.b} ${vector.stroke.widthPt}`,
4484
4667
  commit: (raw) => {
4485
4668
  const stroke = parseStrokeField(raw);
@@ -4611,7 +4794,7 @@ function ShapeDetail(props) {
4611
4794
  }
4612
4795
  },
4613
4796
  {
4614
- label: `X: ${formatPt(frame.xPt)}pt`,
4797
+ label: `X: ${formatPt$1(frame.xPt)}pt`,
4615
4798
  currentValue: String(frame.xPt),
4616
4799
  commit: (raw) => {
4617
4800
  dispatch({
@@ -4626,7 +4809,7 @@ function ShapeDetail(props) {
4626
4809
  }
4627
4810
  },
4628
4811
  {
4629
- label: `Y: ${formatPt(frame.yPt)}pt`,
4812
+ label: `Y: ${formatPt$1(frame.yPt)}pt`,
4630
4813
  currentValue: String(frame.yPt),
4631
4814
  commit: (raw) => {
4632
4815
  dispatch({
@@ -4641,7 +4824,7 @@ function ShapeDetail(props) {
4641
4824
  }
4642
4825
  },
4643
4826
  {
4644
- label: `Width: ${formatPt(frame.widthPt)}pt`,
4827
+ label: `Width: ${formatPt$1(frame.widthPt)}pt`,
4645
4828
  currentValue: String(frame.widthPt),
4646
4829
  commit: (raw) => {
4647
4830
  dispatch({
@@ -4656,7 +4839,7 @@ function ShapeDetail(props) {
4656
4839
  }
4657
4840
  },
4658
4841
  {
4659
- label: `Height: ${formatPt(frame.heightPt)}pt`,
4842
+ label: `Height: ${formatPt$1(frame.heightPt)}pt`,
4660
4843
  currentValue: String(frame.heightPt),
4661
4844
  commit: (raw) => {
4662
4845
  dispatch({
@@ -4671,7 +4854,7 @@ function ShapeDetail(props) {
4671
4854
  }
4672
4855
  },
4673
4856
  {
4674
- label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt(shape.rotationDeg)} deg`}`,
4857
+ label: `Rotation: ${shape.rotationDeg === void 0 ? "none" : `${formatPt$1(shape.rotationDeg)} deg`}`,
4675
4858
  currentValue: shape.rotationDeg === void 0 ? "" : String(shape.rotationDeg),
4676
4859
  commit: (raw) => {
4677
4860
  const trimmed = raw.trim();
@@ -6997,26 +7180,75 @@ function requirePdfDocument(openDocument) {
6997
7180
  if (openDocument?.format !== "pdf" && openDocument?.format !== "xlsx") throw new Error("A PDF inspection screen rendered without an open PDF or xlsx document; the app router only reaches this screen group from pdfPageList, which is only ever the root screen of one of those two formats.");
6998
7181
  return openDocument;
6999
7182
  }
7183
+ function isEditablePdfDocument(doc) {
7184
+ return doc.format === "pdf";
7185
+ }
7000
7186
  function formatSize(widthPt, heightPt) {
7001
7187
  return `${widthPt.toFixed(0)}×${heightPt.toFixed(0)}pt`;
7002
7188
  }
7003
- //#endregion
7004
- //#region src/tui/screens/editors/pdf/page-list.tsx
7005
- const LAYOUT_ITEM_KIND_ORDER = [
7006
- "text",
7007
- "image",
7008
- "rect",
7009
- "ellipse",
7010
- "line",
7011
- "path",
7012
- "link"
7013
- ];
7014
- function summariseItemKinds(items) {
7015
- const counts = /* @__PURE__ */ new Map();
7016
- for (const item of items) {
7017
- const current = counts.get(item.kind);
7018
- counts.set(item.kind, current === void 0 ? 1 : current + 1);
7019
- }
7189
+ function formatPt(value) {
7190
+ return value.toFixed(1);
7191
+ }
7192
+ function formatColor(color) {
7193
+ const byte = (component) => Math.round(component * 255).toString(16).padStart(2, "0");
7194
+ return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}`;
7195
+ }
7196
+ function formatStroke(stroke) {
7197
+ return `${formatColor(stroke.color)} @ ${stroke.widthPt.toFixed(1)}pt`;
7198
+ }
7199
+ function parseRequiredColorField(raw, fallback) {
7200
+ return parseColorField(raw) ?? fallback;
7201
+ }
7202
+ function parseFontWeight(raw) {
7203
+ return raw.trim().toLowerCase() === "bold" ? "bold" : "normal";
7204
+ }
7205
+ function parseFontStyle(raw) {
7206
+ return raw.trim().toLowerCase() === "italic" ? "italic" : "normal";
7207
+ }
7208
+ function parseOptionalNumberField(raw) {
7209
+ const trimmed = raw.trim();
7210
+ if (trimmed.length === 0) return;
7211
+ const parsed = Number.parseFloat(trimmed);
7212
+ return Number.isFinite(parsed) ? parsed : void 0;
7213
+ }
7214
+ function defaultTriangleLayoutSubpaths(widthPt, heightPt) {
7215
+ return [{
7216
+ startXPt: 0,
7217
+ startYPt: heightPt,
7218
+ segments: [{
7219
+ kind: "line",
7220
+ xPt: widthPt / 2,
7221
+ yPt: 0
7222
+ }, {
7223
+ kind: "line",
7224
+ xPt: widthPt,
7225
+ yPt: heightPt
7226
+ }],
7227
+ closed: true
7228
+ }];
7229
+ }
7230
+ function inferImageFormat(path) {
7231
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
7232
+ if (extension === "png") return "png";
7233
+ if (extension === "jpg" || extension === "jpeg") return "jpeg";
7234
+ }
7235
+ //#endregion
7236
+ //#region src/tui/screens/editors/pdf/page-list.tsx
7237
+ const LAYOUT_ITEM_KIND_ORDER = [
7238
+ "text",
7239
+ "image",
7240
+ "rect",
7241
+ "ellipse",
7242
+ "line",
7243
+ "path",
7244
+ "link"
7245
+ ];
7246
+ function summariseItemKinds(items) {
7247
+ const counts = /* @__PURE__ */ new Map();
7248
+ for (const item of items) {
7249
+ const current = counts.get(item.kind);
7250
+ counts.set(item.kind, current === void 0 ? 1 : current + 1);
7251
+ }
7020
7252
  const parts = [];
7021
7253
  for (const kind of LAYOUT_ITEM_KIND_ORDER) {
7022
7254
  const count = counts.get(kind);
@@ -7043,195 +7275,1036 @@ function PdfPageListScreen() {
7043
7275
  const entry = pages[index];
7044
7276
  if (entry === void 0) return;
7045
7277
  dispatch({
7046
- type: "PUSH_SCREEN",
7047
- screen: {
7048
- kind: "pdfPageItems",
7049
- pageIndex: entry.pageIndex
7278
+ type: "PUSH_SCREEN",
7279
+ screen: {
7280
+ kind: "pdfPageItems",
7281
+ pageIndex: entry.pageIndex
7282
+ }
7283
+ });
7284
+ },
7285
+ onBack: () => {
7286
+ dispatch({ type: "POP_SCREEN" });
7287
+ },
7288
+ isActive: !anyOverlayOpen(state)
7289
+ });
7290
+ return /* @__PURE__ */ jsxs(Box, {
7291
+ flexDirection: "column",
7292
+ children: [/* @__PURE__ */ jsxs(Text, {
7293
+ bold: true,
7294
+ children: [
7295
+ "Pages (",
7296
+ pages.length,
7297
+ " of ",
7298
+ doc.layout.pages.length,
7299
+ ")"
7300
+ ]
7301
+ }), /* @__PURE__ */ jsx(ListView, {
7302
+ items: pages,
7303
+ selectedIndex,
7304
+ emptyMessage: query === "" ? "This PDF has no pages." : `No pages match "${state.searchQuery}".`,
7305
+ renderItem: ({ page, pageIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
7306
+ color: isSelected ? "cyan" : void 0,
7307
+ inverse: isSelected,
7308
+ children: [
7309
+ "Page ",
7310
+ pageIndex + 1,
7311
+ " -- ",
7312
+ pageSummaryText(page)
7313
+ ]
7314
+ })
7315
+ })]
7316
+ });
7317
+ }
7318
+ //#endregion
7319
+ //#region src/tui/screens/editors/pdf/item-detail.tsx
7320
+ function formatPoint(xPt, yPt) {
7321
+ return `(${xPt.toFixed(1)}, ${yPt.toFixed(1)})pt`;
7322
+ }
7323
+ function fieldsFor(item) {
7324
+ const fields = [["Kind", item.kind]];
7325
+ switch (item.kind) {
7326
+ case "text":
7327
+ fields.push(["Text", item.text]);
7328
+ fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7329
+ fields.push(["Font family", item.font.family]);
7330
+ fields.push(["Font weight", item.font.weight]);
7331
+ fields.push(["Font style", item.font.style]);
7332
+ fields.push(["Size", `${item.sizePt}pt`]);
7333
+ fields.push(["Colour", formatColor(item.color)]);
7334
+ if (item.widthPt !== void 0) fields.push(["Width", `${item.widthPt}pt`]);
7335
+ if (item.rotationDeg !== void 0) fields.push(["Rotation", `${item.rotationDeg}°`]);
7336
+ if (item.underline !== void 0) fields.push(["Underline", item.underline ? "yes" : "no"]);
7337
+ break;
7338
+ case "image":
7339
+ fields.push(["Image ID", item.imageId]);
7340
+ fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7341
+ fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7342
+ if (item.rotationDeg !== void 0) fields.push(["Rotation", `${item.rotationDeg}°`]);
7343
+ break;
7344
+ case "rect":
7345
+ case "ellipse":
7346
+ fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7347
+ fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7348
+ if (item.fill !== void 0) fields.push(["Fill", formatColor(item.fill)]);
7349
+ if (item.stroke !== void 0) fields.push(["Stroke", formatStroke(item.stroke)]);
7350
+ break;
7351
+ case "line":
7352
+ fields.push(["From", formatPoint(item.x1Pt, item.y1Pt)]);
7353
+ fields.push(["To", formatPoint(item.x2Pt, item.y2Pt)]);
7354
+ fields.push(["Colour", formatColor(item.color)]);
7355
+ fields.push(["Width", `${item.widthPt}pt`]);
7356
+ break;
7357
+ case "path":
7358
+ fields.push(["Subpaths", `${item.subpaths.length}`]);
7359
+ fields.push(["Segments", `${item.subpaths.reduce((total, subpath) => total + subpath.segments.length, 0)}`]);
7360
+ if (item.fill !== void 0) fields.push(["Fill", formatColor(item.fill)]);
7361
+ if (item.fillRule !== void 0) fields.push(["Fill rule", item.fillRule]);
7362
+ if (item.stroke !== void 0) fields.push(["Stroke", formatStroke(item.stroke)]);
7363
+ break;
7364
+ case "link":
7365
+ fields.push(["URI", item.uri]);
7366
+ fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7367
+ fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7368
+ break;
7369
+ default: return item;
7370
+ }
7371
+ if (item.sourcePath !== void 0) fields.push(["Source path", item.sourcePath]);
7372
+ return fields;
7373
+ }
7374
+ function ReadOnlyItemDetail(props) {
7375
+ useInput((input, key) => {
7376
+ if (key.escape || key.leftArrow || input === "h") props.onBack();
7377
+ }, { isActive: props.isActive });
7378
+ return /* @__PURE__ */ jsxs(Box, {
7379
+ flexDirection: "column",
7380
+ children: [
7381
+ /* @__PURE__ */ jsxs(Text, {
7382
+ bold: true,
7383
+ children: [
7384
+ "Page ",
7385
+ props.pageIndex + 1,
7386
+ ", item ",
7387
+ props.itemIndex + 1
7388
+ ]
7389
+ }),
7390
+ fieldsFor(props.item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
7391
+ label,
7392
+ ": ",
7393
+ value
7394
+ ] }, label)),
7395
+ /* @__PURE__ */ jsx(Text, {
7396
+ dimColor: true,
7397
+ children: "Esc / ← / h to go back"
7398
+ })
7399
+ ]
7400
+ });
7401
+ }
7402
+ function buildFrameRows(frame, onFrameChange) {
7403
+ return [
7404
+ {
7405
+ label: `X: ${formatPt(frame.xPt)}pt`,
7406
+ currentValue: String(frame.xPt),
7407
+ commit: (raw) => onFrameChange({
7408
+ ...frame,
7409
+ xPt: parseNumberField(raw, frame.xPt)
7410
+ })
7411
+ },
7412
+ {
7413
+ label: `Y: ${formatPt(frame.yPt)}pt`,
7414
+ currentValue: String(frame.yPt),
7415
+ commit: (raw) => onFrameChange({
7416
+ ...frame,
7417
+ yPt: parseNumberField(raw, frame.yPt)
7418
+ })
7419
+ },
7420
+ {
7421
+ label: `Width: ${formatPt(frame.widthPt)}pt`,
7422
+ currentValue: String(frame.widthPt),
7423
+ commit: (raw) => onFrameChange({
7424
+ ...frame,
7425
+ widthPt: parseNumberField(raw, frame.widthPt)
7426
+ })
7427
+ },
7428
+ {
7429
+ label: `Height: ${formatPt(frame.heightPt)}pt`,
7430
+ currentValue: String(frame.heightPt),
7431
+ commit: (raw) => onFrameChange({
7432
+ ...frame,
7433
+ heightPt: parseNumberField(raw, frame.heightPt)
7434
+ })
7435
+ }
7436
+ ];
7437
+ }
7438
+ function buildFillStrokeRows(fill, stroke, onFillChange, onStrokeChange) {
7439
+ return [{
7440
+ label: `Fill: ${fill === void 0 ? "none" : formatColor(fill)}`,
7441
+ currentValue: fill === void 0 ? "" : `${fill.r} ${fill.g} ${fill.b}`,
7442
+ commit: (raw) => onFillChange(parseColorField(raw))
7443
+ }, {
7444
+ label: `Stroke: ${stroke === void 0 ? "none" : formatStroke(stroke)}`,
7445
+ currentValue: stroke === void 0 ? "" : `${stroke.color.r} ${stroke.color.g} ${stroke.color.b} ${stroke.widthPt}`,
7446
+ commit: (raw) => onStrokeChange(parseStrokeField(raw))
7447
+ }];
7448
+ }
7449
+ function buildTextRows(item, pageIndex, itemIndex, dispatch) {
7450
+ return [
7451
+ {
7452
+ label: `Text: ${item.text}`,
7453
+ currentValue: item.text,
7454
+ commit: (raw) => dispatch({
7455
+ type: "SET_PDF_TEXT_TEXT",
7456
+ pageIndex,
7457
+ itemIndex,
7458
+ text: raw
7459
+ })
7460
+ },
7461
+ {
7462
+ label: `X: ${formatPt(item.xPt)}pt`,
7463
+ currentValue: String(item.xPt),
7464
+ commit: (raw) => dispatch({
7465
+ type: "SET_PDF_TEXT_POSITION",
7466
+ pageIndex,
7467
+ itemIndex,
7468
+ xPt: parseNumberField(raw, item.xPt),
7469
+ yPt: item.yPt
7470
+ })
7471
+ },
7472
+ {
7473
+ label: `Y: ${formatPt(item.yPt)}pt`,
7474
+ currentValue: String(item.yPt),
7475
+ commit: (raw) => dispatch({
7476
+ type: "SET_PDF_TEXT_POSITION",
7477
+ pageIndex,
7478
+ itemIndex,
7479
+ xPt: item.xPt,
7480
+ yPt: parseNumberField(raw, item.yPt)
7481
+ })
7482
+ },
7483
+ {
7484
+ label: `Font family: ${item.font.family}`,
7485
+ currentValue: item.font.family,
7486
+ commit: (raw) => {
7487
+ const family = raw.trim();
7488
+ dispatch({
7489
+ type: "SET_PDF_TEXT_FONT",
7490
+ pageIndex,
7491
+ itemIndex,
7492
+ font: {
7493
+ ...item.font,
7494
+ family: family.length === 0 ? item.font.family : family
7495
+ }
7496
+ });
7497
+ }
7498
+ },
7499
+ {
7500
+ label: `Font weight: ${item.font.weight} (Enter to toggle)`,
7501
+ currentValue: "",
7502
+ activate: () => dispatch({
7503
+ type: "SET_PDF_TEXT_FONT",
7504
+ pageIndex,
7505
+ itemIndex,
7506
+ font: {
7507
+ ...item.font,
7508
+ weight: item.font.weight === "bold" ? "normal" : "bold"
7509
+ }
7510
+ })
7511
+ },
7512
+ {
7513
+ label: `Font style: ${item.font.style} (Enter to toggle)`,
7514
+ currentValue: "",
7515
+ activate: () => dispatch({
7516
+ type: "SET_PDF_TEXT_FONT",
7517
+ pageIndex,
7518
+ itemIndex,
7519
+ font: {
7520
+ ...item.font,
7521
+ style: item.font.style === "italic" ? "normal" : "italic"
7522
+ }
7523
+ })
7524
+ },
7525
+ {
7526
+ label: `Size: ${item.sizePt}pt`,
7527
+ currentValue: String(item.sizePt),
7528
+ commit: (raw) => dispatch({
7529
+ type: "SET_PDF_TEXT_SIZE",
7530
+ pageIndex,
7531
+ itemIndex,
7532
+ sizePt: parseNumberField(raw, item.sizePt)
7533
+ })
7534
+ },
7535
+ {
7536
+ label: `Colour: ${formatColor(item.color)}`,
7537
+ currentValue: `${item.color.r} ${item.color.g} ${item.color.b}`,
7538
+ commit: (raw) => dispatch({
7539
+ type: "SET_PDF_TEXT_COLOR",
7540
+ pageIndex,
7541
+ itemIndex,
7542
+ color: parseRequiredColorField(raw, item.color)
7543
+ })
7544
+ },
7545
+ {
7546
+ label: `Width: ${item.widthPt === void 0 ? "unset" : `${item.widthPt}pt`}`,
7547
+ currentValue: item.widthPt === void 0 ? "" : String(item.widthPt),
7548
+ commit: (raw) => dispatch({
7549
+ type: "SET_PDF_TEXT_WIDTH",
7550
+ pageIndex,
7551
+ itemIndex,
7552
+ widthPt: parseOptionalNumberField(raw)
7553
+ })
7554
+ },
7555
+ {
7556
+ label: `Rotation: ${item.rotationDeg === void 0 ? "unset" : `${item.rotationDeg}°`}`,
7557
+ currentValue: item.rotationDeg === void 0 ? "" : String(item.rotationDeg),
7558
+ commit: (raw) => dispatch({
7559
+ type: "SET_PDF_TEXT_ROTATION",
7560
+ pageIndex,
7561
+ itemIndex,
7562
+ rotationDeg: parseOptionalNumberField(raw)
7563
+ })
7564
+ },
7565
+ {
7566
+ label: `Underline: ${item.underline === true ? "yes" : "no"} (Enter to toggle)`,
7567
+ currentValue: "",
7568
+ activate: () => dispatch({
7569
+ type: "TOGGLE_PDF_TEXT_UNDERLINE",
7570
+ pageIndex,
7571
+ itemIndex
7572
+ })
7573
+ }
7574
+ ];
7575
+ }
7576
+ function buildRectRows(item, pageIndex, itemIndex, dispatch) {
7577
+ return [...buildFrameRows(item, (frame) => dispatch({
7578
+ type: "SET_PDF_RECT_FRAME",
7579
+ pageIndex,
7580
+ itemIndex,
7581
+ ...frame
7582
+ })), ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7583
+ type: "SET_PDF_RECT_FILL",
7584
+ pageIndex,
7585
+ itemIndex,
7586
+ fill
7587
+ }), (stroke) => dispatch({
7588
+ type: "SET_PDF_RECT_STROKE",
7589
+ pageIndex,
7590
+ itemIndex,
7591
+ stroke
7592
+ }))];
7593
+ }
7594
+ function buildEllipseRows(item, pageIndex, itemIndex, dispatch) {
7595
+ return [...buildFrameRows(item, (frame) => dispatch({
7596
+ type: "SET_PDF_ELLIPSE_FRAME",
7597
+ pageIndex,
7598
+ itemIndex,
7599
+ ...frame
7600
+ })), ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7601
+ type: "SET_PDF_ELLIPSE_FILL",
7602
+ pageIndex,
7603
+ itemIndex,
7604
+ fill
7605
+ }), (stroke) => dispatch({
7606
+ type: "SET_PDF_ELLIPSE_STROKE",
7607
+ pageIndex,
7608
+ itemIndex,
7609
+ stroke
7610
+ }))];
7611
+ }
7612
+ function buildLineRows(item, pageIndex, itemIndex, dispatch) {
7613
+ return [
7614
+ {
7615
+ label: `From X: ${formatPt(item.x1Pt)}pt`,
7616
+ currentValue: String(item.x1Pt),
7617
+ commit: (raw) => dispatch({
7618
+ type: "SET_PDF_LINE_FROM",
7619
+ pageIndex,
7620
+ itemIndex,
7621
+ x1Pt: parseNumberField(raw, item.x1Pt),
7622
+ y1Pt: item.y1Pt
7623
+ })
7624
+ },
7625
+ {
7626
+ label: `From Y: ${formatPt(item.y1Pt)}pt`,
7627
+ currentValue: String(item.y1Pt),
7628
+ commit: (raw) => dispatch({
7629
+ type: "SET_PDF_LINE_FROM",
7630
+ pageIndex,
7631
+ itemIndex,
7632
+ x1Pt: item.x1Pt,
7633
+ y1Pt: parseNumberField(raw, item.y1Pt)
7634
+ })
7635
+ },
7636
+ {
7637
+ label: `To X: ${formatPt(item.x2Pt)}pt`,
7638
+ currentValue: String(item.x2Pt),
7639
+ commit: (raw) => dispatch({
7640
+ type: "SET_PDF_LINE_TO",
7641
+ pageIndex,
7642
+ itemIndex,
7643
+ x2Pt: parseNumberField(raw, item.x2Pt),
7644
+ y2Pt: item.y2Pt
7645
+ })
7646
+ },
7647
+ {
7648
+ label: `To Y: ${formatPt(item.y2Pt)}pt`,
7649
+ currentValue: String(item.y2Pt),
7650
+ commit: (raw) => dispatch({
7651
+ type: "SET_PDF_LINE_TO",
7652
+ pageIndex,
7653
+ itemIndex,
7654
+ x2Pt: item.x2Pt,
7655
+ y2Pt: parseNumberField(raw, item.y2Pt)
7656
+ })
7657
+ },
7658
+ {
7659
+ label: `Colour: ${formatColor(item.color)}`,
7660
+ currentValue: `${item.color.r} ${item.color.g} ${item.color.b}`,
7661
+ commit: (raw) => dispatch({
7662
+ type: "SET_PDF_LINE_COLOR",
7663
+ pageIndex,
7664
+ itemIndex,
7665
+ color: parseRequiredColorField(raw, item.color)
7666
+ })
7667
+ },
7668
+ {
7669
+ label: `Width: ${item.widthPt}pt`,
7670
+ currentValue: String(item.widthPt),
7671
+ commit: (raw) => dispatch({
7672
+ type: "SET_PDF_LINE_WIDTH",
7673
+ pageIndex,
7674
+ itemIndex,
7675
+ widthPt: Math.max(parseNumberField(raw, item.widthPt), Number.EPSILON)
7676
+ })
7677
+ }
7678
+ ];
7679
+ }
7680
+ function buildPathRows(item, pageIndex, itemIndex, dispatch) {
7681
+ return [{
7682
+ label: `Fill rule: ${item.fillRule ?? "nonzero (default)"} (Enter to cycle)`,
7683
+ currentValue: "",
7684
+ activate: () => dispatch({
7685
+ type: "SET_PDF_PATH_FILL_RULE",
7686
+ pageIndex,
7687
+ itemIndex,
7688
+ fillRule: item.fillRule === "evenodd" ? void 0 : item.fillRule === "nonzero" ? "evenodd" : "nonzero"
7689
+ })
7690
+ }, ...buildFillStrokeRows(item.fill, item.stroke, (fill) => dispatch({
7691
+ type: "SET_PDF_PATH_FILL",
7692
+ pageIndex,
7693
+ itemIndex,
7694
+ fill
7695
+ }), (stroke) => dispatch({
7696
+ type: "SET_PDF_PATH_STROKE",
7697
+ pageIndex,
7698
+ itemIndex,
7699
+ stroke
7700
+ }))];
7701
+ }
7702
+ function buildImageRows(item, pageIndex, itemIndex, dispatch, onReplaceImage) {
7703
+ return [
7704
+ ...buildFrameRows(item, (frame) => dispatch({
7705
+ type: "SET_PDF_IMAGE_FRAME",
7706
+ pageIndex,
7707
+ itemIndex,
7708
+ ...frame
7709
+ })),
7710
+ {
7711
+ label: `Rotation: ${item.rotationDeg === void 0 ? "unset" : `${item.rotationDeg}°`}`,
7712
+ currentValue: item.rotationDeg === void 0 ? "" : String(item.rotationDeg),
7713
+ commit: (raw) => dispatch({
7714
+ type: "SET_PDF_IMAGE_ROTATION",
7715
+ pageIndex,
7716
+ itemIndex,
7717
+ rotationDeg: parseOptionalNumberField(raw)
7718
+ })
7719
+ },
7720
+ {
7721
+ label: "Replace image...",
7722
+ currentValue: "",
7723
+ activate: onReplaceImage
7724
+ }
7725
+ ];
7726
+ }
7727
+ function buildLinkRows(item, pageIndex, itemIndex, dispatch) {
7728
+ return [{
7729
+ label: `URI: ${item.uri}`,
7730
+ currentValue: item.uri,
7731
+ commit: (raw) => dispatch({
7732
+ type: "SET_PDF_LINK_URI",
7733
+ pageIndex,
7734
+ itemIndex,
7735
+ uri: raw
7736
+ })
7737
+ }, ...buildFrameRows(item, (frame) => dispatch({
7738
+ type: "SET_PDF_LINK_FRAME",
7739
+ pageIndex,
7740
+ itemIndex,
7741
+ ...frame
7742
+ }))];
7743
+ }
7744
+ function buildRowsFor(item, pageIndex, itemIndex, dispatch, onReplaceImage) {
7745
+ switch (item.kind) {
7746
+ case "text": return buildTextRows(item, pageIndex, itemIndex, dispatch);
7747
+ case "rect": return buildRectRows(item, pageIndex, itemIndex, dispatch);
7748
+ case "ellipse": return buildEllipseRows(item, pageIndex, itemIndex, dispatch);
7749
+ case "line": return buildLineRows(item, pageIndex, itemIndex, dispatch);
7750
+ case "path": return buildPathRows(item, pageIndex, itemIndex, dispatch);
7751
+ case "image": return buildImageRows(item, pageIndex, itemIndex, dispatch, onReplaceImage);
7752
+ case "link": return buildLinkRows(item, pageIndex, itemIndex, dispatch);
7753
+ }
7754
+ }
7755
+ function pathSummary(item) {
7756
+ const segmentCount = item.subpaths.reduce((total, subpath) => total + subpath.segments.length, 0);
7757
+ return `${item.subpaths.length} subpath${item.subpaths.length === 1 ? "" : "s"}, ${segmentCount} segment${segmentCount === 1 ? "" : "s"} (not editable here -- see documents.js's own PdfPathItem doc comment)`;
7758
+ }
7759
+ async function applyImageReplace(pageIndex, itemIndex, path, dispatch) {
7760
+ const format = inferImageFormat(path);
7761
+ if (format === void 0) {
7762
+ dispatch({
7763
+ type: "SET_STATUS",
7764
+ severity: "warning",
7765
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not replaced`
7766
+ });
7767
+ return;
7768
+ }
7769
+ try {
7770
+ dispatch({
7771
+ type: "SET_PDF_IMAGE_SOURCE",
7772
+ pageIndex,
7773
+ itemIndex,
7774
+ format,
7775
+ bytes: new Uint8Array(await readInput(path))
7776
+ });
7777
+ } catch (error) {
7778
+ dispatch({
7779
+ type: "SET_STATUS",
7780
+ severity: "error",
7781
+ text: `Could not read ${path}: ${describeError(error)}`
7782
+ });
7783
+ }
7784
+ }
7785
+ function EditableItemDetail(props) {
7786
+ const dispatch = useAppDispatch();
7787
+ const { doc, pageIndex, itemIndex } = props;
7788
+ const [editingField, setEditingField] = useState(void 0);
7789
+ const [draft, setDraft] = useState("");
7790
+ const [replacingImage, setReplacingImage] = useState(false);
7791
+ const item = doc.editor.page(pageIndex)?.items()[itemIndex];
7792
+ const rows = item === void 0 ? [] : buildRowsFor(item, pageIndex, itemIndex, dispatch, () => setReplacingImage(true));
7793
+ const { selectedIndex } = useNavigationInput({
7794
+ itemCount: rows.length,
7795
+ isActive: props.isActive && editingField === void 0 && !replacingImage,
7796
+ onBack: () => {
7797
+ dispatch({ type: "POP_SCREEN" });
7798
+ },
7799
+ onSelect: (index) => {
7800
+ const row = rows[index];
7801
+ if (row === void 0) return;
7802
+ if (row.activate !== void 0) {
7803
+ row.activate();
7804
+ return;
7805
+ }
7806
+ setDraft(row.currentValue);
7807
+ setEditingField(index);
7808
+ }
7809
+ });
7810
+ if (item === void 0) return /* @__PURE__ */ jsxs(Box, {
7811
+ flexDirection: "column",
7812
+ children: [/* @__PURE__ */ jsxs(Text, {
7813
+ color: "yellow",
7814
+ children: [
7815
+ "There is no item ",
7816
+ itemIndex + 1,
7817
+ " on page ",
7818
+ pageIndex + 1,
7819
+ " any more."
7820
+ ]
7821
+ }), /* @__PURE__ */ jsx(Text, {
7822
+ dimColor: true,
7823
+ children: "Esc to go back"
7824
+ })]
7825
+ });
7826
+ if (replacingImage) return /* @__PURE__ */ jsx(FieldWizard, {
7827
+ fields: [{
7828
+ key: "path",
7829
+ label: "Image file path (.png/.jpg/.jpeg)",
7830
+ defaultValue: ""
7831
+ }],
7832
+ onCancel: () => {
7833
+ setReplacingImage(false);
7834
+ },
7835
+ onComplete: (values) => {
7836
+ applyImageReplace(pageIndex, itemIndex, requireFieldValue(values, "path"), dispatch).then(() => {
7837
+ setReplacingImage(false);
7838
+ });
7839
+ }
7840
+ });
7841
+ if (editingField !== void 0) {
7842
+ const row = rows[editingField];
7843
+ if (row === void 0) throw new Error(`EditableItemDetail is editing field index ${editingField}, but there are only ${rows.length} rows -- selecting a row always sets editingField to a valid index from that same rows array, so this indicates a bug in that selection.`);
7844
+ return /* @__PURE__ */ jsxs(Box, {
7845
+ flexDirection: "column",
7846
+ borderStyle: "round",
7847
+ paddingX: 1,
7848
+ children: [/* @__PURE__ */ jsx(Text, {
7849
+ bold: true,
7850
+ children: row.label
7851
+ }), /* @__PURE__ */ jsx(TextField, {
7852
+ value: draft,
7853
+ isFocused: true,
7854
+ onChange: setDraft,
7855
+ onCancel: () => {
7856
+ setEditingField(void 0);
7857
+ },
7858
+ onSubmit: (value) => {
7859
+ row.commit?.(value);
7860
+ setEditingField(void 0);
7861
+ }
7862
+ })]
7863
+ });
7864
+ }
7865
+ return /* @__PURE__ */ jsxs(Box, {
7866
+ flexDirection: "column",
7867
+ children: [
7868
+ /* @__PURE__ */ jsxs(Text, {
7869
+ bold: true,
7870
+ children: [
7871
+ "Page ",
7872
+ pageIndex + 1,
7873
+ ", item ",
7874
+ itemIndex + 1,
7875
+ " -- ",
7876
+ item.kind
7877
+ ]
7878
+ }),
7879
+ item.kind === "path" && /* @__PURE__ */ jsx(Text, {
7880
+ dimColor: true,
7881
+ children: pathSummary(item)
7882
+ }),
7883
+ item.kind === "image" && /* @__PURE__ */ jsxs(Text, {
7884
+ dimColor: true,
7885
+ children: ["Image ID: ", item.imageId]
7886
+ }),
7887
+ /* @__PURE__ */ jsx(ListView, {
7888
+ items: rows,
7889
+ selectedIndex,
7890
+ reservedRows: 5,
7891
+ renderItem: (row, isSelected) => /* @__PURE__ */ jsx(Text, {
7892
+ color: isSelected ? "cyan" : void 0,
7893
+ inverse: isSelected,
7894
+ children: row.label
7895
+ })
7896
+ }),
7897
+ /* @__PURE__ */ jsx(Text, {
7898
+ dimColor: true,
7899
+ children: "Enter to edit a field, Esc to go back"
7900
+ })
7901
+ ]
7902
+ });
7903
+ }
7904
+ function PdfItemDetailScreen() {
7905
+ const state = useAppState();
7906
+ const dispatch = useAppDispatch();
7907
+ const doc = requirePdfDocument(state.openDocument);
7908
+ const screen = currentScreen(state);
7909
+ if (screen.kind !== "pdfItemDetail") throw new Error(`PdfItemDetailScreen rendered while the current screen is "${screen.kind}", not "pdfItemDetail".`);
7910
+ const isActive = !anyOverlayOpen(state);
7911
+ if (!isEditablePdfDocument(doc)) {
7912
+ const page = doc.layout.pages[screen.pageIndex];
7913
+ if (page === void 0) throw new Error(`pdfItemDetail was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
7914
+ const item = page.items[screen.itemIndex];
7915
+ if (item === void 0) throw new Error(`pdfItemDetail was pushed for item ${screen.itemIndex} on page ${screen.pageIndex}, but that page has no item at that index.`);
7916
+ return /* @__PURE__ */ jsx(ReadOnlyItemDetail, {
7917
+ item,
7918
+ pageIndex: screen.pageIndex,
7919
+ itemIndex: screen.itemIndex,
7920
+ isActive,
7921
+ onBack: () => {
7922
+ dispatch({ type: "POP_SCREEN" });
7923
+ }
7924
+ });
7925
+ }
7926
+ return /* @__PURE__ */ jsx(EditableItemDetail, {
7927
+ doc,
7928
+ pageIndex: screen.pageIndex,
7929
+ itemIndex: screen.itemIndex,
7930
+ isActive
7931
+ });
7932
+ }
7933
+ //#endregion
7934
+ //#region src/tui/screens/editors/pdf/page-items.tsx
7935
+ const TEXT_PREVIEW_MAX_CHARS = 48;
7936
+ function truncate(text, maxChars) {
7937
+ return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
7938
+ }
7939
+ function pathDimensions(item) {
7940
+ let minX = Number.POSITIVE_INFINITY;
7941
+ let minY = Number.POSITIVE_INFINITY;
7942
+ let maxX = Number.NEGATIVE_INFINITY;
7943
+ let maxY = Number.NEGATIVE_INFINITY;
7944
+ const consider = (xPt, yPt) => {
7945
+ minX = Math.min(minX, xPt);
7946
+ minY = Math.min(minY, yPt);
7947
+ maxX = Math.max(maxX, xPt);
7948
+ maxY = Math.max(maxY, yPt);
7949
+ };
7950
+ for (const subpath of item.subpaths) {
7951
+ consider(subpath.startXPt, subpath.startYPt);
7952
+ for (const segment of subpath.segments) {
7953
+ if (segment.kind === "cubic") {
7954
+ consider(segment.c1xPt, segment.c1yPt);
7955
+ consider(segment.c2xPt, segment.c2yPt);
7956
+ }
7957
+ consider(segment.xPt, segment.yPt);
7958
+ }
7959
+ }
7960
+ if (minX > maxX) return "empty path";
7961
+ return formatSize(maxX - minX, maxY - minY);
7962
+ }
7963
+ function previewFor(item) {
7964
+ switch (item.kind) {
7965
+ case "text": return truncate(item.text, TEXT_PREVIEW_MAX_CHARS);
7966
+ case "link": return item.uri;
7967
+ case "image":
7968
+ case "rect":
7969
+ case "ellipse": return formatSize(item.widthPt, item.heightPt);
7970
+ case "line": return formatSize(Math.abs(item.x2Pt - item.x1Pt), Math.abs(item.y2Pt - item.y1Pt));
7971
+ case "path": return pathDimensions(item);
7972
+ }
7973
+ }
7974
+ const ADD_KIND_OPTIONS = [
7975
+ {
7976
+ kind: "text",
7977
+ label: "Text"
7978
+ },
7979
+ {
7980
+ kind: "rect",
7981
+ label: "Rectangle"
7982
+ },
7983
+ {
7984
+ kind: "ellipse",
7985
+ label: "Ellipse"
7986
+ },
7987
+ {
7988
+ kind: "line",
7989
+ label: "Line"
7990
+ },
7991
+ {
7992
+ kind: "path",
7993
+ label: "Path (fixed triangle shape)"
7994
+ },
7995
+ {
7996
+ kind: "image",
7997
+ label: "Image"
7998
+ },
7999
+ {
8000
+ kind: "link",
8001
+ label: "Link"
8002
+ }
8003
+ ];
8004
+ const GEOMETRY_FIELDS = [
8005
+ {
8006
+ key: "xPt",
8007
+ label: "X (pt)",
8008
+ defaultValue: "40"
8009
+ },
8010
+ {
8011
+ key: "yPt",
8012
+ label: "Y (pt)",
8013
+ defaultValue: "40"
8014
+ },
8015
+ {
8016
+ key: "widthPt",
8017
+ label: "Width (pt)",
8018
+ defaultValue: "160"
8019
+ },
8020
+ {
8021
+ key: "heightPt",
8022
+ label: "Height (pt)",
8023
+ defaultValue: "100"
8024
+ }
8025
+ ];
8026
+ const FILL_FIELD = {
8027
+ key: "fill",
8028
+ label: "Fill \"r g b\" (0-1 each), blank for none",
8029
+ defaultValue: "0.8 0.8 0.8"
8030
+ };
8031
+ const STROKE_FIELD = {
8032
+ key: "stroke",
8033
+ label: "Stroke \"r g b widthPt\" (0-1 colour, pt width), blank for none",
8034
+ defaultValue: "0 0 0 1"
8035
+ };
8036
+ const REQUIRED_COLOR_FIELD = {
8037
+ key: "color",
8038
+ label: "Colour \"r g b\" (0-1 each)",
8039
+ defaultValue: "0 0 0"
8040
+ };
8041
+ function fieldsForAddKind(kind) {
8042
+ switch (kind) {
8043
+ case "text": return [
8044
+ {
8045
+ key: "xPt",
8046
+ label: "X (pt)",
8047
+ defaultValue: "40"
8048
+ },
8049
+ {
8050
+ key: "yPt",
8051
+ label: "Y (pt)",
8052
+ defaultValue: "40"
8053
+ },
8054
+ {
8055
+ key: "text",
8056
+ label: "Text",
8057
+ defaultValue: "Text"
8058
+ },
8059
+ {
8060
+ key: "fontFamily",
8061
+ label: "Font family",
8062
+ defaultValue: "Helvetica"
8063
+ },
8064
+ {
8065
+ key: "fontWeight",
8066
+ label: "Font weight (normal/bold)",
8067
+ defaultValue: "normal"
8068
+ },
8069
+ {
8070
+ key: "fontStyle",
8071
+ label: "Font style (normal/italic)",
8072
+ defaultValue: "normal"
8073
+ },
8074
+ {
8075
+ key: "sizePt",
8076
+ label: "Size (pt)",
8077
+ defaultValue: "12"
8078
+ },
8079
+ REQUIRED_COLOR_FIELD
8080
+ ];
8081
+ case "rect":
8082
+ case "ellipse":
8083
+ case "path": return [
8084
+ ...GEOMETRY_FIELDS,
8085
+ FILL_FIELD,
8086
+ STROKE_FIELD
8087
+ ];
8088
+ case "line": return [
8089
+ {
8090
+ key: "fromXPt",
8091
+ label: "From X (pt)",
8092
+ defaultValue: "40"
8093
+ },
8094
+ {
8095
+ key: "fromYPt",
8096
+ label: "From Y (pt)",
8097
+ defaultValue: "40"
8098
+ },
8099
+ {
8100
+ key: "toXPt",
8101
+ label: "To X (pt)",
8102
+ defaultValue: "200"
8103
+ },
8104
+ {
8105
+ key: "toYPt",
8106
+ label: "To Y (pt)",
8107
+ defaultValue: "40"
8108
+ },
8109
+ REQUIRED_COLOR_FIELD,
8110
+ {
8111
+ key: "widthPt",
8112
+ label: "Width (pt)",
8113
+ defaultValue: "1"
8114
+ }
8115
+ ];
8116
+ case "image": return [...GEOMETRY_FIELDS, {
8117
+ key: "path",
8118
+ label: "Image file path (.png/.jpg/.jpeg)",
8119
+ defaultValue: ""
8120
+ }];
8121
+ case "link": return [{
8122
+ key: "uri",
8123
+ label: "URI",
8124
+ defaultValue: "https://example.com"
8125
+ }, ...GEOMETRY_FIELDS];
8126
+ }
8127
+ }
8128
+ function readFrame(values) {
8129
+ return {
8130
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
8131
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
8132
+ widthPt: parseNumberField(requireFieldValue(values, "widthPt"), 100),
8133
+ heightPt: parseNumberField(requireFieldValue(values, "heightPt"), 60)
8134
+ };
8135
+ }
8136
+ async function applyAddKind(kind, pageIndex, values, dispatch) {
8137
+ switch (kind) {
8138
+ case "text":
8139
+ dispatch({
8140
+ type: "ADD_PDF_TEXT",
8141
+ pageIndex,
8142
+ init: {
8143
+ xPt: parseNumberField(requireFieldValue(values, "xPt"), 0),
8144
+ yPt: parseNumberField(requireFieldValue(values, "yPt"), 0),
8145
+ text: requireFieldValue(values, "text"),
8146
+ font: {
8147
+ family: requireFieldValue(values, "fontFamily").trim() || "Helvetica",
8148
+ weight: parseFontWeight(requireFieldValue(values, "fontWeight")),
8149
+ style: parseFontStyle(requireFieldValue(values, "fontStyle"))
8150
+ },
8151
+ sizePt: Math.max(parseNumberField(requireFieldValue(values, "sizePt"), 12), Number.EPSILON),
8152
+ color: parseColorField(requireFieldValue(values, "color")) ?? {
8153
+ r: 0,
8154
+ g: 0,
8155
+ b: 0
8156
+ }
8157
+ }
8158
+ });
8159
+ return;
8160
+ case "rect":
8161
+ dispatch({
8162
+ type: "ADD_PDF_RECT",
8163
+ pageIndex,
8164
+ init: {
8165
+ ...readFrame(values),
8166
+ fill: parseColorField(requireFieldValue(values, "fill")),
8167
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8168
+ }
8169
+ });
8170
+ return;
8171
+ case "ellipse":
8172
+ dispatch({
8173
+ type: "ADD_PDF_ELLIPSE",
8174
+ pageIndex,
8175
+ init: {
8176
+ ...readFrame(values),
8177
+ fill: parseColorField(requireFieldValue(values, "fill")),
8178
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8179
+ }
8180
+ });
8181
+ return;
8182
+ case "line":
8183
+ dispatch({
8184
+ type: "ADD_PDF_LINE",
8185
+ pageIndex,
8186
+ init: {
8187
+ x1Pt: parseNumberField(requireFieldValue(values, "fromXPt"), 0),
8188
+ y1Pt: parseNumberField(requireFieldValue(values, "fromYPt"), 0),
8189
+ x2Pt: parseNumberField(requireFieldValue(values, "toXPt"), 100),
8190
+ y2Pt: parseNumberField(requireFieldValue(values, "toYPt"), 0),
8191
+ color: parseColorField(requireFieldValue(values, "color")) ?? {
8192
+ r: 0,
8193
+ g: 0,
8194
+ b: 0
8195
+ },
8196
+ widthPt: Math.max(parseNumberField(requireFieldValue(values, "widthPt"), 1), Number.EPSILON)
8197
+ }
8198
+ });
8199
+ return;
8200
+ case "path": {
8201
+ const frame = readFrame(values);
8202
+ dispatch({
8203
+ type: "ADD_PDF_PATH",
8204
+ pageIndex,
8205
+ init: {
8206
+ subpaths: defaultTriangleLayoutSubpaths(frame.widthPt, frame.heightPt),
8207
+ fill: parseColorField(requireFieldValue(values, "fill")),
8208
+ stroke: parseStrokeField(requireFieldValue(values, "stroke"))
8209
+ }
8210
+ });
8211
+ return;
8212
+ }
8213
+ case "image": {
8214
+ const frame = readFrame(values);
8215
+ const path = requireFieldValue(values, "path");
8216
+ const format = inferImageFormat(path);
8217
+ if (format === void 0) {
8218
+ dispatch({
8219
+ type: "SET_STATUS",
8220
+ severity: "warning",
8221
+ text: `${path} is not a .png or .jpg/.jpeg file -- image not added`
8222
+ });
8223
+ return;
8224
+ }
8225
+ try {
8226
+ const bytes = new Uint8Array(await readInput(path));
8227
+ dispatch({
8228
+ type: "ADD_PDF_IMAGE",
8229
+ pageIndex,
8230
+ init: {
8231
+ ...frame,
8232
+ format,
8233
+ bytes
8234
+ }
8235
+ });
8236
+ } catch (error) {
8237
+ dispatch({
8238
+ type: "SET_STATUS",
8239
+ severity: "error",
8240
+ text: `Could not read ${path}: ${describeError(error)}`
8241
+ });
8242
+ }
8243
+ return;
8244
+ }
8245
+ case "link": {
8246
+ const frame = readFrame(values);
8247
+ dispatch({
8248
+ type: "ADD_PDF_LINK",
8249
+ pageIndex,
8250
+ init: {
8251
+ uri: requireFieldValue(values, "uri"),
8252
+ ...frame
7050
8253
  }
7051
8254
  });
7052
- },
7053
- onBack: () => {
7054
- dispatch({ type: "POP_SCREEN" });
7055
- },
7056
- isActive: !anyOverlayOpen(state)
8255
+ return;
8256
+ }
8257
+ }
8258
+ }
8259
+ function AddItemFlow(props) {
8260
+ const dispatch = useAppDispatch();
8261
+ const [kind, setKind] = useState(void 0);
8262
+ const { selectedIndex } = useNavigationInput({
8263
+ itemCount: ADD_KIND_OPTIONS.length,
8264
+ isActive: props.isActive && kind === void 0,
8265
+ onBack: props.onCancel,
8266
+ onSelect: (index) => {
8267
+ const option = ADD_KIND_OPTIONS[index];
8268
+ if (option === void 0) return;
8269
+ setKind(option.kind);
8270
+ }
7057
8271
  });
7058
- return /* @__PURE__ */ jsxs(Box, {
8272
+ if (kind === void 0) return /* @__PURE__ */ jsxs(Box, {
7059
8273
  flexDirection: "column",
7060
- children: [/* @__PURE__ */ jsxs(Text, {
8274
+ borderStyle: "round",
8275
+ paddingX: 1,
8276
+ children: [/* @__PURE__ */ jsx(Text, {
7061
8277
  bold: true,
7062
- children: [
7063
- "Pages (",
7064
- pages.length,
7065
- " of ",
7066
- doc.layout.pages.length,
7067
- ")"
7068
- ]
8278
+ children: "Add item -- choose a kind"
7069
8279
  }), /* @__PURE__ */ jsx(ListView, {
7070
- items: pages,
8280
+ items: ADD_KIND_OPTIONS,
7071
8281
  selectedIndex,
7072
- emptyMessage: query === "" ? "This PDF has no pages." : `No pages match "${state.searchQuery}".`,
7073
- renderItem: ({ page, pageIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
8282
+ reservedRows: 6,
8283
+ renderItem: (option, isSelected) => /* @__PURE__ */ jsxs(Text, {
7074
8284
  color: isSelected ? "cyan" : void 0,
7075
- inverse: isSelected,
7076
- children: [
7077
- "Page ",
7078
- pageIndex + 1,
7079
- " -- ",
7080
- pageSummaryText(page)
7081
- ]
8285
+ children: [isSelected ? "> " : " ", option.label]
7082
8286
  })
7083
8287
  })]
7084
8288
  });
7085
- }
7086
- //#endregion
7087
- //#region src/tui/screens/editors/pdf/item-detail.tsx
7088
- function formatPoint(xPt, yPt) {
7089
- return `(${xPt.toFixed(1)}, ${yPt.toFixed(1)})pt`;
7090
- }
7091
- function formatColor(color) {
7092
- const byte = (component) => Math.round(component * 255).toString(16).padStart(2, "0");
7093
- return `#${byte(color.r)}${byte(color.g)}${byte(color.b)}`;
7094
- }
7095
- function formatStroke(stroke) {
7096
- return `${formatColor(stroke.color)} @ ${stroke.widthPt.toFixed(1)}pt`;
7097
- }
7098
- function fieldsFor(item) {
7099
- const fields = [["Kind", item.kind]];
7100
- switch (item.kind) {
7101
- case "text":
7102
- fields.push(["Text", item.text]);
7103
- fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7104
- fields.push(["Font family", item.font.family]);
7105
- fields.push(["Font weight", item.font.weight]);
7106
- fields.push(["Font style", item.font.style]);
7107
- fields.push(["Size", `${item.sizePt}pt`]);
7108
- fields.push(["Colour", formatColor(item.color)]);
7109
- if (item.widthPt !== void 0) fields.push(["Width", `${item.widthPt}pt`]);
7110
- if (item.rotationDeg !== void 0) fields.push(["Rotation", `${item.rotationDeg}°`]);
7111
- if (item.underline !== void 0) fields.push(["Underline", item.underline ? "yes" : "no"]);
7112
- break;
7113
- case "image":
7114
- fields.push(["Image ID", item.imageId]);
7115
- fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7116
- fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7117
- if (item.rotationDeg !== void 0) fields.push(["Rotation", `${item.rotationDeg}°`]);
7118
- break;
7119
- case "rect":
7120
- case "ellipse":
7121
- fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7122
- fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7123
- if (item.fill !== void 0) fields.push(["Fill", formatColor(item.fill)]);
7124
- if (item.stroke !== void 0) fields.push(["Stroke", formatStroke(item.stroke)]);
7125
- break;
7126
- case "line":
7127
- fields.push(["From", formatPoint(item.x1Pt, item.y1Pt)]);
7128
- fields.push(["To", formatPoint(item.x2Pt, item.y2Pt)]);
7129
- fields.push(["Colour", formatColor(item.color)]);
7130
- fields.push(["Width", `${item.widthPt}pt`]);
7131
- break;
7132
- case "path":
7133
- fields.push(["Subpaths", `${item.subpaths.length}`]);
7134
- fields.push(["Segments", `${item.subpaths.reduce((total, subpath) => total + subpath.segments.length, 0)}`]);
7135
- if (item.fill !== void 0) fields.push(["Fill", formatColor(item.fill)]);
7136
- if (item.fillRule !== void 0) fields.push(["Fill rule", item.fillRule]);
7137
- if (item.stroke !== void 0) fields.push(["Stroke", formatStroke(item.stroke)]);
7138
- break;
7139
- case "link":
7140
- fields.push(["URI", item.uri]);
7141
- fields.push(["Position", formatPoint(item.xPt, item.yPt)]);
7142
- fields.push(["Size", formatSize(item.widthPt, item.heightPt)]);
7143
- break;
7144
- default: return item;
7145
- }
7146
- if (item.sourcePath !== void 0) fields.push(["Source path", item.sourcePath]);
7147
- return fields;
7148
- }
7149
- function PdfItemDetailScreen() {
7150
- const state = useAppState();
7151
- const dispatch = useAppDispatch();
7152
- const doc = requirePdfDocument(state.openDocument);
7153
- const screen = currentScreen(state);
7154
- if (screen.kind !== "pdfItemDetail") throw new Error(`PdfItemDetailScreen rendered while the current screen is "${screen.kind}", not "pdfItemDetail".`);
7155
- const page = doc.layout.pages[screen.pageIndex];
7156
- if (page === void 0) throw new Error(`pdfItemDetail was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
7157
- const item = page.items[screen.itemIndex];
7158
- if (item === void 0) throw new Error(`pdfItemDetail was pushed for item ${screen.itemIndex} on page ${screen.pageIndex}, but that page has no item at that index.`);
7159
- useInput((input, key) => {
7160
- if (key.escape || key.leftArrow || input === "h") dispatch({ type: "POP_SCREEN" });
7161
- }, { isActive: !anyOverlayOpen(state) });
7162
- return /* @__PURE__ */ jsxs(Box, {
7163
- flexDirection: "column",
7164
- children: [
7165
- /* @__PURE__ */ jsxs(Text, {
7166
- bold: true,
7167
- children: [
7168
- "Page ",
7169
- screen.pageIndex + 1,
7170
- ", item ",
7171
- screen.itemIndex + 1
7172
- ]
7173
- }),
7174
- fieldsFor(item).map(([label, value]) => /* @__PURE__ */ jsxs(Text, { children: [
7175
- label,
7176
- ": ",
7177
- value
7178
- ] }, label)),
7179
- /* @__PURE__ */ jsx(Text, {
7180
- dimColor: true,
7181
- children: "Esc / ← / h to go back"
7182
- })
7183
- ]
7184
- });
7185
- }
7186
- //#endregion
7187
- //#region src/tui/screens/editors/pdf/page-items.tsx
7188
- const TEXT_PREVIEW_MAX_CHARS = 48;
7189
- function truncate(text, maxChars) {
7190
- return text.length > maxChars ? `${text.slice(0, maxChars)}…` : text;
7191
- }
7192
- function pathDimensions(item) {
7193
- let minX = Number.POSITIVE_INFINITY;
7194
- let minY = Number.POSITIVE_INFINITY;
7195
- let maxX = Number.NEGATIVE_INFINITY;
7196
- let maxY = Number.NEGATIVE_INFINITY;
7197
- const consider = (xPt, yPt) => {
7198
- minX = Math.min(minX, xPt);
7199
- minY = Math.min(minY, yPt);
7200
- maxX = Math.max(maxX, xPt);
7201
- maxY = Math.max(maxY, yPt);
7202
- };
7203
- for (const subpath of item.subpaths) {
7204
- consider(subpath.startXPt, subpath.startYPt);
7205
- for (const segment of subpath.segments) {
7206
- if (segment.kind === "cubic") {
7207
- consider(segment.c1xPt, segment.c1yPt);
7208
- consider(segment.c2xPt, segment.c2yPt);
7209
- }
7210
- consider(segment.xPt, segment.yPt);
8289
+ return /* @__PURE__ */ jsx(FieldWizard, {
8290
+ fields: fieldsForAddKind(kind),
8291
+ onCancel: props.onCancel,
8292
+ onComplete: (values) => {
8293
+ applyAddKind(kind, props.pageIndex, values, dispatch).then(props.onCreated);
7211
8294
  }
7212
- }
7213
- if (minX > maxX) return "empty path";
7214
- return formatSize(maxX - minX, maxY - minY);
7215
- }
7216
- function previewFor(item) {
7217
- switch (item.kind) {
7218
- case "text": return truncate(item.text, TEXT_PREVIEW_MAX_CHARS);
7219
- case "link": return item.uri;
7220
- case "image":
7221
- case "rect":
7222
- case "ellipse": return formatSize(item.widthPt, item.heightPt);
7223
- case "line": return formatSize(Math.abs(item.x2Pt - item.x1Pt), Math.abs(item.y2Pt - item.y1Pt));
7224
- case "path": return pathDimensions(item);
7225
- }
8295
+ });
7226
8296
  }
7227
8297
  function PdfPageItemsScreen() {
7228
8298
  const state = useAppState();
7229
8299
  const dispatch = useAppDispatch();
7230
8300
  const doc = requirePdfDocument(state.openDocument);
8301
+ const editable = isEditablePdfDocument(doc);
7231
8302
  const screen = currentScreen(state);
7232
8303
  if (screen.kind !== "pdfPageItems") throw new Error(`PdfPageItemsScreen rendered while the current screen is "${screen.kind}", not "pdfPageItems".`);
7233
8304
  const page = doc.layout.pages[screen.pageIndex];
7234
8305
  if (page === void 0) throw new Error(`pdfPageItems was pushed for page ${screen.pageIndex}, but the open PDF has no page at that index.`);
8306
+ const [isAdding, setIsAdding] = useState(false);
8307
+ const overlayOpen = anyOverlayOpen(state);
7235
8308
  const query = state.searchQuery.trim().toLowerCase();
7236
8309
  const indexed = page.items.map((item, itemIndex) => ({
7237
8310
  item,
@@ -7240,6 +8313,7 @@ function PdfPageItemsScreen() {
7240
8313
  const items = query === "" ? indexed : indexed.filter((entry) => `${entry.item.kind} ${previewFor(entry.item)}`.toLowerCase().includes(query));
7241
8314
  const { selectedIndex } = useNavigationInput({
7242
8315
  itemCount: items.length,
8316
+ isActive: !overlayOpen && !isAdding,
7243
8317
  onSelect: (index) => {
7244
8318
  const entry = items[index];
7245
8319
  if (entry === void 0) return;
@@ -7255,37 +8329,66 @@ function PdfPageItemsScreen() {
7255
8329
  onBack: () => {
7256
8330
  dispatch({ type: "POP_SCREEN" });
7257
8331
  },
7258
- isActive: !anyOverlayOpen(state)
8332
+ onAppend: editable ? () => {
8333
+ setIsAdding(true);
8334
+ } : void 0
8335
+ });
8336
+ useInput((input) => {
8337
+ if (input !== "d") return;
8338
+ const entry = items[selectedIndex];
8339
+ if (entry === void 0) return;
8340
+ dispatch({
8341
+ type: "REMOVE_PDF_ITEM",
8342
+ pageIndex: screen.pageIndex,
8343
+ itemIndex: entry.itemIndex
8344
+ });
8345
+ }, { isActive: editable && !overlayOpen && !isAdding && items.length > 0 });
8346
+ if (isAdding) return /* @__PURE__ */ jsx(AddItemFlow, {
8347
+ pageIndex: screen.pageIndex,
8348
+ isActive: !overlayOpen,
8349
+ onCancel: () => {
8350
+ setIsAdding(false);
8351
+ },
8352
+ onCreated: () => {
8353
+ setIsAdding(false);
8354
+ }
7259
8355
  });
7260
8356
  return /* @__PURE__ */ jsxs(Box, {
7261
8357
  flexDirection: "column",
7262
- children: [/* @__PURE__ */ jsxs(Text, {
7263
- bold: true,
7264
- children: [
7265
- "Page ",
7266
- screen.pageIndex + 1,
7267
- " items (",
7268
- items.length,
7269
- " of ",
7270
- page.items.length,
7271
- ")"
7272
- ]
7273
- }), /* @__PURE__ */ jsx(ListView, {
7274
- items,
7275
- selectedIndex,
7276
- emptyMessage: query === "" ? "This page has no items." : `No items match "${state.searchQuery}".`,
7277
- renderItem: ({ item, itemIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
7278
- color: isSelected ? "cyan" : void 0,
7279
- inverse: isSelected,
8358
+ children: [
8359
+ /* @__PURE__ */ jsxs(Text, {
8360
+ bold: true,
7280
8361
  children: [
7281
- itemIndex + 1,
7282
- ". ",
7283
- item.kind,
7284
- " -- ",
7285
- previewFor(item)
8362
+ "Page ",
8363
+ screen.pageIndex + 1,
8364
+ " items (",
8365
+ items.length,
8366
+ " of ",
8367
+ page.items.length,
8368
+ ")"
7286
8369
  ]
8370
+ }),
8371
+ /* @__PURE__ */ jsx(ListView, {
8372
+ items,
8373
+ selectedIndex,
8374
+ emptyMessage: query === "" ? editable ? "This page has no items -- press 'a' to add one" : "This page has no items." : `No items match "${state.searchQuery}".`,
8375
+ renderItem: ({ item, itemIndex }, isSelected) => /* @__PURE__ */ jsxs(Text, {
8376
+ color: isSelected ? "cyan" : void 0,
8377
+ inverse: isSelected,
8378
+ children: [
8379
+ itemIndex + 1,
8380
+ ". ",
8381
+ item.kind,
8382
+ " -- ",
8383
+ previewFor(item)
8384
+ ]
8385
+ })
8386
+ }),
8387
+ editable && /* @__PURE__ */ jsx(Text, {
8388
+ dimColor: true,
8389
+ children: "a to add an item, d to delete the selected item"
7287
8390
  })
7288
- })]
8391
+ ]
7289
8392
  });
7290
8393
  }
7291
8394
  //#endregion
@@ -7444,7 +8547,12 @@ function FilePickerScreen() {
7444
8547
  function openAtPath(path) {
7445
8548
  (async () => {
7446
8549
  try {
7447
- const doc = await openDocumentAtPath(path);
8550
+ const doc = await openDocumentAtPath(path, { onDiagnostic: (diagnostic) => {
8551
+ dispatch({
8552
+ type: "APPEND_DIAGNOSTIC",
8553
+ diagnostic
8554
+ });
8555
+ } });
7448
8556
  dispatch({
7449
8557
  type: "OPEN_FILE_SUCCESS",
7450
8558
  path,
@@ -7795,7 +8903,7 @@ function metadataFor(doc) {
7795
8903
  case "odp": return readOdpContent(doc.editor.toPackage()).metadata;
7796
8904
  case "ods": return readOdsContent(doc.editor.toPackage()).metadata;
7797
8905
  case "odg": return readOdgContent(doc.editor.toPackage()).metadata;
7798
- case "markdown": return readMarkdownContent(doc.source).metadata;
8906
+ case "markdown": return readMarkdownContent(doc.editor.toMarkdownText()).metadata;
7799
8907
  case "pdf":
7800
8908
  case "xlsx": return doc.layout.metadata;
7801
8909
  case "odb": throw new Error("A .odb database has no document-level metadata -- it is a table/form/report container, not a single document with its own title/author/etc.");
@@ -7860,7 +8968,9 @@ function ScreenBody({ screen }) {
7860
8968
  case "launcher": return /* @__PURE__ */ jsx(LauncherScreen, {});
7861
8969
  case "filePicker": return /* @__PURE__ */ jsx(FilePickerScreen, {});
7862
8970
  case "newDocumentPicker": return /* @__PURE__ */ jsx(NewDocumentPickerScreen, {});
7863
- case "bodyList": return format === "odt" ? /* @__PURE__ */ jsx(OdtBodyListScreen, {}) : /* @__PURE__ */ jsx(DocxBodyListScreen, {});
8971
+ case "bodyList":
8972
+ if (format === "odt") return /* @__PURE__ */ jsx(OdtBodyListScreen, {});
8973
+ return format === "markdown" ? /* @__PURE__ */ jsx(MarkdownBodyListScreen, {}) : /* @__PURE__ */ jsx(DocxBodyListScreen, {});
7864
8974
  case "docxExtras": return /* @__PURE__ */ jsx(DocxExtrasScreen, {});
7865
8975
  case "paragraphDetail": return /* @__PURE__ */ jsx(ParagraphDetailScreen, {});
7866
8976
  case "runEditor": return /* @__PURE__ */ jsx(RunEditorScreen, {});
@@ -7886,8 +8996,7 @@ function ScreenBody({ screen }) {
7886
8996
  case "odbReportList": return /* @__PURE__ */ jsx(OdbReportListScreen, {});
7887
8997
  case "odbReportDetail": return /* @__PURE__ */ jsx(OdbReportDetailScreen, {});
7888
8998
  case "odbReportRender": return /* @__PURE__ */ jsx(OdbReportRenderScreen, {});
7889
- case "markdownLineList": return /* @__PURE__ */ jsx(MarkdownLineListScreen, {});
7890
- case "markdownLineEditor": return /* @__PURE__ */ jsx(MarkdownLineEditorScreen, {});
8999
+ case "viewSource": return /* @__PURE__ */ jsx(MarkdownViewSourceScreen, {});
7891
9000
  case "pdfPageList": return /* @__PURE__ */ jsx(PdfPageListScreen, {});
7892
9001
  case "pdfPageItems": return /* @__PURE__ */ jsx(PdfPageItemsScreen, {});
7893
9002
  case "pdfItemDetail": return /* @__PURE__ */ jsx(PdfItemDetailScreen, {});
@@ -7933,7 +9042,12 @@ function AppShell({ startPath }) {
7933
9042
  let cancelled = false;
7934
9043
  (async () => {
7935
9044
  try {
7936
- const doc = await openDocumentAtPath(startPath);
9045
+ const doc = await openDocumentAtPath(startPath, { onDiagnostic: (diagnostic) => {
9046
+ dispatch({
9047
+ type: "APPEND_DIAGNOSTIC",
9048
+ diagnostic
9049
+ });
9050
+ } });
7937
9051
  if (!cancelled) dispatch({
7938
9052
  type: "OPEN_FILE_SUCCESS",
7939
9053
  path: startPath,