@productcloudos/editor 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/pc-editor.esm.js +2360 -310
  2. package/dist/pc-editor.esm.js.map +1 -1
  3. package/dist/pc-editor.js +2363 -309
  4. package/dist/pc-editor.js.map +1 -1
  5. package/dist/pc-editor.min.js +1 -1
  6. package/dist/pc-editor.min.js.map +1 -1
  7. package/dist/types/lib/core/PCEditor.d.ts +74 -1
  8. package/dist/types/lib/core/PCEditor.d.ts.map +1 -1
  9. package/dist/types/lib/fonts/FontManager.d.ts +71 -0
  10. package/dist/types/lib/fonts/FontManager.d.ts.map +1 -0
  11. package/dist/types/lib/fonts/index.d.ts +3 -0
  12. package/dist/types/lib/fonts/index.d.ts.map +1 -0
  13. package/dist/types/lib/index.d.ts +6 -4
  14. package/dist/types/lib/index.d.ts.map +1 -1
  15. package/dist/types/lib/objects/table/TableObject.d.ts +72 -1
  16. package/dist/types/lib/objects/table/TableObject.d.ts.map +1 -1
  17. package/dist/types/lib/objects/table/types.d.ts +20 -0
  18. package/dist/types/lib/objects/table/types.d.ts.map +1 -1
  19. package/dist/types/lib/panes/ConditionalSectionPane.d.ts +62 -0
  20. package/dist/types/lib/panes/ConditionalSectionPane.d.ts.map +1 -0
  21. package/dist/types/lib/panes/FormattingPane.d.ts +1 -0
  22. package/dist/types/lib/panes/FormattingPane.d.ts.map +1 -1
  23. package/dist/types/lib/panes/ImagePane.d.ts.map +1 -1
  24. package/dist/types/lib/panes/RepeatingSectionPane.d.ts.map +1 -1
  25. package/dist/types/lib/panes/TablePane.d.ts.map +1 -1
  26. package/dist/types/lib/panes/TableRowLoopPane.d.ts.map +1 -1
  27. package/dist/types/lib/panes/TextBoxPane.d.ts.map +1 -1
  28. package/dist/types/lib/panes/index.d.ts +2 -0
  29. package/dist/types/lib/panes/index.d.ts.map +1 -1
  30. package/dist/types/lib/rendering/CanvasManager.d.ts +1 -0
  31. package/dist/types/lib/rendering/CanvasManager.d.ts.map +1 -1
  32. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts +17 -1
  33. package/dist/types/lib/rendering/FlowingTextRenderer.d.ts.map +1 -1
  34. package/dist/types/lib/rendering/PDFGenerator.d.ts +13 -0
  35. package/dist/types/lib/rendering/PDFGenerator.d.ts.map +1 -1
  36. package/dist/types/lib/text/ConditionalSectionManager.d.ts +101 -0
  37. package/dist/types/lib/text/ConditionalSectionManager.d.ts.map +1 -0
  38. package/dist/types/lib/text/FlowingTextContent.d.ts +44 -6
  39. package/dist/types/lib/text/FlowingTextContent.d.ts.map +1 -1
  40. package/dist/types/lib/text/ParagraphFormatting.d.ts +1 -1
  41. package/dist/types/lib/text/ParagraphFormatting.d.ts.map +1 -1
  42. package/dist/types/lib/text/PredicateEvaluator.d.ts +23 -0
  43. package/dist/types/lib/text/PredicateEvaluator.d.ts.map +1 -0
  44. package/dist/types/lib/text/index.d.ts +3 -1
  45. package/dist/types/lib/text/index.d.ts.map +1 -1
  46. package/dist/types/lib/text/types.d.ts +21 -0
  47. package/dist/types/lib/text/types.d.ts.map +1 -1
  48. package/dist/types/lib/types/index.d.ts +13 -0
  49. package/dist/types/lib/types/index.d.ts.map +1 -1
  50. package/package.json +2 -1
package/dist/pc-editor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var pdfLib = require('pdf-lib');
4
+ var fontkit = require('@pdf-lib/fontkit');
4
5
  var pdfjsLib = require('pdfjs-dist');
5
6
 
6
7
  function _interopNamespaceDefault(e) {
@@ -2307,6 +2308,291 @@ class RepeatingSectionManager extends EventEmitter {
2307
2308
  }
2308
2309
  }
2309
2310
 
2311
+ /**
2312
+ * Manages conditional sections within text content.
2313
+ * Conditional sections define ranges of content that are shown or hidden
2314
+ * based on a boolean predicate evaluated against merge data.
2315
+ * They start and end at paragraph boundaries.
2316
+ */
2317
+ class ConditionalSectionManager extends EventEmitter {
2318
+ constructor() {
2319
+ super();
2320
+ this.sections = new Map();
2321
+ this.nextId = 1;
2322
+ }
2323
+ /**
2324
+ * Create a new conditional section.
2325
+ * @param startIndex Text index at paragraph start (must be 0 or immediately after a newline)
2326
+ * @param endIndex Text index at closing paragraph start (must be immediately after a newline)
2327
+ * @param predicate The predicate expression to evaluate (e.g., "isActive")
2328
+ */
2329
+ create(startIndex, endIndex, predicate) {
2330
+ const id = `cond-${this.nextId++}`;
2331
+ const section = {
2332
+ id,
2333
+ predicate,
2334
+ startIndex,
2335
+ endIndex
2336
+ };
2337
+ this.sections.set(id, section);
2338
+ this.emit('section-added', { section });
2339
+ return section;
2340
+ }
2341
+ /**
2342
+ * Remove a conditional section by ID.
2343
+ */
2344
+ remove(id) {
2345
+ const section = this.sections.get(id);
2346
+ if (section) {
2347
+ this.sections.delete(id);
2348
+ this.emit('section-removed', { section });
2349
+ }
2350
+ return section;
2351
+ }
2352
+ /**
2353
+ * Get a conditional section by ID.
2354
+ */
2355
+ getSection(id) {
2356
+ return this.sections.get(id);
2357
+ }
2358
+ /**
2359
+ * Get all conditional sections.
2360
+ */
2361
+ getSections() {
2362
+ return Array.from(this.sections.values());
2363
+ }
2364
+ /**
2365
+ * Get all conditional sections sorted by startIndex.
2366
+ */
2367
+ getSectionsSorted() {
2368
+ return this.getSections().sort((a, b) => a.startIndex - b.startIndex);
2369
+ }
2370
+ /**
2371
+ * Get all conditional sections sorted by startIndex in descending order.
2372
+ * Useful for processing sections end-to-start during merge.
2373
+ */
2374
+ getSectionsDescending() {
2375
+ return this.getSections().sort((a, b) => b.startIndex - a.startIndex);
2376
+ }
2377
+ /**
2378
+ * Find a conditional section that contains the given text index.
2379
+ */
2380
+ getSectionContaining(textIndex) {
2381
+ for (const section of this.sections.values()) {
2382
+ if (textIndex >= section.startIndex && textIndex < section.endIndex) {
2383
+ return section;
2384
+ }
2385
+ }
2386
+ return undefined;
2387
+ }
2388
+ /**
2389
+ * Find a conditional section that has a boundary at the given text index.
2390
+ * Returns the section if textIndex matches startIndex or endIndex.
2391
+ */
2392
+ getSectionAtBoundary(textIndex) {
2393
+ for (const section of this.sections.values()) {
2394
+ if (section.startIndex === textIndex || section.endIndex === textIndex) {
2395
+ return section;
2396
+ }
2397
+ }
2398
+ return undefined;
2399
+ }
2400
+ /**
2401
+ * Update a section's predicate.
2402
+ */
2403
+ updatePredicate(id, predicate) {
2404
+ const section = this.sections.get(id);
2405
+ if (!section) {
2406
+ return false;
2407
+ }
2408
+ section.predicate = predicate;
2409
+ this.emit('section-updated', { section });
2410
+ return true;
2411
+ }
2412
+ /**
2413
+ * Update a section's visual state (called during rendering).
2414
+ */
2415
+ updateVisualState(id, visualState) {
2416
+ const section = this.sections.get(id);
2417
+ if (!section) {
2418
+ return false;
2419
+ }
2420
+ section.visualState = visualState;
2421
+ return true;
2422
+ }
2423
+ /**
2424
+ * Shift section positions when text is inserted.
2425
+ * @param fromIndex The position where text was inserted
2426
+ * @param delta The number of characters inserted (positive)
2427
+ */
2428
+ shiftSections(fromIndex, delta) {
2429
+ let changed = false;
2430
+ for (const section of this.sections.values()) {
2431
+ if (fromIndex <= section.startIndex) {
2432
+ section.startIndex += delta;
2433
+ section.endIndex += delta;
2434
+ changed = true;
2435
+ }
2436
+ else if (fromIndex < section.endIndex) {
2437
+ section.endIndex += delta;
2438
+ changed = true;
2439
+ }
2440
+ }
2441
+ if (changed) {
2442
+ this.emit('sections-shifted', { fromIndex, delta });
2443
+ }
2444
+ }
2445
+ /**
2446
+ * Handle deletion of text range.
2447
+ * Sections entirely within the deleted range are removed.
2448
+ * Sections partially overlapping are adjusted or removed.
2449
+ * @returns Array of removed sections
2450
+ */
2451
+ handleDeletion(start, length) {
2452
+ const end = start + length;
2453
+ const removedSections = [];
2454
+ const sectionsToUpdate = [];
2455
+ for (const section of this.sections.values()) {
2456
+ if (section.startIndex >= start && section.endIndex <= end) {
2457
+ removedSections.push(section);
2458
+ continue;
2459
+ }
2460
+ if (section.startIndex < end && section.endIndex > start) {
2461
+ if (start <= section.startIndex) {
2462
+ removedSections.push(section);
2463
+ continue;
2464
+ }
2465
+ if (start < section.endIndex) {
2466
+ if (end >= section.endIndex) {
2467
+ const newEnd = start;
2468
+ if (newEnd <= section.startIndex) {
2469
+ removedSections.push(section);
2470
+ continue;
2471
+ }
2472
+ sectionsToUpdate.push({
2473
+ id: section.id,
2474
+ newStart: section.startIndex,
2475
+ newEnd: newEnd
2476
+ });
2477
+ }
2478
+ else {
2479
+ const newEnd = section.endIndex - length;
2480
+ sectionsToUpdate.push({
2481
+ id: section.id,
2482
+ newStart: section.startIndex,
2483
+ newEnd: newEnd
2484
+ });
2485
+ }
2486
+ continue;
2487
+ }
2488
+ }
2489
+ if (section.startIndex >= end) {
2490
+ sectionsToUpdate.push({
2491
+ id: section.id,
2492
+ newStart: section.startIndex - length,
2493
+ newEnd: section.endIndex - length
2494
+ });
2495
+ }
2496
+ }
2497
+ for (const section of removedSections) {
2498
+ this.sections.delete(section.id);
2499
+ this.emit('section-removed', { section });
2500
+ }
2501
+ for (const update of sectionsToUpdate) {
2502
+ const section = this.sections.get(update.id);
2503
+ if (section) {
2504
+ section.startIndex = update.newStart;
2505
+ section.endIndex = update.newEnd;
2506
+ }
2507
+ }
2508
+ if (removedSections.length > 0 || sectionsToUpdate.length > 0) {
2509
+ this.emit('sections-changed');
2510
+ }
2511
+ return removedSections;
2512
+ }
2513
+ /**
2514
+ * Validate that the given boundaries are at paragraph boundaries.
2515
+ * Also checks that conditionals don't partially overlap repeating sections.
2516
+ * @param start The proposed start index
2517
+ * @param end The proposed end index
2518
+ * @param content The text content to validate against
2519
+ * @returns true if valid, false otherwise
2520
+ */
2521
+ validateBoundaries(start, end, content) {
2522
+ if (start !== 0 && content[start - 1] !== '\n') {
2523
+ return false;
2524
+ }
2525
+ if (end !== 0 && end < content.length && content[end - 1] !== '\n') {
2526
+ return false;
2527
+ }
2528
+ if (end <= start) {
2529
+ return false;
2530
+ }
2531
+ // Check for overlapping conditional sections
2532
+ for (const existing of this.sections.values()) {
2533
+ if ((start >= existing.startIndex && start < existing.endIndex) ||
2534
+ (end > existing.startIndex && end <= existing.endIndex) ||
2535
+ (start <= existing.startIndex && end >= existing.endIndex)) {
2536
+ return false;
2537
+ }
2538
+ }
2539
+ return true;
2540
+ }
2541
+ /**
2542
+ * Get the number of conditional sections.
2543
+ */
2544
+ get count() {
2545
+ return this.sections.size;
2546
+ }
2547
+ /**
2548
+ * Check if there are any conditional sections.
2549
+ */
2550
+ get isEmpty() {
2551
+ return this.sections.size === 0;
2552
+ }
2553
+ /**
2554
+ * Clear all conditional sections.
2555
+ */
2556
+ clear() {
2557
+ const hadSections = this.sections.size > 0;
2558
+ this.sections.clear();
2559
+ if (hadSections) {
2560
+ this.emit('sections-cleared');
2561
+ }
2562
+ }
2563
+ /**
2564
+ * Serialize all sections to JSON.
2565
+ */
2566
+ toJSON() {
2567
+ return this.getSectionsSorted().map(section => ({
2568
+ id: section.id,
2569
+ predicate: section.predicate,
2570
+ startIndex: section.startIndex,
2571
+ endIndex: section.endIndex
2572
+ }));
2573
+ }
2574
+ /**
2575
+ * Load sections from serialized data.
2576
+ */
2577
+ fromJSON(data) {
2578
+ this.clear();
2579
+ for (const sectionData of data) {
2580
+ const section = {
2581
+ id: sectionData.id,
2582
+ predicate: sectionData.predicate,
2583
+ startIndex: sectionData.startIndex,
2584
+ endIndex: sectionData.endIndex
2585
+ };
2586
+ this.sections.set(section.id, section);
2587
+ const idNum = parseInt(sectionData.id.replace('cond-', ''), 10);
2588
+ if (!isNaN(idNum) && idNum >= this.nextId) {
2589
+ this.nextId = idNum + 1;
2590
+ }
2591
+ }
2592
+ this.emit('sections-loaded', { count: this.sections.size });
2593
+ }
2594
+ }
2595
+
2310
2596
  /**
2311
2597
  * HyperlinkManager - Manages hyperlinks within flowing text content
2312
2598
  */
@@ -6212,12 +6498,20 @@ class TableObject extends BaseEmbeddedObject {
6212
6498
  this._coveredCells = new Map();
6213
6499
  // Row loops for merge expansion
6214
6500
  this._rowLoops = new Map();
6501
+ // Row conditionals for conditional display
6502
+ this._rowConditionals = new Map();
6215
6503
  // Layout caching for performance
6216
6504
  this._layoutDirty = true;
6217
6505
  this._cachedRowHeights = [];
6218
6506
  this._cachedRowPositions = [];
6219
6507
  // Multi-page rendering info: pageIndex -> slice render info
6220
6508
  this._renderedSlices = new Map();
6509
+ // ============================================
6510
+ // Table Row Conditionals
6511
+ // ============================================
6512
+ this._nextCondId = 1;
6513
+ this._selectedRowLoopId = null;
6514
+ this._selectedRowConditionalId = null;
6221
6515
  // Tables ONLY support block positioning - force it regardless of config
6222
6516
  this._position = 'block';
6223
6517
  // Initialize defaults
@@ -6911,6 +7205,85 @@ class TableObject extends BaseEmbeddedObject {
6911
7205
  loopRangesOverlap(start1, end1, start2, end2) {
6912
7206
  return start1 <= end2 && start2 <= end1;
6913
7207
  }
7208
+ generateRowConditionalId() {
7209
+ return `row-cond-${this._nextCondId++}`;
7210
+ }
7211
+ /**
7212
+ * Create a row conditional.
7213
+ */
7214
+ createRowConditional(startRowIndex, endRowIndex, predicate) {
7215
+ if (startRowIndex < 0 || endRowIndex >= this._rows.length) {
7216
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Invalid row range');
7217
+ return null;
7218
+ }
7219
+ if (startRowIndex > endRowIndex) {
7220
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Start index must be <= end index');
7221
+ return null;
7222
+ }
7223
+ // Check for overlapping conditionals
7224
+ for (const existing of this._rowConditionals.values()) {
7225
+ if (this.loopRangesOverlap(startRowIndex, endRowIndex, existing.startRowIndex, existing.endRowIndex)) {
7226
+ Logger.warn('[pc-editor:TableObject.createRowConditional] Range overlaps with existing conditional');
7227
+ return null;
7228
+ }
7229
+ }
7230
+ const cond = {
7231
+ id: this.generateRowConditionalId(),
7232
+ predicate,
7233
+ startRowIndex,
7234
+ endRowIndex
7235
+ };
7236
+ this._rowConditionals.set(cond.id, cond);
7237
+ this.emit('row-conditional-created', { conditional: cond });
7238
+ this.emit('content-changed', {});
7239
+ return cond;
7240
+ }
7241
+ /**
7242
+ * Remove a row conditional by ID.
7243
+ */
7244
+ removeRowConditional(id) {
7245
+ const cond = this._rowConditionals.get(id);
7246
+ if (!cond)
7247
+ return false;
7248
+ this._rowConditionals.delete(id);
7249
+ this.emit('row-conditional-removed', { conditionalId: id });
7250
+ return true;
7251
+ }
7252
+ /**
7253
+ * Get a row conditional by ID.
7254
+ */
7255
+ getRowConditional(id) {
7256
+ return this._rowConditionals.get(id);
7257
+ }
7258
+ /**
7259
+ * Get all row conditionals.
7260
+ */
7261
+ getAllRowConditionals() {
7262
+ return Array.from(this._rowConditionals.values());
7263
+ }
7264
+ /**
7265
+ * Get the row conditional at a given row index.
7266
+ */
7267
+ getRowConditionalAtRow(rowIndex) {
7268
+ for (const cond of this._rowConditionals.values()) {
7269
+ if (rowIndex >= cond.startRowIndex && rowIndex <= cond.endRowIndex) {
7270
+ return cond;
7271
+ }
7272
+ }
7273
+ return undefined;
7274
+ }
7275
+ /**
7276
+ * Update a row conditional's predicate.
7277
+ */
7278
+ updateRowConditionalPredicate(id, predicate) {
7279
+ const cond = this._rowConditionals.get(id);
7280
+ if (!cond)
7281
+ return false;
7282
+ cond.predicate = predicate;
7283
+ this.emit('row-conditional-updated', { conditional: cond });
7284
+ this.emit('content-changed', {});
7285
+ return true;
7286
+ }
6914
7287
  /**
6915
7288
  * Shift row loop indices when rows are inserted or removed.
6916
7289
  * @param fromIndex The row index where insertion/removal occurred
@@ -6951,6 +7324,41 @@ class TableObject extends BaseEmbeddedObject {
6951
7324
  this._rowLoops.delete(id);
6952
7325
  this.emit('row-loop-removed', { loopId: id, reason: 'row-deleted' });
6953
7326
  }
7327
+ // Also shift row conditional indices
7328
+ this.shiftRowConditionalIndices(fromIndex, delta);
7329
+ }
7330
+ /**
7331
+ * Shift row conditional indices when rows are inserted or removed.
7332
+ */
7333
+ shiftRowConditionalIndices(fromIndex, delta) {
7334
+ const condsToRemove = [];
7335
+ for (const cond of this._rowConditionals.values()) {
7336
+ if (delta < 0) {
7337
+ const removeCount = Math.abs(delta);
7338
+ const removeEnd = fromIndex + removeCount - 1;
7339
+ if (fromIndex <= cond.endRowIndex && removeEnd >= cond.startRowIndex) {
7340
+ condsToRemove.push(cond.id);
7341
+ continue;
7342
+ }
7343
+ if (fromIndex < cond.startRowIndex) {
7344
+ cond.startRowIndex += delta;
7345
+ cond.endRowIndex += delta;
7346
+ }
7347
+ }
7348
+ else {
7349
+ if (fromIndex <= cond.startRowIndex) {
7350
+ cond.startRowIndex += delta;
7351
+ cond.endRowIndex += delta;
7352
+ }
7353
+ else if (fromIndex <= cond.endRowIndex) {
7354
+ cond.endRowIndex += delta;
7355
+ }
7356
+ }
7357
+ }
7358
+ for (const id of condsToRemove) {
7359
+ this._rowConditionals.delete(id);
7360
+ this.emit('row-conditional-removed', { conditionalId: id, reason: 'row-deleted' });
7361
+ }
6954
7362
  }
6955
7363
  /**
6956
7364
  * Get rows in a range (for loop expansion).
@@ -7851,6 +8259,10 @@ class TableObject extends BaseEmbeddedObject {
7851
8259
  if (this._rowLoops.size > 0) {
7852
8260
  this.renderRowLoopIndicators(ctx);
7853
8261
  }
8262
+ // Render row conditional indicators
8263
+ if (this._rowConditionals.size > 0) {
8264
+ this.renderRowConditionalIndicators(ctx);
8265
+ }
7854
8266
  // Render cell range selection highlight
7855
8267
  if (this._selectedRange) {
7856
8268
  this.renderRangeSelection(ctx);
@@ -7865,11 +8277,54 @@ class TableObject extends BaseEmbeddedObject {
7865
8277
  }
7866
8278
  }
7867
8279
  /**
7868
- * Render row loop indicators (colored stripe on left side of loop rows).
8280
+ * Select a row loop by ID (for pane display).
8281
+ */
8282
+ selectRowLoop(loopId) {
8283
+ this._selectedRowLoopId = loopId;
8284
+ }
8285
+ /**
8286
+ * Get the currently selected row loop ID.
8287
+ */
8288
+ get selectedRowLoopId() {
8289
+ return this._selectedRowLoopId;
8290
+ }
8291
+ /**
8292
+ * Hit-test a point against row loop labels.
8293
+ * Point should be in table-local coordinates.
8294
+ * Returns the loop if a label was clicked, null otherwise.
7869
8295
  */
8296
+ getRowLoopAtPoint(point) {
8297
+ let rowPositions = this._cachedRowPositions;
8298
+ if (rowPositions.length === 0) {
8299
+ rowPositions = [];
8300
+ let y = 0;
8301
+ for (const row of this._rows) {
8302
+ rowPositions.push(y);
8303
+ y += row.calculatedHeight;
8304
+ }
8305
+ }
8306
+ for (const loop of this._rowLoops.values()) {
8307
+ const startY = rowPositions[loop.startRowIndex] || 0;
8308
+ let _endY = startY;
8309
+ for (let i = loop.startRowIndex; i <= loop.endRowIndex && i < this._rows.length; i++) {
8310
+ _endY += this._rows[i].calculatedHeight;
8311
+ }
8312
+ // Label bounds (matches rendering)
8313
+ const labelWidth = 30; // approximate for "Loop" at 10px
8314
+ const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
8315
+ const labelX = -6 - labelWidth - 4;
8316
+ const labelY = startY - labelHeight - 2;
8317
+ if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
8318
+ point.y >= labelY && point.y <= labelY + labelHeight + 4) {
8319
+ return loop;
8320
+ }
8321
+ }
8322
+ return null;
8323
+ }
7870
8324
  renderRowLoopIndicators(ctx) {
7871
- const indicatorWidth = 4;
7872
- const labelPadding = 4;
8325
+ const color = TableObject.LOOP_COLOR;
8326
+ const padding = TableObject.LOOP_LABEL_PADDING;
8327
+ const radius = TableObject.LOOP_LABEL_RADIUS;
7873
8328
  // Calculate row Y positions if not cached
7874
8329
  let rowPositions = this._cachedRowPositions;
7875
8330
  if (rowPositions.length === 0) {
@@ -7880,12 +8335,8 @@ class TableObject extends BaseEmbeddedObject {
7880
8335
  y += row.calculatedHeight;
7881
8336
  }
7882
8337
  }
7883
- // Colors for different loops (cycle through these)
7884
- const loopColors = ['#9b59b6', '#3498db', '#e67e22', '#1abc9c', '#e74c3c'];
7885
- let colorIndex = 0;
7886
8338
  for (const loop of this._rowLoops.values()) {
7887
- const color = loopColors[colorIndex % loopColors.length];
7888
- colorIndex++;
8339
+ const isSelected = this._selectedRowLoopId === loop.id;
7889
8340
  // Calculate the Y range for this loop
7890
8341
  const startY = rowPositions[loop.startRowIndex] || 0;
7891
8342
  let endY = startY;
@@ -7895,31 +8346,149 @@ class TableObject extends BaseEmbeddedObject {
7895
8346
  const loopHeight = endY - startY;
7896
8347
  // Draw colored stripe on left side
7897
8348
  ctx.fillStyle = color;
7898
- ctx.fillRect(-indicatorWidth - 2, startY, indicatorWidth, loopHeight);
7899
- // Draw loop label on the first row
8349
+ ctx.fillRect(-6, startY, 4, loopHeight);
8350
+ // Draw vertical connector line
8351
+ ctx.strokeStyle = color;
8352
+ ctx.lineWidth = 1;
8353
+ ctx.beginPath();
8354
+ ctx.moveTo(-4, startY);
8355
+ ctx.lineTo(-4, endY);
8356
+ ctx.stroke();
8357
+ // Draw "Loop" label — matches text flow style
7900
8358
  ctx.save();
7901
8359
  ctx.font = '10px Arial';
7902
- ctx.fillStyle = color;
7903
- // Rotate text to be vertical along the stripe
7904
- const labelText = `⟳ ${loop.fieldPath}`;
7905
- const textMetrics = ctx.measureText(labelText);
7906
- // Position label to the left of the stripe
7907
- ctx.translate(-indicatorWidth - labelPadding - textMetrics.width - 4, startY + loopHeight / 2);
7908
- ctx.fillText(labelText, 0, 4);
8360
+ const labelText = 'Loop';
8361
+ const metrics = ctx.measureText(labelText);
8362
+ const boxWidth = metrics.width + padding * 2;
8363
+ const boxHeight = 10 + padding * 2;
8364
+ const labelX = -6 - boxWidth - 4;
8365
+ const labelY = startY - boxHeight - 2;
8366
+ ctx.beginPath();
8367
+ ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
8368
+ if (isSelected) {
8369
+ // Selected: filled background with white text
8370
+ ctx.fillStyle = color;
8371
+ ctx.fill();
8372
+ ctx.fillStyle = '#ffffff';
8373
+ }
8374
+ else {
8375
+ // Not selected: white background, outlined with colored text
8376
+ ctx.fillStyle = '#ffffff';
8377
+ ctx.fill();
8378
+ ctx.strokeStyle = color;
8379
+ ctx.lineWidth = 1.5;
8380
+ ctx.stroke();
8381
+ ctx.fillStyle = color;
8382
+ }
8383
+ ctx.textBaseline = 'middle';
8384
+ ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
7909
8385
  ctx.restore();
7910
- // Draw top and bottom brackets
8386
+ }
8387
+ }
8388
+ /**
8389
+ * Select a row conditional by ID (for pane display).
8390
+ */
8391
+ selectRowConditional(conditionalId) {
8392
+ this._selectedRowConditionalId = conditionalId;
8393
+ }
8394
+ /**
8395
+ * Get the currently selected row conditional ID.
8396
+ */
8397
+ get selectedRowConditionalId() {
8398
+ return this._selectedRowConditionalId;
8399
+ }
8400
+ /**
8401
+ * Hit-test a point against row conditional labels.
8402
+ * Point should be in table-local coordinates.
8403
+ */
8404
+ getRowConditionalAtPoint(point) {
8405
+ let rowPositions = this._cachedRowPositions;
8406
+ if (rowPositions.length === 0) {
8407
+ rowPositions = [];
8408
+ let y = 0;
8409
+ for (const row of this._rows) {
8410
+ rowPositions.push(y);
8411
+ y += row.calculatedHeight;
8412
+ }
8413
+ }
8414
+ for (const cond of this._rowConditionals.values()) {
8415
+ const startY = rowPositions[cond.startRowIndex] || 0;
8416
+ let _endY = startY;
8417
+ for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
8418
+ _endY += this._rows[i].calculatedHeight;
8419
+ }
8420
+ // Label bounds (right side of table, offset from loop labels)
8421
+ const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
8422
+ const labelWidth = 22; // approximate for "If" at 10px
8423
+ const labelHeight = 10 + TableObject.LOOP_LABEL_PADDING * 2;
8424
+ const labelX = totalWidth + 10;
8425
+ const labelY = startY - labelHeight - 2;
8426
+ if (point.x >= labelX && point.x <= labelX + labelWidth + 4 &&
8427
+ point.y >= labelY && point.y <= labelY + labelHeight + 4) {
8428
+ return cond;
8429
+ }
8430
+ }
8431
+ return null;
8432
+ }
8433
+ renderRowConditionalIndicators(ctx) {
8434
+ const color = TableObject.COND_COLOR;
8435
+ const padding = TableObject.LOOP_LABEL_PADDING;
8436
+ const radius = TableObject.LOOP_LABEL_RADIUS;
8437
+ let rowPositions = this._cachedRowPositions;
8438
+ if (rowPositions.length === 0) {
8439
+ rowPositions = [];
8440
+ let y = 0;
8441
+ for (const row of this._rows) {
8442
+ rowPositions.push(y);
8443
+ y += row.calculatedHeight;
8444
+ }
8445
+ }
8446
+ const totalWidth = this._columns.reduce((sum, col) => sum + col.width, 0);
8447
+ for (const cond of this._rowConditionals.values()) {
8448
+ const isSelected = this._selectedRowConditionalId === cond.id;
8449
+ const startY = rowPositions[cond.startRowIndex] || 0;
8450
+ let endY = startY;
8451
+ for (let i = cond.startRowIndex; i <= cond.endRowIndex && i < this._rows.length; i++) {
8452
+ endY += this._rows[i].calculatedHeight;
8453
+ }
8454
+ const condHeight = endY - startY;
8455
+ // Draw colored stripe on right side
8456
+ ctx.fillStyle = color;
8457
+ ctx.fillRect(totalWidth + 2, startY, 4, condHeight);
8458
+ // Draw vertical connector line
7911
8459
  ctx.strokeStyle = color;
7912
8460
  ctx.lineWidth = 1;
7913
8461
  ctx.beginPath();
7914
- // Top bracket
7915
- ctx.moveTo(-indicatorWidth - 2, startY);
7916
- ctx.lineTo(-indicatorWidth - 6, startY);
7917
- ctx.lineTo(-indicatorWidth - 6, startY + 6);
7918
- // Bottom bracket
7919
- ctx.moveTo(-indicatorWidth - 2, endY);
7920
- ctx.lineTo(-indicatorWidth - 6, endY);
7921
- ctx.lineTo(-indicatorWidth - 6, endY - 6);
8462
+ ctx.moveTo(totalWidth + 4, startY);
8463
+ ctx.lineTo(totalWidth + 4, endY);
7922
8464
  ctx.stroke();
8465
+ // Draw "If" label
8466
+ ctx.save();
8467
+ ctx.font = '10px Arial';
8468
+ const labelText = 'If';
8469
+ const metrics = ctx.measureText(labelText);
8470
+ const boxWidth = metrics.width + padding * 2;
8471
+ const boxHeight = 10 + padding * 2;
8472
+ const labelX = totalWidth + 10;
8473
+ const labelY = startY - boxHeight - 2;
8474
+ ctx.beginPath();
8475
+ ctx.roundRect(labelX, labelY, boxWidth, boxHeight, radius);
8476
+ if (isSelected) {
8477
+ ctx.fillStyle = color;
8478
+ ctx.fill();
8479
+ ctx.fillStyle = '#ffffff';
8480
+ }
8481
+ else {
8482
+ ctx.fillStyle = '#ffffff';
8483
+ ctx.fill();
8484
+ ctx.strokeStyle = color;
8485
+ ctx.lineWidth = 1.5;
8486
+ ctx.stroke();
8487
+ ctx.fillStyle = color;
8488
+ }
8489
+ ctx.textBaseline = 'middle';
8490
+ ctx.fillText(labelText, labelX + padding, labelY + boxHeight / 2);
8491
+ ctx.restore();
7923
8492
  }
7924
8493
  }
7925
8494
  /**
@@ -8038,6 +8607,14 @@ class TableObject extends BaseEmbeddedObject {
8038
8607
  columns: this._columns.map(col => ({ ...col })),
8039
8608
  rows: this._rows.map(row => row.toData()),
8040
8609
  rowLoops,
8610
+ rowConditionals: this._rowConditionals.size > 0
8611
+ ? Array.from(this._rowConditionals.values()).map(c => ({
8612
+ id: c.id,
8613
+ predicate: c.predicate,
8614
+ startRowIndex: c.startRowIndex,
8615
+ endRowIndex: c.endRowIndex
8616
+ }))
8617
+ : undefined,
8041
8618
  defaultCellPadding: this._defaultCellPadding,
8042
8619
  defaultBorderColor: this._defaultBorderColor,
8043
8620
  defaultBorderWidth: this._defaultBorderWidth,
@@ -8081,6 +8658,17 @@ class TableObject extends BaseEmbeddedObject {
8081
8658
  });
8082
8659
  }
8083
8660
  }
8661
+ // Load row conditionals if present
8662
+ if (data.data.rowConditionals) {
8663
+ for (const condData of data.data.rowConditionals) {
8664
+ table._rowConditionals.set(condData.id, {
8665
+ id: condData.id,
8666
+ predicate: condData.predicate,
8667
+ startRowIndex: condData.startRowIndex,
8668
+ endRowIndex: condData.endRowIndex
8669
+ });
8670
+ }
8671
+ }
8084
8672
  table.updateCoveredCells();
8085
8673
  return table;
8086
8674
  }
@@ -8110,6 +8698,18 @@ class TableObject extends BaseEmbeddedObject {
8110
8698
  });
8111
8699
  }
8112
8700
  }
8701
+ // Restore row conditionals if any
8702
+ this._rowConditionals.clear();
8703
+ if (data.data.rowConditionals) {
8704
+ for (const condData of data.data.rowConditionals) {
8705
+ this._rowConditionals.set(condData.id, {
8706
+ id: condData.id,
8707
+ predicate: condData.predicate,
8708
+ startRowIndex: condData.startRowIndex,
8709
+ endRowIndex: condData.endRowIndex
8710
+ });
8711
+ }
8712
+ }
8113
8713
  // Restore defaults
8114
8714
  if (data.data.defaultCellPadding !== undefined) {
8115
8715
  this._defaultCellPadding = data.data.defaultCellPadding;
@@ -8129,6 +8729,13 @@ class TableObject extends BaseEmbeddedObject {
8129
8729
  return TableObject.fromData(this.toData());
8130
8730
  }
8131
8731
  }
8732
+ /**
8733
+ * Render row loop indicators (colored stripe on left side of loop rows).
8734
+ */
8735
+ TableObject.LOOP_COLOR = '#6B46C1';
8736
+ TableObject.LOOP_LABEL_PADDING = 4;
8737
+ TableObject.LOOP_LABEL_RADIUS = 4;
8738
+ TableObject.COND_COLOR = '#D97706'; // Orange
8132
8739
 
8133
8740
  /**
8134
8741
  * TableResizeHandler - Handles column and row resize operations for tables.
@@ -8514,6 +9121,7 @@ class FlowingTextContent extends EventEmitter {
8514
9121
  this.substitutionFields = new SubstitutionFieldManager();
8515
9122
  this.embeddedObjects = new EmbeddedObjectManager();
8516
9123
  this.repeatingSections = new RepeatingSectionManager();
9124
+ this.conditionalSections = new ConditionalSectionManager();
8517
9125
  this.hyperlinks = new HyperlinkManager();
8518
9126
  this.layout = new TextLayout();
8519
9127
  this.setupEventForwarding();
@@ -8551,6 +9159,7 @@ class FlowingTextContent extends EventEmitter {
8551
9159
  this.substitutionFields.handleDeletion(data.start, data.length);
8552
9160
  this.embeddedObjects.handleDeletion(data.start, data.length);
8553
9161
  this.repeatingSections.handleDeletion(data.start, data.length);
9162
+ this.conditionalSections.handleDeletion(data.start, data.length);
8554
9163
  this.paragraphFormatting.handleDeletion(data.start, data.length);
8555
9164
  this.hyperlinks.handleDeletion(data.start, data.length);
8556
9165
  this.emit('content-changed', {
@@ -8602,6 +9211,16 @@ class FlowingTextContent extends EventEmitter {
8602
9211
  this.repeatingSections.on('section-updated', (data) => {
8603
9212
  this.emit('repeating-section-updated', data);
8604
9213
  });
9214
+ // Forward conditional section events
9215
+ this.conditionalSections.on('section-added', (data) => {
9216
+ this.emit('conditional-section-added', data);
9217
+ });
9218
+ this.conditionalSections.on('section-removed', (data) => {
9219
+ this.emit('conditional-section-removed', data);
9220
+ });
9221
+ this.conditionalSections.on('section-updated', (data) => {
9222
+ this.emit('conditional-section-updated', data);
9223
+ });
8605
9224
  // Forward hyperlink events
8606
9225
  this.hyperlinks.on('hyperlink-added', (data) => {
8607
9226
  this.emit('hyperlink-added', data);
@@ -8659,6 +9278,7 @@ class FlowingTextContent extends EventEmitter {
8659
9278
  this.substitutionFields.shiftFields(insertAt, text.length);
8660
9279
  this.embeddedObjects.shiftObjects(insertAt, text.length);
8661
9280
  this.repeatingSections.shiftSections(insertAt, text.length);
9281
+ this.conditionalSections.shiftSections(insertAt, text.length);
8662
9282
  this.hyperlinks.shiftHyperlinks(insertAt, text.length);
8663
9283
  // Insert the text first so we have the full content
8664
9284
  this.textState.insertText(text, insertAt);
@@ -8731,6 +9351,7 @@ class FlowingTextContent extends EventEmitter {
8731
9351
  this.substitutionFields.shiftFields(position, text.length);
8732
9352
  this.embeddedObjects.shiftObjects(position, text.length);
8733
9353
  this.repeatingSections.shiftSections(position, text.length);
9354
+ this.conditionalSections.shiftSections(position, text.length);
8734
9355
  this.hyperlinks.shiftHyperlinks(position, text.length);
8735
9356
  // Insert the text
8736
9357
  const content = this.textState.getText();
@@ -8748,6 +9369,7 @@ class FlowingTextContent extends EventEmitter {
8748
9369
  this.substitutionFields.handleDeletion(position, length);
8749
9370
  this.embeddedObjects.handleDeletion(position, length);
8750
9371
  this.repeatingSections.handleDeletion(position, length);
9372
+ this.conditionalSections.handleDeletion(position, length);
8751
9373
  this.paragraphFormatting.handleDeletion(position, length);
8752
9374
  this.hyperlinks.handleDeletion(position, length);
8753
9375
  // Delete the text
@@ -9137,6 +9759,7 @@ class FlowingTextContent extends EventEmitter {
9137
9759
  this.substitutionFields.shiftFields(insertAt, 1);
9138
9760
  this.embeddedObjects.shiftObjects(insertAt, 1);
9139
9761
  this.repeatingSections.shiftSections(insertAt, 1);
9762
+ this.conditionalSections.shiftSections(insertAt, 1);
9140
9763
  // Insert the placeholder character
9141
9764
  this.textState.insertText(OBJECT_REPLACEMENT_CHAR, insertAt);
9142
9765
  // Shift paragraph formatting with the complete content
@@ -9384,6 +10007,7 @@ class FlowingTextContent extends EventEmitter {
9384
10007
  this.substitutionFields.clear();
9385
10008
  this.embeddedObjects.clear();
9386
10009
  this.repeatingSections.clear();
10010
+ this.conditionalSections.clear();
9387
10011
  this.hyperlinks.clear();
9388
10012
  }
9389
10013
  // ============================================
@@ -9740,44 +10364,60 @@ class FlowingTextContent extends EventEmitter {
9740
10364
  // List Operations
9741
10365
  // ============================================
9742
10366
  /**
9743
- * Toggle bullet list for the current paragraph (or selection).
10367
+ * Get paragraph starts affected by the current selection or cursor position.
9744
10368
  */
9745
- toggleBulletList() {
10369
+ getAffectedParagraphStarts() {
10370
+ const content = this.textState.getText();
10371
+ const selection = this.getSelection();
10372
+ if (selection && selection.start !== selection.end) {
10373
+ return this.paragraphFormatting.getParagraphBoundariesInRange(selection.start, selection.end, content);
10374
+ }
9746
10375
  const cursorPos = this.textState.getCursorPosition();
10376
+ return [this.paragraphFormatting.getParagraphStart(cursorPos, content)];
10377
+ }
10378
+ /**
10379
+ * Toggle bullet list for the current paragraph(s) in selection.
10380
+ */
10381
+ toggleBulletList() {
9747
10382
  const content = this.textState.getText();
9748
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9749
- this.paragraphFormatting.toggleList(paragraphStart, 'bullet');
9750
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10383
+ const paragraphStarts = this.getAffectedParagraphStarts();
10384
+ for (const start of paragraphStarts) {
10385
+ this.paragraphFormatting.toggleList(start, 'bullet');
10386
+ }
10387
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9751
10388
  }
9752
10389
  /**
9753
- * Toggle numbered list for the current paragraph (or selection).
10390
+ * Toggle numbered list for the current paragraph(s) in selection.
9754
10391
  */
9755
10392
  toggleNumberedList() {
9756
- const cursorPos = this.textState.getCursorPosition();
9757
10393
  const content = this.textState.getText();
9758
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9759
- this.paragraphFormatting.toggleList(paragraphStart, 'number');
9760
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10394
+ const paragraphStarts = this.getAffectedParagraphStarts();
10395
+ for (const start of paragraphStarts) {
10396
+ this.paragraphFormatting.toggleList(start, 'number');
10397
+ }
10398
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9761
10399
  }
9762
10400
  /**
9763
- * Indent the current paragraph (increase list nesting level).
10401
+ * Indent the current paragraph(s) in selection.
9764
10402
  */
9765
10403
  indentParagraph() {
9766
- const cursorPos = this.textState.getCursorPosition();
9767
10404
  const content = this.textState.getText();
9768
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9769
- this.paragraphFormatting.indentParagraph(paragraphStart);
9770
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10405
+ const paragraphStarts = this.getAffectedParagraphStarts();
10406
+ for (const start of paragraphStarts) {
10407
+ this.paragraphFormatting.indentParagraph(start);
10408
+ }
10409
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9771
10410
  }
9772
10411
  /**
9773
- * Outdent the current paragraph (decrease list nesting level).
10412
+ * Outdent the current paragraph(s) in selection.
9774
10413
  */
9775
10414
  outdentParagraph() {
9776
- const cursorPos = this.textState.getCursorPosition();
9777
10415
  const content = this.textState.getText();
9778
- const paragraphStart = this.paragraphFormatting.getParagraphStart(cursorPos, content);
9779
- this.paragraphFormatting.outdentParagraph(paragraphStart);
9780
- this.emit('content-changed', { text: content, cursorPosition: cursorPos });
10416
+ const paragraphStarts = this.getAffectedParagraphStarts();
10417
+ for (const start of paragraphStarts) {
10418
+ this.paragraphFormatting.outdentParagraph(start);
10419
+ }
10420
+ this.emit('content-changed', { text: content, cursorPosition: this.textState.getCursorPosition() });
9781
10421
  }
9782
10422
  /**
9783
10423
  * Get the list formatting for the current paragraph.
@@ -9939,6 +10579,79 @@ class FlowingTextContent extends EventEmitter {
9939
10579
  return result;
9940
10580
  }
9941
10581
  // ============================================
10582
+ // Conditional Section Operations
10583
+ // ============================================
10584
+ /**
10585
+ * Get the conditional section manager.
10586
+ */
10587
+ getConditionalSectionManager() {
10588
+ return this.conditionalSections;
10589
+ }
10590
+ /**
10591
+ * Get all conditional sections.
10592
+ */
10593
+ getConditionalSections() {
10594
+ return this.conditionalSections.getSections();
10595
+ }
10596
+ /**
10597
+ * Create a conditional section.
10598
+ * @param startIndex Text index at paragraph start (must be at a paragraph boundary)
10599
+ * @param endIndex Text index at closing paragraph start (must be at a paragraph boundary)
10600
+ * @param predicate The predicate expression to evaluate
10601
+ * @returns The created section, or null if boundaries are invalid
10602
+ */
10603
+ createConditionalSection(startIndex, endIndex, predicate) {
10604
+ const content = this.textState.getText();
10605
+ if (!this.conditionalSections.validateBoundaries(startIndex, endIndex, content)) {
10606
+ return null;
10607
+ }
10608
+ const section = this.conditionalSections.create(startIndex, endIndex, predicate);
10609
+ this.emit('content-changed', {
10610
+ text: content,
10611
+ cursorPosition: this.textState.getCursorPosition()
10612
+ });
10613
+ return section;
10614
+ }
10615
+ /**
10616
+ * Remove a conditional section by ID.
10617
+ */
10618
+ removeConditionalSection(id) {
10619
+ const section = this.conditionalSections.remove(id);
10620
+ if (section) {
10621
+ this.emit('content-changed', {
10622
+ text: this.textState.getText(),
10623
+ cursorPosition: this.textState.getCursorPosition()
10624
+ });
10625
+ return true;
10626
+ }
10627
+ return false;
10628
+ }
10629
+ /**
10630
+ * Get a conditional section by ID.
10631
+ */
10632
+ getConditionalSection(id) {
10633
+ return this.conditionalSections.getSection(id);
10634
+ }
10635
+ /**
10636
+ * Find a conditional section that has a boundary at the given text index.
10637
+ */
10638
+ getConditionalSectionAtBoundary(textIndex) {
10639
+ return this.conditionalSections.getSectionAtBoundary(textIndex);
10640
+ }
10641
+ /**
10642
+ * Update a conditional section's predicate.
10643
+ */
10644
+ updateConditionalSectionPredicate(id, predicate) {
10645
+ const result = this.conditionalSections.updatePredicate(id, predicate);
10646
+ if (result) {
10647
+ this.emit('content-changed', {
10648
+ text: this.textState.getText(),
10649
+ cursorPosition: this.textState.getCursorPosition()
10650
+ });
10651
+ }
10652
+ return result;
10653
+ }
10654
+ // ============================================
9942
10655
  // Serialization
9943
10656
  // ============================================
9944
10657
  /**
@@ -9992,6 +10705,8 @@ class FlowingTextContent extends EventEmitter {
9992
10705
  }));
9993
10706
  // Serialize repeating sections
9994
10707
  const repeatingSectionsData = this.repeatingSections.toJSON();
10708
+ // Serialize conditional sections
10709
+ const conditionalSectionsData = this.conditionalSections.toJSON();
9995
10710
  // Serialize embedded objects
9996
10711
  const embeddedObjects = [];
9997
10712
  const objectsMap = this.embeddedObjects.getObjects();
@@ -10009,6 +10724,7 @@ class FlowingTextContent extends EventEmitter {
10009
10724
  paragraphFormatting: paragraphFormatting.length > 0 ? paragraphFormatting : undefined,
10010
10725
  substitutionFields: substitutionFieldsData.length > 0 ? substitutionFieldsData : undefined,
10011
10726
  repeatingSections: repeatingSectionsData.length > 0 ? repeatingSectionsData : undefined,
10727
+ conditionalSections: conditionalSectionsData.length > 0 ? conditionalSectionsData : undefined,
10012
10728
  embeddedObjects: embeddedObjects.length > 0 ? embeddedObjects : undefined,
10013
10729
  hyperlinks: hyperlinksData.length > 0 ? hyperlinksData : undefined
10014
10730
  };
@@ -10047,6 +10763,10 @@ class FlowingTextContent extends EventEmitter {
10047
10763
  if (data.repeatingSections && data.repeatingSections.length > 0) {
10048
10764
  content.getRepeatingSectionManager().fromJSON(data.repeatingSections);
10049
10765
  }
10766
+ // Restore conditional sections
10767
+ if (data.conditionalSections && data.conditionalSections.length > 0) {
10768
+ content.getConditionalSectionManager().fromJSON(data.conditionalSections);
10769
+ }
10050
10770
  // Restore embedded objects using factory
10051
10771
  if (data.embeddedObjects && data.embeddedObjects.length > 0) {
10052
10772
  for (const ref of data.embeddedObjects) {
@@ -10101,6 +10821,10 @@ class FlowingTextContent extends EventEmitter {
10101
10821
  if (data.repeatingSections && data.repeatingSections.length > 0) {
10102
10822
  this.repeatingSections.fromJSON(data.repeatingSections);
10103
10823
  }
10824
+ // Restore conditional sections
10825
+ if (data.conditionalSections && data.conditionalSections.length > 0) {
10826
+ this.conditionalSections.fromJSON(data.conditionalSections);
10827
+ }
10104
10828
  // Restore embedded objects
10105
10829
  if (data.embeddedObjects && data.embeddedObjects.length > 0) {
10106
10830
  for (const ref of data.embeddedObjects) {
@@ -10120,6 +10844,349 @@ class FlowingTextContent extends EventEmitter {
10120
10844
  }
10121
10845
  }
10122
10846
 
10847
+ /**
10848
+ * Simple recursive-descent predicate evaluator.
10849
+ * Supports:
10850
+ * - Truthiness: `isActive`
10851
+ * - Negation: `!isActive`
10852
+ * - Comparisons: ==, !=, >, <, >=, <=
10853
+ * - Logical: &&, ||, parentheses
10854
+ * - Literals: "approved", 100, true/false
10855
+ * - Dot notation: customer.isVIP
10856
+ */
10857
+ function tokenize(input) {
10858
+ const tokens = [];
10859
+ let i = 0;
10860
+ while (i < input.length) {
10861
+ const ch = input[i];
10862
+ // Skip whitespace
10863
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
10864
+ i++;
10865
+ continue;
10866
+ }
10867
+ // Parentheses
10868
+ if (ch === '(' || ch === ')') {
10869
+ tokens.push({ type: 'paren', value: ch });
10870
+ i++;
10871
+ continue;
10872
+ }
10873
+ // Two-character operators
10874
+ if (i + 1 < input.length) {
10875
+ const two = input[i] + input[i + 1];
10876
+ if (two === '==' || two === '!=' || two === '>=' || two === '<=' || two === '&&' || two === '||' || two === '=~' || two === '!~') {
10877
+ tokens.push({ type: 'operator', value: two });
10878
+ i += 2;
10879
+ continue;
10880
+ }
10881
+ }
10882
+ // Single-character operators
10883
+ if (ch === '>' || ch === '<') {
10884
+ tokens.push({ type: 'operator', value: ch });
10885
+ i++;
10886
+ continue;
10887
+ }
10888
+ // Not operator
10889
+ if (ch === '!') {
10890
+ tokens.push({ type: 'not' });
10891
+ i++;
10892
+ continue;
10893
+ }
10894
+ // String literals
10895
+ if (ch === '"' || ch === "'") {
10896
+ const quote = ch;
10897
+ i++;
10898
+ let str = '';
10899
+ while (i < input.length && input[i] !== quote) {
10900
+ if (input[i] === '\\' && i + 1 < input.length) {
10901
+ i++;
10902
+ str += input[i];
10903
+ }
10904
+ else {
10905
+ str += input[i];
10906
+ }
10907
+ i++;
10908
+ }
10909
+ i++; // skip closing quote
10910
+ tokens.push({ type: 'string', value: str });
10911
+ continue;
10912
+ }
10913
+ // Numbers
10914
+ if (ch >= '0' && ch <= '9') {
10915
+ let num = '';
10916
+ while (i < input.length && ((input[i] >= '0' && input[i] <= '9') || input[i] === '.')) {
10917
+ num += input[i];
10918
+ i++;
10919
+ }
10920
+ tokens.push({ type: 'number', value: parseFloat(num) });
10921
+ continue;
10922
+ }
10923
+ // Identifiers (including dot notation: customer.isVIP)
10924
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
10925
+ let ident = '';
10926
+ while (i < input.length &&
10927
+ ((input[i] >= 'a' && input[i] <= 'z') ||
10928
+ (input[i] >= 'A' && input[i] <= 'Z') ||
10929
+ (input[i] >= '0' && input[i] <= '9') ||
10930
+ input[i] === '_' || input[i] === '$' || input[i] === '.')) {
10931
+ ident += input[i];
10932
+ i++;
10933
+ }
10934
+ if (ident === 'true') {
10935
+ tokens.push({ type: 'boolean', value: true });
10936
+ }
10937
+ else if (ident === 'false') {
10938
+ tokens.push({ type: 'boolean', value: false });
10939
+ }
10940
+ else {
10941
+ tokens.push({ type: 'identifier', value: ident });
10942
+ }
10943
+ continue;
10944
+ }
10945
+ // Unknown character — skip
10946
+ i++;
10947
+ }
10948
+ tokens.push({ type: 'eof' });
10949
+ return tokens;
10950
+ }
10951
+ class Parser {
10952
+ constructor(tokens, data) {
10953
+ this.pos = 0;
10954
+ this.tokens = tokens;
10955
+ this.data = data;
10956
+ }
10957
+ peek() {
10958
+ return this.tokens[this.pos];
10959
+ }
10960
+ advance() {
10961
+ const token = this.tokens[this.pos];
10962
+ this.pos++;
10963
+ return token;
10964
+ }
10965
+ /**
10966
+ * Parse the full expression.
10967
+ * Grammar:
10968
+ * expr → or_expr
10969
+ * or_expr → and_expr ('||' and_expr)*
10970
+ * and_expr → unary (('==' | '!=' | '>' | '<' | '>=' | '<=') unary)?
10971
+ * | unary ('&&' unary_or_comparison)*
10972
+ * unary → '!' unary | primary
10973
+ * primary → '(' expr ')' | literal | identifier
10974
+ */
10975
+ parse() {
10976
+ const result = this.parseOr();
10977
+ return result;
10978
+ }
10979
+ parseOr() {
10980
+ let left = this.parseAnd();
10981
+ while (this.peek().type === 'operator' && this.peek().value === '||') {
10982
+ this.advance();
10983
+ const right = this.parseAnd();
10984
+ left = this.isTruthy(left) || this.isTruthy(right);
10985
+ }
10986
+ return left;
10987
+ }
10988
+ parseAnd() {
10989
+ let left = this.parseComparison();
10990
+ while (this.peek().type === 'operator' && this.peek().value === '&&') {
10991
+ this.advance();
10992
+ const right = this.parseComparison();
10993
+ left = this.isTruthy(left) && this.isTruthy(right);
10994
+ }
10995
+ return left;
10996
+ }
10997
+ parseComparison() {
10998
+ const left = this.parseUnary();
10999
+ const token = this.peek();
11000
+ if (token.type === 'operator') {
11001
+ const op = token.value;
11002
+ if (op === '==' || op === '!=' || op === '>' || op === '<' || op === '>=' || op === '<=' || op === '=~' || op === '!~') {
11003
+ this.advance();
11004
+ const right = this.parseUnary();
11005
+ return this.compare(left, op, right);
11006
+ }
11007
+ }
11008
+ return left;
11009
+ }
11010
+ parseUnary() {
11011
+ if (this.peek().type === 'not') {
11012
+ this.advance();
11013
+ const value = this.parseUnary();
11014
+ return !this.isTruthy(value);
11015
+ }
11016
+ return this.parsePrimary();
11017
+ }
11018
+ parsePrimary() {
11019
+ const token = this.peek();
11020
+ if (token.type === 'paren' && token.value === '(') {
11021
+ this.advance();
11022
+ const value = this.parseOr();
11023
+ // Consume closing paren
11024
+ if (this.peek().type === 'paren' && this.peek().value === ')') {
11025
+ this.advance();
11026
+ }
11027
+ return value;
11028
+ }
11029
+ if (token.type === 'string') {
11030
+ this.advance();
11031
+ return token.value;
11032
+ }
11033
+ if (token.type === 'number') {
11034
+ this.advance();
11035
+ return token.value;
11036
+ }
11037
+ if (token.type === 'boolean') {
11038
+ this.advance();
11039
+ return token.value;
11040
+ }
11041
+ if (token.type === 'identifier') {
11042
+ this.advance();
11043
+ return this.resolveIdentifier(token.value);
11044
+ }
11045
+ // EOF or unexpected — return undefined
11046
+ this.advance();
11047
+ return undefined;
11048
+ }
11049
+ resolveIdentifier(path) {
11050
+ const parts = path.split('.');
11051
+ let current = this.data;
11052
+ for (const part of parts) {
11053
+ if (current === null || current === undefined) {
11054
+ return undefined;
11055
+ }
11056
+ if (typeof current === 'object') {
11057
+ current = current[part];
11058
+ }
11059
+ else {
11060
+ return undefined;
11061
+ }
11062
+ }
11063
+ return current;
11064
+ }
11065
+ compare(left, op, right) {
11066
+ // Regex match: left is coerced to string, right is the pattern string
11067
+ if (op === '=~' || op === '!~') {
11068
+ const str = this.toString(left);
11069
+ const pattern = this.toString(right);
11070
+ try {
11071
+ const regex = new RegExp(pattern);
11072
+ const matches = regex.test(str);
11073
+ return op === '=~' ? matches : !matches;
11074
+ }
11075
+ catch {
11076
+ // Invalid regex pattern — treat as no match
11077
+ return op === '!~';
11078
+ }
11079
+ }
11080
+ // For ordering operators, coerce both sides to numbers if either side is numeric
11081
+ if (op === '>' || op === '<' || op === '>=' || op === '<=') {
11082
+ const l = this.toNumber(left);
11083
+ const r = this.toNumber(right);
11084
+ switch (op) {
11085
+ case '>': return l > r;
11086
+ case '<': return l < r;
11087
+ case '>=': return l >= r;
11088
+ case '<=': return l <= r;
11089
+ }
11090
+ }
11091
+ // For equality, coerce to numbers if both sides look numeric
11092
+ const ln = this.toNumberIfNumeric(left);
11093
+ const rn = this.toNumberIfNumeric(right);
11094
+ switch (op) {
11095
+ case '==': return ln == rn; // eslint-disable-line eqeqeq
11096
+ case '!=': return ln != rn; // eslint-disable-line eqeqeq
11097
+ default: return false;
11098
+ }
11099
+ }
11100
+ /**
11101
+ * Convert a value to a string for regex matching.
11102
+ */
11103
+ toString(value) {
11104
+ if (value === null || value === undefined)
11105
+ return '';
11106
+ if (typeof value === 'string')
11107
+ return value;
11108
+ return String(value);
11109
+ }
11110
+ /**
11111
+ * Convert a value to a number. Strings that look like numbers are parsed.
11112
+ * Non-numeric values become NaN.
11113
+ */
11114
+ toNumber(value) {
11115
+ if (typeof value === 'number')
11116
+ return value;
11117
+ if (typeof value === 'string') {
11118
+ const n = Number(value);
11119
+ return isNaN(n) ? NaN : n;
11120
+ }
11121
+ if (typeof value === 'boolean')
11122
+ return value ? 1 : 0;
11123
+ return NaN;
11124
+ }
11125
+ /**
11126
+ * If a value is a string that looks like a number, convert it.
11127
+ * Otherwise return the value as-is. Used for == / != so that
11128
+ * "5" == 5 is true but "hello" == "hello" still works.
11129
+ */
11130
+ toNumberIfNumeric(value) {
11131
+ if (typeof value === 'string' && value.length > 0) {
11132
+ const n = Number(value);
11133
+ if (!isNaN(n))
11134
+ return n;
11135
+ }
11136
+ return value;
11137
+ }
11138
+ isTruthy(value) {
11139
+ if (value === null || value === undefined)
11140
+ return false;
11141
+ if (typeof value === 'boolean')
11142
+ return value;
11143
+ if (typeof value === 'number')
11144
+ return value !== 0;
11145
+ if (typeof value === 'string')
11146
+ return value.length > 0;
11147
+ if (Array.isArray(value))
11148
+ return value.length > 0;
11149
+ return true;
11150
+ }
11151
+ }
11152
+ /**
11153
+ * Static predicate evaluator for conditional sections.
11154
+ */
11155
+ class PredicateEvaluator {
11156
+ /**
11157
+ * Evaluate a predicate expression against data.
11158
+ * @param predicate The predicate string (e.g., "isActive", "count > 0")
11159
+ * @param data The data context to evaluate against
11160
+ * @returns true if the predicate is truthy, false otherwise
11161
+ */
11162
+ static evaluate(predicate, data) {
11163
+ if (!predicate || predicate.trim().length === 0) {
11164
+ return false;
11165
+ }
11166
+ try {
11167
+ const tokens = tokenize(predicate.trim());
11168
+ const parser = new Parser(tokens, data);
11169
+ const result = parser.parse();
11170
+ // Convert result to boolean
11171
+ if (result === null || result === undefined)
11172
+ return false;
11173
+ if (typeof result === 'boolean')
11174
+ return result;
11175
+ if (typeof result === 'number')
11176
+ return result !== 0;
11177
+ if (typeof result === 'string')
11178
+ return result.length > 0;
11179
+ if (Array.isArray(result))
11180
+ return result.length > 0;
11181
+ return true;
11182
+ }
11183
+ catch {
11184
+ // If evaluation fails, treat as false
11185
+ return false;
11186
+ }
11187
+ }
11188
+ }
11189
+
10123
11190
  /**
10124
11191
  * Abstract base class providing common functionality for regions.
10125
11192
  */
@@ -11155,6 +12222,11 @@ const LOOP_INDICATOR_COLOR = '#6B46C1'; // Purple
11155
12222
  const LOOP_LABEL_PADDING = 4;
11156
12223
  const LOOP_LABEL_RADIUS = 4;
11157
12224
  const LOOP_LINE_DASH = [4, 4];
12225
+ // Conditional section indicator styling
12226
+ const COND_INDICATOR_COLOR = '#D97706'; // Orange
12227
+ const COND_LABEL_PADDING = 4;
12228
+ const COND_LABEL_RADIUS = 4;
12229
+ const COND_LINE_DASH = [4, 4];
11158
12230
  // Hyperlink styling
11159
12231
  const DEFAULT_HYPERLINK_COLOR = '#0066CC'; // Blue
11160
12232
  class FlowingTextRenderer extends EventEmitter {
@@ -11412,8 +12484,6 @@ class FlowingTextRenderer extends EventEmitter {
11412
12484
  if (pageIndex === 0) {
11413
12485
  // Clear table continuations when starting a new render cycle
11414
12486
  this.clearTableContinuations();
11415
- // Clear content hit targets - they will be re-registered during render
11416
- this._hitTestManager.clearCategory('content');
11417
12487
  // This is the first page, flow all text
11418
12488
  const flowedPages = this.flowTextForPage(page, ctx, contentBounds);
11419
12489
  this.flowedPages.set(page.id, flowedPages);
@@ -11713,6 +12783,8 @@ class FlowingTextRenderer extends EventEmitter {
11713
12783
  const pageCount = firstPage ? (this.flowedPages.get(firstPage.id)?.length || 1) : 1;
11714
12784
  // Get hyperlinks for rendering
11715
12785
  const hyperlinks = flowingContent.getAllHyperlinks();
12786
+ // Track relative objects to render after all lines (so they appear on top)
12787
+ const relativeObjects = [];
11716
12788
  // Render each line
11717
12789
  let y = bounds.y;
11718
12790
  for (let lineIndex = 0; lineIndex < flowedLines.length; lineIndex++) {
@@ -11725,6 +12797,18 @@ class FlowingTextRenderer extends EventEmitter {
11725
12797
  if (clipToBounds && y > bounds.y + bounds.height) {
11726
12798
  break;
11727
12799
  }
12800
+ // Collect relative objects from this line
12801
+ if (line.embeddedObjects) {
12802
+ for (const embeddedObj of line.embeddedObjects) {
12803
+ if (embeddedObj.isAnchor && embeddedObj.object.position === 'relative') {
12804
+ relativeObjects.push({
12805
+ object: embeddedObj.object,
12806
+ anchorX: bounds.x,
12807
+ anchorY: y
12808
+ });
12809
+ }
12810
+ }
12811
+ }
11728
12812
  this.renderFlowedLine(line, ctx, { x: bounds.x, y }, maxWidth, pageIndex, cursorTextIndex, pageCount, hyperlinks);
11729
12813
  y += line.height;
11730
12814
  }
@@ -11735,6 +12819,10 @@ class FlowingTextRenderer extends EventEmitter {
11735
12819
  if (clipToBounds) {
11736
12820
  ctx.restore();
11737
12821
  }
12822
+ // Render relative objects on top of text (outside clip region)
12823
+ if (relativeObjects.length > 0) {
12824
+ this.renderRelativeObjects(relativeObjects, ctx, pageIndex);
12825
+ }
11738
12826
  }
11739
12827
  /**
11740
12828
  * Render selection highlight for a region.
@@ -13794,11 +14882,283 @@ class FlowingTextRenderer extends EventEmitter {
13794
14882
  verticalEndY = endInfo.y;
13795
14883
  }
13796
14884
  else if (endsAfterPage) {
13797
- // Section continues to next page, end at bottom of content area
14885
+ // Section continues to next page, end at bottom of content area
14886
+ verticalEndY = contentBounds.y + contentBounds.height;
14887
+ }
14888
+ else {
14889
+ verticalEndY = verticalStartY; // No vertical line if neither start nor end
14890
+ }
14891
+ if (verticalEndY > verticalStartY) {
14892
+ ctx.beginPath();
14893
+ ctx.moveTo(connectorX, verticalStartY);
14894
+ ctx.lineTo(connectorX, verticalEndY);
14895
+ ctx.stroke();
14896
+ }
14897
+ // Draw "Loop" label last so it's in front of all lines
14898
+ if (hasStart) {
14899
+ const startY = startInfo.y;
14900
+ this.drawLoopLabel(ctx, labelX, startY - 10, 'Loop', isSelected);
14901
+ }
14902
+ // Update section's visual state
14903
+ section.visualState = {
14904
+ startPageIndex: hasStart ? pageIndex : -1,
14905
+ startY: hasStart ? startInfo.y : 0,
14906
+ endPageIndex: hasEnd ? pageIndex : -1,
14907
+ endY: hasEnd ? endInfo.y : 0,
14908
+ spansMultiplePages: !hasStart || !hasEnd
14909
+ };
14910
+ ctx.restore();
14911
+ }
14912
+ /**
14913
+ * Draw the "Loop" label in a rounded rectangle.
14914
+ * When not selected, draws an outlined rectangle.
14915
+ * When selected, draws a filled rectangle.
14916
+ */
14917
+ drawLoopLabel(ctx, x, y, text, isSelected = false) {
14918
+ ctx.save();
14919
+ ctx.font = '10px Arial';
14920
+ const metrics = ctx.measureText(text);
14921
+ const textWidth = metrics.width;
14922
+ const textHeight = 10;
14923
+ const boxWidth = textWidth + LOOP_LABEL_PADDING * 2;
14924
+ const boxHeight = textHeight + LOOP_LABEL_PADDING * 2;
14925
+ ctx.beginPath();
14926
+ this.roundRect(ctx, x, y, boxWidth, boxHeight, LOOP_LABEL_RADIUS);
14927
+ if (isSelected) {
14928
+ // Selected: filled background with white text
14929
+ ctx.fillStyle = LOOP_INDICATOR_COLOR;
14930
+ ctx.fill();
14931
+ ctx.fillStyle = '#ffffff';
14932
+ }
14933
+ else {
14934
+ // Not selected: white background, outlined with colored text
14935
+ ctx.fillStyle = '#ffffff';
14936
+ ctx.fill();
14937
+ ctx.strokeStyle = LOOP_INDICATOR_COLOR;
14938
+ ctx.lineWidth = 1.5;
14939
+ ctx.stroke();
14940
+ ctx.fillStyle = LOOP_INDICATOR_COLOR;
14941
+ }
14942
+ // Draw text
14943
+ ctx.textBaseline = 'middle';
14944
+ ctx.fillText(text, x + LOOP_LABEL_PADDING, y + boxHeight / 2);
14945
+ ctx.restore();
14946
+ }
14947
+ /**
14948
+ * Draw a rounded rectangle path.
14949
+ */
14950
+ roundRect(ctx, x, y, width, height, radius) {
14951
+ ctx.moveTo(x + radius, y);
14952
+ ctx.lineTo(x + width - radius, y);
14953
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
14954
+ ctx.lineTo(x + width, y + height - radius);
14955
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
14956
+ ctx.lineTo(x + radius, y + height);
14957
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
14958
+ ctx.lineTo(x, y + radius);
14959
+ ctx.quadraticCurveTo(x, y, x + radius, y);
14960
+ ctx.closePath();
14961
+ }
14962
+ /**
14963
+ * Find the Y position for a text index on a flowed page.
14964
+ * Returns the Y position at the TOP of the line containing the text index.
14965
+ */
14966
+ findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
14967
+ let y = contentBounds.y;
14968
+ for (let i = 0; i < flowedPage.lines.length; i++) {
14969
+ const line = flowedPage.lines[i];
14970
+ // Check if this line contains the text index
14971
+ if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
14972
+ return { y, lineIndex: i };
14973
+ }
14974
+ // Check if text index is exactly at the start of this line
14975
+ // (for section boundaries that are at paragraph starts)
14976
+ if (textIndex === line.startIndex) {
14977
+ return { y, lineIndex: i };
14978
+ }
14979
+ y += line.height;
14980
+ }
14981
+ // Check if text index is just past the last line (end of content)
14982
+ if (flowedPage.lines.length > 0) {
14983
+ const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
14984
+ if (textIndex === lastLine.endIndex + 1) {
14985
+ return { y, lineIndex: flowedPage.lines.length - 1 };
14986
+ }
14987
+ }
14988
+ return null;
14989
+ }
14990
+ /**
14991
+ * Check if a section spans across a flowed page (starts before and ends after).
14992
+ */
14993
+ sectionSpansPage(section, flowedPage) {
14994
+ if (flowedPage.lines.length === 0)
14995
+ return false;
14996
+ const pageStart = flowedPage.startIndex;
14997
+ const pageEnd = flowedPage.endIndex;
14998
+ // Section spans this page if it started before and ends after
14999
+ return section.startIndex < pageStart && section.endIndex > pageEnd;
15000
+ }
15001
+ /**
15002
+ * Get a repeating section at a point (for click detection).
15003
+ * Checks if the point is on the Loop label or vertical connector.
15004
+ */
15005
+ getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
15006
+ const labelX = pageBounds.x + 5;
15007
+ const labelWidth = 32;
15008
+ const connectorX = labelX + labelWidth / 2;
15009
+ const hitRadius = 10; // Pixels for click detection
15010
+ for (const section of sections) {
15011
+ const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
15012
+ const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
15013
+ const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
15014
+ if (!startInfo && !endInfo && !sectionSpansThisPage) {
15015
+ continue;
15016
+ }
15017
+ // Check if click is on the Loop label
15018
+ if (startInfo) {
15019
+ const labelY = startInfo.y - 10;
15020
+ const labelHeight = 18;
15021
+ if (point.x >= labelX &&
15022
+ point.x <= labelX + labelWidth &&
15023
+ point.y >= labelY &&
15024
+ point.y <= labelY + labelHeight) {
15025
+ return section;
15026
+ }
15027
+ }
15028
+ // Check if click is on the vertical connector line
15029
+ let verticalStartY;
15030
+ let verticalEndY;
15031
+ if (startInfo) {
15032
+ verticalStartY = startInfo.y;
15033
+ }
15034
+ else {
15035
+ verticalStartY = contentBounds.y;
15036
+ }
15037
+ if (endInfo) {
15038
+ verticalEndY = endInfo.y;
15039
+ }
15040
+ else if (sectionSpansThisPage) {
15041
+ verticalEndY = contentBounds.y + flowedPage.height;
15042
+ }
15043
+ else {
15044
+ continue;
15045
+ }
15046
+ if (Math.abs(point.x - connectorX) <= hitRadius &&
15047
+ point.y >= verticalStartY &&
15048
+ point.y <= verticalEndY) {
15049
+ return section;
15050
+ }
15051
+ }
15052
+ return null;
15053
+ }
15054
+ // ============================================
15055
+ // Conditional Section Indicators
15056
+ // ============================================
15057
+ /**
15058
+ * Render conditional section indicators for a page.
15059
+ */
15060
+ renderConditionalSectionIndicators(sections, pageIndex, ctx, contentBounds, flowedPage, pageBounds, selectedSectionId = null) {
15061
+ for (const section of sections) {
15062
+ this.renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, pageBounds, section.id === selectedSectionId);
15063
+ }
15064
+ }
15065
+ /**
15066
+ * Render a single conditional section indicator.
15067
+ */
15068
+ renderConditionalIndicator(section, pageIndex, ctx, contentBounds, flowedPage, _pageBounds, isSelected = false) {
15069
+ const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
15070
+ const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
15071
+ const sectionOverlapsPage = section.startIndex < flowedPage.endIndex &&
15072
+ section.endIndex > flowedPage.startIndex;
15073
+ if (!sectionOverlapsPage) {
15074
+ return;
15075
+ }
15076
+ const hasStart = startInfo !== null;
15077
+ const hasEnd = endInfo !== null;
15078
+ const startsBeforePage = section.startIndex < flowedPage.startIndex;
15079
+ const endsAfterPage = section.endIndex > flowedPage.endIndex;
15080
+ ctx.save();
15081
+ ctx.strokeStyle = COND_INDICATOR_COLOR;
15082
+ ctx.fillStyle = COND_INDICATOR_COLOR;
15083
+ ctx.lineWidth = 1;
15084
+ // Position on the right side of the content area
15085
+ const labelWidth = 22;
15086
+ const labelX = contentBounds.x + contentBounds.width + 5;
15087
+ const connectorX = labelX + labelWidth / 2;
15088
+ // Draw start indicator lines
15089
+ if (hasStart) {
15090
+ const startY = startInfo.y;
15091
+ ctx.setLineDash(COND_LINE_DASH);
15092
+ ctx.beginPath();
15093
+ ctx.moveTo(contentBounds.x, startY);
15094
+ ctx.lineTo(contentBounds.x + contentBounds.width, startY);
15095
+ ctx.stroke();
15096
+ ctx.setLineDash([]);
15097
+ ctx.beginPath();
15098
+ ctx.moveTo(contentBounds.x + contentBounds.width, startY);
15099
+ ctx.lineTo(labelX, startY);
15100
+ ctx.stroke();
15101
+ }
15102
+ else if (startsBeforePage) {
15103
+ const topY = contentBounds.y;
15104
+ ctx.setLineDash(COND_LINE_DASH);
15105
+ ctx.beginPath();
15106
+ ctx.moveTo(contentBounds.x, topY);
15107
+ ctx.lineTo(contentBounds.x + contentBounds.width, topY);
15108
+ ctx.stroke();
15109
+ ctx.setLineDash([]);
15110
+ ctx.beginPath();
15111
+ ctx.moveTo(connectorX, topY);
15112
+ ctx.lineTo(contentBounds.x + contentBounds.width, topY);
15113
+ ctx.stroke();
15114
+ }
15115
+ // Draw end indicator
15116
+ if (hasEnd) {
15117
+ const endY = endInfo.y;
15118
+ ctx.setLineDash(COND_LINE_DASH);
15119
+ ctx.beginPath();
15120
+ ctx.moveTo(contentBounds.x, endY);
15121
+ ctx.lineTo(contentBounds.x + contentBounds.width, endY);
15122
+ ctx.stroke();
15123
+ ctx.setLineDash([]);
15124
+ ctx.beginPath();
15125
+ ctx.moveTo(connectorX, endY);
15126
+ ctx.lineTo(contentBounds.x + contentBounds.width, endY);
15127
+ ctx.stroke();
15128
+ }
15129
+ else if (endsAfterPage) {
15130
+ const bottomY = contentBounds.y + contentBounds.height;
15131
+ ctx.setLineDash(COND_LINE_DASH);
15132
+ ctx.beginPath();
15133
+ ctx.moveTo(contentBounds.x, bottomY);
15134
+ ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
15135
+ ctx.stroke();
15136
+ ctx.setLineDash([]);
15137
+ ctx.beginPath();
15138
+ ctx.moveTo(connectorX, bottomY);
15139
+ ctx.lineTo(contentBounds.x + contentBounds.width, bottomY);
15140
+ ctx.stroke();
15141
+ }
15142
+ // Draw vertical connector line
15143
+ let verticalStartY;
15144
+ let verticalEndY;
15145
+ if (hasStart) {
15146
+ verticalStartY = startInfo.y;
15147
+ }
15148
+ else if (startsBeforePage) {
15149
+ verticalStartY = contentBounds.y;
15150
+ }
15151
+ else {
15152
+ verticalStartY = contentBounds.y;
15153
+ }
15154
+ if (hasEnd) {
15155
+ verticalEndY = endInfo.y;
15156
+ }
15157
+ else if (endsAfterPage) {
13798
15158
  verticalEndY = contentBounds.y + contentBounds.height;
13799
15159
  }
13800
15160
  else {
13801
- verticalEndY = verticalStartY; // No vertical line if neither start nor end
15161
+ verticalEndY = verticalStartY;
13802
15162
  }
13803
15163
  if (verticalEndY > verticalStartY) {
13804
15164
  ctx.beginPath();
@@ -13806,12 +15166,12 @@ class FlowingTextRenderer extends EventEmitter {
13806
15166
  ctx.lineTo(connectorX, verticalEndY);
13807
15167
  ctx.stroke();
13808
15168
  }
13809
- // Draw "Loop" label last so it's in front of all lines
15169
+ // Draw "If" label
13810
15170
  if (hasStart) {
13811
15171
  const startY = startInfo.y;
13812
- this.drawLoopLabel(ctx, labelX, startY - 10, 'Loop', isSelected);
15172
+ this.drawCondLabel(ctx, labelX, startY - 10, 'If', isSelected);
13813
15173
  }
13814
- // Update section's visual state
15174
+ // Update visual state
13815
15175
  section.visualState = {
13816
15176
  startPageIndex: hasStart ? pageIndex : -1,
13817
15177
  startY: hasStart ? startInfo.y : 0,
@@ -13822,111 +15182,52 @@ class FlowingTextRenderer extends EventEmitter {
13822
15182
  ctx.restore();
13823
15183
  }
13824
15184
  /**
13825
- * Draw the "Loop" label in a rounded rectangle.
13826
- * When not selected, draws an outlined rectangle.
13827
- * When selected, draws a filled rectangle.
15185
+ * Draw the "If" label in a rounded rectangle.
13828
15186
  */
13829
- drawLoopLabel(ctx, x, y, text, isSelected = false) {
15187
+ drawCondLabel(ctx, x, y, text, isSelected = false) {
13830
15188
  ctx.save();
13831
15189
  ctx.font = '10px Arial';
13832
15190
  const metrics = ctx.measureText(text);
13833
15191
  const textWidth = metrics.width;
13834
15192
  const textHeight = 10;
13835
- const boxWidth = textWidth + LOOP_LABEL_PADDING * 2;
13836
- const boxHeight = textHeight + LOOP_LABEL_PADDING * 2;
15193
+ const boxWidth = textWidth + COND_LABEL_PADDING * 2;
15194
+ const boxHeight = textHeight + COND_LABEL_PADDING * 2;
13837
15195
  ctx.beginPath();
13838
- this.roundRect(ctx, x, y, boxWidth, boxHeight, LOOP_LABEL_RADIUS);
15196
+ this.roundRect(ctx, x, y, boxWidth, boxHeight, COND_LABEL_RADIUS);
13839
15197
  if (isSelected) {
13840
- // Selected: filled background with white text
13841
- ctx.fillStyle = LOOP_INDICATOR_COLOR;
15198
+ ctx.fillStyle = COND_INDICATOR_COLOR;
13842
15199
  ctx.fill();
13843
15200
  ctx.fillStyle = '#ffffff';
13844
15201
  }
13845
15202
  else {
13846
- // Not selected: white background, outlined with colored text
13847
15203
  ctx.fillStyle = '#ffffff';
13848
15204
  ctx.fill();
13849
- ctx.strokeStyle = LOOP_INDICATOR_COLOR;
15205
+ ctx.strokeStyle = COND_INDICATOR_COLOR;
13850
15206
  ctx.lineWidth = 1.5;
13851
15207
  ctx.stroke();
13852
- ctx.fillStyle = LOOP_INDICATOR_COLOR;
15208
+ ctx.fillStyle = COND_INDICATOR_COLOR;
13853
15209
  }
13854
- // Draw text
13855
15210
  ctx.textBaseline = 'middle';
13856
- ctx.fillText(text, x + LOOP_LABEL_PADDING, y + boxHeight / 2);
15211
+ ctx.fillText(text, x + COND_LABEL_PADDING, y + boxHeight / 2);
13857
15212
  ctx.restore();
13858
15213
  }
13859
15214
  /**
13860
- * Draw a rounded rectangle path.
13861
- */
13862
- roundRect(ctx, x, y, width, height, radius) {
13863
- ctx.moveTo(x + radius, y);
13864
- ctx.lineTo(x + width - radius, y);
13865
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
13866
- ctx.lineTo(x + width, y + height - radius);
13867
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
13868
- ctx.lineTo(x + radius, y + height);
13869
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
13870
- ctx.lineTo(x, y + radius);
13871
- ctx.quadraticCurveTo(x, y, x + radius, y);
13872
- ctx.closePath();
13873
- }
13874
- /**
13875
- * Find the Y position for a text index on a flowed page.
13876
- * Returns the Y position at the TOP of the line containing the text index.
13877
- */
13878
- findLineYForTextIndex(flowedPage, textIndex, contentBounds) {
13879
- let y = contentBounds.y;
13880
- for (let i = 0; i < flowedPage.lines.length; i++) {
13881
- const line = flowedPage.lines[i];
13882
- // Check if this line contains the text index
13883
- if (textIndex >= line.startIndex && textIndex <= line.endIndex) {
13884
- return { y, lineIndex: i };
13885
- }
13886
- // Check if text index is exactly at the start of this line
13887
- // (for section boundaries that are at paragraph starts)
13888
- if (textIndex === line.startIndex) {
13889
- return { y, lineIndex: i };
13890
- }
13891
- y += line.height;
13892
- }
13893
- // Check if text index is just past the last line (end of content)
13894
- if (flowedPage.lines.length > 0) {
13895
- const lastLine = flowedPage.lines[flowedPage.lines.length - 1];
13896
- if (textIndex === lastLine.endIndex + 1) {
13897
- return { y, lineIndex: flowedPage.lines.length - 1 };
13898
- }
13899
- }
13900
- return null;
13901
- }
13902
- /**
13903
- * Check if a section spans across a flowed page (starts before and ends after).
13904
- */
13905
- sectionSpansPage(section, flowedPage) {
13906
- if (flowedPage.lines.length === 0)
13907
- return false;
13908
- const pageStart = flowedPage.startIndex;
13909
- const pageEnd = flowedPage.endIndex;
13910
- // Section spans this page if it started before and ends after
13911
- return section.startIndex < pageStart && section.endIndex > pageEnd;
13912
- }
13913
- /**
13914
- * Get a repeating section at a point (for click detection).
13915
- * Checks if the point is on the Loop label or vertical connector.
15215
+ * Get a conditional section at a point (for click detection).
13916
15216
  */
13917
- getRepeatingSectionAtPoint(point, sections, _pageIndex, pageBounds, contentBounds, flowedPage) {
13918
- const labelX = pageBounds.x + 5;
13919
- const labelWidth = 32;
15217
+ getConditionalSectionAtPoint(point, sections, _pageIndex, _pageBounds, contentBounds, flowedPage) {
15218
+ const labelWidth = 22;
15219
+ const labelX = contentBounds.x + contentBounds.width + 5;
13920
15220
  const connectorX = labelX + labelWidth / 2;
13921
- const hitRadius = 10; // Pixels for click detection
15221
+ const hitRadius = 10;
13922
15222
  for (const section of sections) {
13923
15223
  const startInfo = this.findLineYForTextIndex(flowedPage, section.startIndex, contentBounds);
13924
15224
  const endInfo = this.findLineYForTextIndex(flowedPage, section.endIndex, contentBounds);
13925
- const sectionSpansThisPage = this.sectionSpansPage(section, flowedPage);
15225
+ const sectionSpansThisPage = section.startIndex < flowedPage.startIndex &&
15226
+ section.endIndex > flowedPage.endIndex;
13926
15227
  if (!startInfo && !endInfo && !sectionSpansThisPage) {
13927
15228
  continue;
13928
15229
  }
13929
- // Check if click is on the Loop label
15230
+ // Check if click is on the "If" label
13930
15231
  if (startInfo) {
13931
15232
  const labelY = startInfo.y - 10;
13932
15233
  const labelHeight = 18;
@@ -13991,6 +15292,7 @@ class CanvasManager extends EventEmitter {
13991
15292
  this.isSelectingText = false;
13992
15293
  this.textSelectionStartPageId = null;
13993
15294
  this.selectedSectionId = null;
15295
+ this.selectedConditionalSectionId = null;
13994
15296
  this._activeSection = 'body';
13995
15297
  this.lastClickTime = 0;
13996
15298
  this.lastClickPosition = null;
@@ -14130,6 +15432,11 @@ class CanvasManager extends EventEmitter {
14130
15432
  }
14131
15433
  // 2. CONTENT: Render all text and elements
14132
15434
  const pageIndex = this.document.pages.findIndex(p => p.id === page.id);
15435
+ // Clear content hit targets before rendering all sections (header, body, footer)
15436
+ // so that each section's hit targets are re-registered during render
15437
+ if (pageIndex === 0) {
15438
+ this.flowingTextRenderer.hitTestManager.clearCategory('content');
15439
+ }
14133
15440
  // Render header content
14134
15441
  const headerRegion = this.regionManager.getHeaderRegion();
14135
15442
  this.flowingTextRenderer.renderHeaderText(page, ctx, this._activeSection === 'header', headerRegion ?? undefined, pageIndex);
@@ -14157,6 +15464,16 @@ class CanvasManager extends EventEmitter {
14157
15464
  this.flowingTextRenderer.renderRepeatingSectionIndicators(sections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedSectionId);
14158
15465
  }
14159
15466
  }
15467
+ // Render conditional section indicators (only in body)
15468
+ const condSections = bodyFlowingContent?.getConditionalSections() ?? [];
15469
+ if (condSections.length > 0) {
15470
+ const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
15471
+ if (flowedPages && flowedPages[pageIndex]) {
15472
+ const pageDimensions = page.getPageDimensions();
15473
+ const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
15474
+ this.flowingTextRenderer.renderConditionalSectionIndicators(condSections, pageIndex, ctx, contentRect, flowedPages[pageIndex], pageBounds, this.selectedConditionalSectionId);
15475
+ }
15476
+ }
14160
15477
  // Render all elements (without selection marks)
14161
15478
  this.renderPageElements(page, ctx);
14162
15479
  // 3. DISABLEMENT OVERLAYS: Draw overlays on inactive sections
@@ -14454,11 +15771,10 @@ class CanvasManager extends EventEmitter {
14454
15771
  const embeddedObjectHit = hitTestManager.queryByType(mouseDownPageIndex, point, 'embedded-object');
14455
15772
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14456
15773
  const object = embeddedObjectHit.data.object;
14457
- // Check which section the object belongs to - only interact if in active section
15774
+ // If object is in a different section, switch to that section first
14458
15775
  const objectSection = this.getSectionForEmbeddedObject(object);
14459
15776
  if (objectSection && objectSection !== this._activeSection) {
14460
- // Object is in a different section - ignore the interaction
14461
- return;
15777
+ this.setActiveSection(objectSection);
14462
15778
  }
14463
15779
  // For relative-positioned objects, prepare for potential drag
14464
15780
  // Don't start drag immediately - wait for threshold to allow double-click
@@ -14945,14 +16261,12 @@ class CanvasManager extends EventEmitter {
14945
16261
  const embeddedObjectHit = hitTestManager.queryByType(clickedPageIndex, point, 'embedded-object');
14946
16262
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
14947
16263
  const clickedObject = embeddedObjectHit.data.object;
14948
- // Check which section the object belongs to
16264
+ // If object is in a different section, switch to that section first
14949
16265
  const objectSection = this.getSectionForEmbeddedObject(clickedObject);
14950
- // Only allow selection if object is in the active section
14951
16266
  if (objectSection && objectSection !== this._activeSection) {
14952
- // Object is in a different section - ignore the click
14953
- return;
16267
+ this.setActiveSection(objectSection);
14954
16268
  }
14955
- // Clicked on embedded object in the active section - clear text selection and select it
16269
+ // Clicked on embedded object - clear text selection and select it
14956
16270
  const activeFlowingContent = this.getFlowingContentForActiveSection();
14957
16271
  if (activeFlowingContent) {
14958
16272
  activeFlowingContent.clearSelection();
@@ -14989,6 +16303,64 @@ class CanvasManager extends EventEmitter {
14989
16303
  }
14990
16304
  }
14991
16305
  }
16306
+ // Check if we clicked on a conditional section indicator
16307
+ if (bodyFlowingContent) {
16308
+ const condSections = bodyFlowingContent.getConditionalSections();
16309
+ if (condSections.length > 0 && page) {
16310
+ const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
16311
+ const flowedPages = this.flowingTextRenderer.getFlowedPagesForPage(this.document.pages[0].id);
16312
+ if (flowedPages && flowedPages[pageIndex]) {
16313
+ const contentBounds = page.getContentBounds();
16314
+ const contentRect = {
16315
+ x: contentBounds.position.x,
16316
+ y: contentBounds.position.y,
16317
+ width: contentBounds.size.width,
16318
+ height: contentBounds.size.height
16319
+ };
16320
+ const pageDimensions = page.getPageDimensions();
16321
+ const pageBounds = { x: 0, y: 0, width: pageDimensions.width, height: pageDimensions.height };
16322
+ const clickedCondSection = this.flowingTextRenderer.getConditionalSectionAtPoint(point, condSections, pageIndex, pageBounds, contentRect, flowedPages[pageIndex]);
16323
+ if (clickedCondSection) {
16324
+ this.clearSelection();
16325
+ this.selectedConditionalSectionId = clickedCondSection.id;
16326
+ this.render();
16327
+ this.emit('conditional-section-clicked', { section: clickedCondSection });
16328
+ return;
16329
+ }
16330
+ }
16331
+ }
16332
+ }
16333
+ // Check if we clicked on a table row loop label
16334
+ const clickedPageIdx = this.document.pages.findIndex(p => p.id === pageId);
16335
+ const bodyContent = this.document.bodyFlowingContent;
16336
+ if (bodyContent) {
16337
+ const embeddedObjects = bodyContent.getEmbeddedObjects();
16338
+ for (const [, obj] of embeddedObjects.entries()) {
16339
+ if (obj instanceof TableObject && obj.renderedPosition && obj.renderedPageIndex === clickedPageIdx) {
16340
+ // Convert to table-local coordinates
16341
+ const localPoint = {
16342
+ x: point.x - obj.renderedPosition.x,
16343
+ y: point.y - obj.renderedPosition.y
16344
+ };
16345
+ const clickedLoop = obj.getRowLoopAtPoint(localPoint);
16346
+ if (clickedLoop) {
16347
+ // Select this loop
16348
+ obj.selectRowLoop(clickedLoop.id);
16349
+ this.render();
16350
+ this.emit('table-row-loop-clicked', { table: obj, loop: clickedLoop });
16351
+ return;
16352
+ }
16353
+ // Check for row conditional click
16354
+ const clickedCond = obj.getRowConditionalAtPoint(localPoint);
16355
+ if (clickedCond) {
16356
+ obj.selectRowConditional(clickedCond.id);
16357
+ this.render();
16358
+ this.emit('table-row-conditional-clicked', { table: obj, conditional: clickedCond });
16359
+ return;
16360
+ }
16361
+ }
16362
+ }
16363
+ }
14992
16364
  // If no regular element was clicked, try flowing text using unified region click handler
14993
16365
  const ctx = this.contexts.get(pageId);
14994
16366
  const pageIndex = this.document.pages.findIndex(p => p.id === pageId);
@@ -15198,26 +16570,21 @@ class CanvasManager extends EventEmitter {
15198
16570
  const embeddedObjectHit = hitTestManager.queryByType(pageIndex, point, 'embedded-object');
15199
16571
  if (embeddedObjectHit && embeddedObjectHit.data.type === 'embedded-object') {
15200
16572
  const object = embeddedObjectHit.data.object;
15201
- // Only show interactive cursors for objects in the active section
15202
- const objectSection = this.getSectionForEmbeddedObject(object);
15203
- if (objectSection && objectSection !== this._activeSection) ;
15204
- else {
15205
- if (object.position === 'relative') {
15206
- canvas.style.cursor = 'move';
15207
- return;
15208
- }
15209
- // Show text cursor for objects in edit mode, arrow otherwise
15210
- if (object instanceof TextBoxObject && this.editingTextBox === object) {
15211
- canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15212
- }
15213
- else if (object instanceof TableObject && this._focusedControl === object) {
15214
- canvas.style.cursor = CanvasManager.TEXT_CURSOR;
15215
- }
15216
- else {
15217
- canvas.style.cursor = 'default';
15218
- }
16573
+ if (object.position === 'relative') {
16574
+ canvas.style.cursor = 'move';
15219
16575
  return;
15220
16576
  }
16577
+ // Show text cursor for objects in edit mode, arrow otherwise
16578
+ if (object instanceof TextBoxObject && this.editingTextBox === object) {
16579
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
16580
+ }
16581
+ else if (object instanceof TableObject && this._focusedControl === object) {
16582
+ canvas.style.cursor = CanvasManager.TEXT_CURSOR;
16583
+ }
16584
+ else {
16585
+ canvas.style.cursor = 'default';
16586
+ }
16587
+ return;
15221
16588
  }
15222
16589
  // Check for table cells (show text cursor)
15223
16590
  const tableCellHit = hitTestManager.queryByType(pageIndex, point, 'table-cell');
@@ -15407,6 +16774,7 @@ class CanvasManager extends EventEmitter {
15407
16774
  });
15408
16775
  this.selectedElements.clear();
15409
16776
  this.selectedSectionId = null;
16777
+ this.selectedConditionalSectionId = null;
15410
16778
  Logger.log('[pc-editor:CanvasManager] About to render after clearing selection...');
15411
16779
  this.render();
15412
16780
  this.updateResizeHandleHitTargets();
@@ -16489,8 +17857,10 @@ function drawLine(page, x1, y1, x2, y2, color, thickness, pageHeight) {
16489
17857
  * - Repeating section indicators, loop markers
16490
17858
  */
16491
17859
  class PDFGenerator {
16492
- constructor() {
17860
+ constructor(fontManager) {
16493
17861
  this.fontCache = new Map();
17862
+ this.customFontCache = new Map();
17863
+ this.fontManager = fontManager;
16494
17864
  }
16495
17865
  /**
16496
17866
  * Generate a PDF from the document.
@@ -16501,9 +17871,13 @@ class PDFGenerator {
16501
17871
  */
16502
17872
  async generate(document, flowedContent, _options) {
16503
17873
  const pdfDoc = await pdfLib.PDFDocument.create();
17874
+ pdfDoc.registerFontkit(fontkit);
16504
17875
  this.fontCache.clear();
17876
+ this.customFontCache.clear();
16505
17877
  // Embed standard fonts we'll need
16506
17878
  await this.embedStandardFonts(pdfDoc);
17879
+ // Embed any custom fonts that have font data
17880
+ await this.embedCustomFonts(pdfDoc);
16507
17881
  // Render each page
16508
17882
  for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex++) {
16509
17883
  try {
@@ -16625,11 +17999,59 @@ class PDFGenerator {
16625
17999
  }
16626
18000
  return result;
16627
18001
  }
18002
+ /**
18003
+ * Embed custom fonts that have raw font data available.
18004
+ */
18005
+ async embedCustomFonts(pdfDoc) {
18006
+ const fonts = this.fontManager.getAvailableFonts();
18007
+ for (const font of fonts) {
18008
+ if (font.source !== 'custom')
18009
+ continue;
18010
+ for (const variant of font.variants) {
18011
+ if (!variant.fontData)
18012
+ continue;
18013
+ const cacheKey = `custom:${font.family.toLowerCase()}:${variant.weight}:${variant.style}`;
18014
+ try {
18015
+ // Ensure we pass Uint8Array (some pdf-lib versions need it)
18016
+ const fontBytes = variant.fontData instanceof Uint8Array
18017
+ ? variant.fontData
18018
+ : new Uint8Array(variant.fontData);
18019
+ const embedded = await pdfDoc.embedFont(fontBytes, { subset: true });
18020
+ this.customFontCache.set(cacheKey, embedded);
18021
+ Logger.log('[pc-editor:PDFGenerator] Embedded custom font:', font.family, variant.weight, variant.style);
18022
+ }
18023
+ catch (e) {
18024
+ Logger.warn('[pc-editor:PDFGenerator] Failed to embed custom font:', font.family, e);
18025
+ }
18026
+ }
18027
+ }
18028
+ }
18029
+ /**
18030
+ * Check if a font family is a custom font with embedded data.
18031
+ */
18032
+ isCustomFont(family) {
18033
+ return !this.fontManager.isBuiltIn(family) && this.fontManager.isRegistered(family);
18034
+ }
16628
18035
  /**
16629
18036
  * Get a font from cache by formatting style.
18037
+ * Checks custom fonts first, then falls back to standard fonts.
16630
18038
  */
16631
18039
  getFont(formatting) {
16632
- const standardFont = getStandardFont(formatting.fontFamily || 'Arial', formatting.fontWeight, formatting.fontStyle);
18040
+ const family = formatting.fontFamily || 'Arial';
18041
+ const weight = formatting.fontWeight || 'normal';
18042
+ const style = formatting.fontStyle || 'normal';
18043
+ // Try custom font first
18044
+ const customKey = `custom:${family.toLowerCase()}:${weight}:${style}`;
18045
+ const customFont = this.customFontCache.get(customKey);
18046
+ if (customFont)
18047
+ return customFont;
18048
+ // Try custom font with normal variant as fallback
18049
+ const customNormalKey = `custom:${family.toLowerCase()}:normal:normal`;
18050
+ const customNormalFont = this.customFontCache.get(customNormalKey);
18051
+ if (customNormalFont)
18052
+ return customNormalFont;
18053
+ // Fall back to standard fonts
18054
+ const standardFont = getStandardFont(family, weight, style);
16633
18055
  return this.fontCache.get(standardFont) || this.fontCache.get(pdfLib.StandardFonts.Helvetica);
16634
18056
  }
16635
18057
  /**
@@ -16662,12 +18084,14 @@ class PDFGenerator {
16662
18084
  for (const run of line.runs) {
16663
18085
  if (!run.text)
16664
18086
  continue;
16665
- // Filter text to WinAnsi-compatible characters (standard PDF fonts limitation)
16666
- const safeText = this.filterToWinAnsi(run.text);
16667
- if (!safeText)
16668
- continue;
16669
18087
  // Ensure formatting has required properties with defaults
16670
18088
  const formatting = run.formatting || {};
18089
+ // Custom fonts support full Unicode; standard fonts need WinAnsi filtering
18090
+ const safeText = this.isCustomFont(formatting.fontFamily || 'Arial')
18091
+ ? run.text
18092
+ : this.filterToWinAnsi(run.text);
18093
+ if (!safeText)
18094
+ continue;
16671
18095
  const font = this.getFont(formatting);
16672
18096
  const fontSize = formatting.fontSize || 14;
16673
18097
  const color = parseColor(formatting.color || '#000000');
@@ -21342,6 +22766,156 @@ class PDFImporter {
21342
22766
  }
21343
22767
  }
21344
22768
 
22769
+ /**
22770
+ * FontManager - Manages font registration and availability for the editor.
22771
+ *
22772
+ * Built-in fonts are web-safe and map to pdf-lib StandardFonts.
22773
+ * Custom fonts are loaded via the FontFace API for canvas rendering
22774
+ * and their raw bytes are stored for PDF embedding.
22775
+ */
22776
+ /**
22777
+ * Built-in web-safe fonts that need no loading.
22778
+ */
22779
+ const BUILT_IN_FONTS = [
22780
+ 'Arial',
22781
+ 'Times New Roman',
22782
+ 'Courier New',
22783
+ 'Georgia',
22784
+ 'Verdana'
22785
+ ];
22786
+ class FontManager extends EventEmitter {
22787
+ constructor() {
22788
+ super();
22789
+ this.fonts = new Map();
22790
+ // Register built-in fonts
22791
+ for (const family of BUILT_IN_FONTS) {
22792
+ this.fonts.set(family.toLowerCase(), {
22793
+ family,
22794
+ source: 'built-in',
22795
+ variants: [{
22796
+ weight: 'normal',
22797
+ style: 'normal',
22798
+ fontData: null,
22799
+ loaded: true
22800
+ }]
22801
+ });
22802
+ }
22803
+ }
22804
+ /**
22805
+ * Register a custom font. Fetches the font data if a URL is provided,
22806
+ * creates a FontFace for canvas rendering, and stores the raw bytes for PDF embedding.
22807
+ */
22808
+ async registerFont(options) {
22809
+ const { family, url, data, weight = 'normal', style = 'normal' } = options;
22810
+ Logger.log('[pc-editor:FontManager] registerFont', family, weight, style);
22811
+ let fontData = null;
22812
+ // Get font bytes
22813
+ if (data) {
22814
+ fontData = data;
22815
+ }
22816
+ else if (url) {
22817
+ try {
22818
+ const response = await fetch(url);
22819
+ if (!response.ok) {
22820
+ throw new Error(`Failed to fetch font: ${response.status} ${response.statusText}`);
22821
+ }
22822
+ fontData = await response.arrayBuffer();
22823
+ }
22824
+ catch (e) {
22825
+ Logger.error(`[pc-editor:FontManager] Failed to fetch font "${family}" from ${url}:`, e);
22826
+ throw e;
22827
+ }
22828
+ }
22829
+ // Create FontFace for canvas rendering
22830
+ if (fontData && typeof FontFace !== 'undefined') {
22831
+ try {
22832
+ const fontFace = new FontFace(family, fontData, {
22833
+ weight,
22834
+ style
22835
+ });
22836
+ await fontFace.load();
22837
+ document.fonts.add(fontFace);
22838
+ Logger.log('[pc-editor:FontManager] FontFace loaded:', family, weight, style);
22839
+ }
22840
+ catch (e) {
22841
+ Logger.error(`[pc-editor:FontManager] Failed to load FontFace "${family}":`, e);
22842
+ throw e;
22843
+ }
22844
+ }
22845
+ // Register in our map
22846
+ const key = family.toLowerCase();
22847
+ let registration = this.fonts.get(key);
22848
+ if (!registration) {
22849
+ registration = {
22850
+ family,
22851
+ source: 'custom',
22852
+ variants: []
22853
+ };
22854
+ this.fonts.set(key, registration);
22855
+ }
22856
+ else if (registration.source === 'built-in') {
22857
+ // Upgrading a built-in font with custom data (e.g., for PDF embedding)
22858
+ registration.source = 'custom';
22859
+ }
22860
+ // Add or update variant
22861
+ const existingVariant = registration.variants.find(v => v.weight === weight && v.style === style);
22862
+ if (existingVariant) {
22863
+ existingVariant.fontData = fontData;
22864
+ existingVariant.loaded = true;
22865
+ }
22866
+ else {
22867
+ registration.variants.push({
22868
+ weight,
22869
+ style,
22870
+ fontData,
22871
+ loaded: true
22872
+ });
22873
+ }
22874
+ this.emit('font-registered', { family, weight, style });
22875
+ }
22876
+ /**
22877
+ * Get all registered font families.
22878
+ */
22879
+ getAvailableFonts() {
22880
+ return Array.from(this.fonts.values());
22881
+ }
22882
+ /**
22883
+ * Get all available font family names.
22884
+ */
22885
+ getAvailableFontFamilies() {
22886
+ return Array.from(this.fonts.values()).map(f => f.family);
22887
+ }
22888
+ /**
22889
+ * Check if a font family is built-in.
22890
+ */
22891
+ isBuiltIn(family) {
22892
+ const reg = this.fonts.get(family.toLowerCase());
22893
+ return reg?.source === 'built-in';
22894
+ }
22895
+ /**
22896
+ * Check if a font family is registered (built-in or custom).
22897
+ */
22898
+ isRegistered(family) {
22899
+ return this.fonts.has(family.toLowerCase());
22900
+ }
22901
+ /**
22902
+ * Get raw font bytes for PDF embedding.
22903
+ * Returns null for built-in fonts (they use StandardFonts in pdf-lib).
22904
+ */
22905
+ getFontData(family, weight = 'normal', style = 'normal') {
22906
+ const reg = this.fonts.get(family.toLowerCase());
22907
+ if (!reg)
22908
+ return null;
22909
+ // Try exact match first
22910
+ const exact = reg.variants.find(v => v.weight === weight && v.style === style);
22911
+ if (exact?.fontData)
22912
+ return exact.fontData;
22913
+ // Fall back to normal variant
22914
+ const normal = reg.variants.find(v => v.weight === 'normal' && v.style === 'normal');
22915
+ return normal?.fontData || null;
22916
+ }
22917
+ }
22918
+
21345
22919
  class PCEditor extends EventEmitter {
21346
22920
  constructor(container, options) {
21347
22921
  super();
@@ -21368,7 +22942,8 @@ class PCEditor extends EventEmitter {
21368
22942
  units: this.options.units
21369
22943
  });
21370
22944
  this.dataBinder = new DataBinder();
21371
- this.pdfGenerator = new PDFGenerator();
22945
+ this.fontManager = new FontManager();
22946
+ this.pdfGenerator = new PDFGenerator(this.fontManager);
21372
22947
  this.clipboardManager = new ClipboardManager();
21373
22948
  this.initialize();
21374
22949
  }
@@ -21534,6 +23109,10 @@ class PCEditor extends EventEmitter {
21534
23109
  this.canvasManager.on('table-cell-selection-changed', (data) => {
21535
23110
  this.emit('table-cell-selection-changed', data);
21536
23111
  });
23112
+ // Forward table row loop clicks
23113
+ this.canvasManager.on('table-row-loop-clicked', (data) => {
23114
+ this.emit('table-row-loop-clicked', data);
23115
+ });
21537
23116
  this.canvasManager.on('repeating-section-clicked', (data) => {
21538
23117
  // Repeating section clicked - update selection state
21539
23118
  if (data.section && data.section.id) {
@@ -21544,6 +23123,16 @@ class PCEditor extends EventEmitter {
21544
23123
  this.emitSelectionChange();
21545
23124
  }
21546
23125
  });
23126
+ this.canvasManager.on('conditional-section-clicked', (data) => {
23127
+ // Conditional section clicked - update selection state
23128
+ if (data.section && data.section.id) {
23129
+ this.currentSelection = {
23130
+ type: 'conditional-section',
23131
+ sectionId: data.section.id
23132
+ };
23133
+ this.emitSelectionChange();
23134
+ }
23135
+ });
21547
23136
  // Listen for section focus changes from CanvasManager (double-click)
21548
23137
  this.canvasManager.on('section-focus-changed', (data) => {
21549
23138
  // Update our internal state to match the canvas manager
@@ -22372,17 +23961,24 @@ class PCEditor extends EventEmitter {
22372
23961
  this.selectAll();
22373
23962
  return;
22374
23963
  }
22375
- // If an embedded object is selected (but not being edited), arrow keys should deselect it
22376
- // and move the cursor in the text flow
22377
- const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
22378
- if (isArrowKey && this.canvasManager.hasSelectedElements()) {
22379
- // Check if we're not in editing mode
23964
+ // If an embedded object is selected (but not being edited), handle special keys
23965
+ if (this.canvasManager.hasSelectedElements()) {
22380
23966
  const editingTextBox = this.canvasManager.getEditingTextBox();
22381
23967
  const focusedTable = this.canvasManager.getFocusedControl();
22382
23968
  const isEditing = editingTextBox?.editing || (focusedTable instanceof TableObject && focusedTable.editing);
22383
23969
  if (!isEditing) {
22384
- // Clear the selection and let the key be handled by the body content
22385
- this.canvasManager.clearSelection();
23970
+ // Arrow keys: deselect and move cursor in text flow
23971
+ const isArrowKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key);
23972
+ if (isArrowKey) {
23973
+ this.canvasManager.clearSelection();
23974
+ // Fall through to normal key handling
23975
+ }
23976
+ // Backspace/Delete: delete the selected object from the text flow
23977
+ if (e.key === 'Backspace' || e.key === 'Delete') {
23978
+ e.preventDefault();
23979
+ this.deleteSelectedElements();
23980
+ return;
23981
+ }
22386
23982
  }
22387
23983
  }
22388
23984
  // Use the unified focus system to get the currently focused control
@@ -22485,6 +24081,32 @@ class PCEditor extends EventEmitter {
22485
24081
  this.canvasManager.clearSelection();
22486
24082
  this.canvasManager.render();
22487
24083
  }
24084
+ /**
24085
+ * Delete all currently selected embedded objects from the text flow.
24086
+ */
24087
+ deleteSelectedElements() {
24088
+ const selectedElements = this.canvasManager.getSelectedElements();
24089
+ if (selectedElements.length === 0)
24090
+ return;
24091
+ for (const elementId of selectedElements) {
24092
+ const objectInfo = this.findEmbeddedObjectInfo(elementId);
24093
+ if (objectInfo) {
24094
+ // Delete the placeholder character at the object's text index
24095
+ // This removes the object from the text flow
24096
+ objectInfo.content.deleteText(objectInfo.textIndex, 1);
24097
+ // Return focus to the parent flowing content
24098
+ const cursorPos = Math.min(objectInfo.textIndex, objectInfo.content.getText().length);
24099
+ objectInfo.content.setCursorPosition(cursorPos);
24100
+ this.canvasManager.setFocus(objectInfo.content);
24101
+ if (objectInfo.section !== this.canvasManager.getActiveSection()) {
24102
+ this.canvasManager.setActiveSection(objectInfo.section);
24103
+ }
24104
+ }
24105
+ }
24106
+ this.canvasManager.clearSelection();
24107
+ this.canvasManager.render();
24108
+ this.emit('content-changed', {});
24109
+ }
22488
24110
  /**
22489
24111
  * Find embedded object info by ID across all flowing content sources.
22490
24112
  */
@@ -23602,11 +25224,17 @@ class PCEditor extends EventEmitter {
23602
25224
  let totalFieldCount = 0;
23603
25225
  // Step 1: Expand repeating sections in body (header/footer don't support them)
23604
25226
  this.expandRepeatingSections(bodyContent, data);
23605
- // Step 2: Expand table row loops in body, header, and footer
25227
+ // Step 2: Evaluate conditional sections in body (remove content where predicate is false)
25228
+ this.evaluateConditionalSections(bodyContent, data);
25229
+ // Step 3: Expand table row loops in body, header, and footer
23606
25230
  this.expandTableRowLoops(bodyContent, data);
23607
25231
  this.expandTableRowLoops(this.document.headerFlowingContent, data);
23608
25232
  this.expandTableRowLoops(this.document.footerFlowingContent, data);
23609
- // Step 3: Substitute all fields in body
25233
+ // Step 4: Evaluate table row conditionals in body, header, and footer
25234
+ this.evaluateTableRowConditionals(bodyContent, data);
25235
+ this.evaluateTableRowConditionals(this.document.headerFlowingContent, data);
25236
+ this.evaluateTableRowConditionals(this.document.footerFlowingContent, data);
25237
+ // Step 5: Substitute all fields in body
23610
25238
  totalFieldCount += this.substituteFieldsInContent(bodyContent, data);
23611
25239
  // Step 4: Substitute all fields in embedded objects in body
23612
25240
  totalFieldCount += this.substituteFieldsInEmbeddedObjects(bodyContent, data);
@@ -23809,14 +25437,67 @@ class PCEditor extends EventEmitter {
23809
25437
  }
23810
25438
  }
23811
25439
  }
23812
- // Rewrite field names in the original (first) iteration to use index 0
23813
- for (const field of fieldsInSection) {
23814
- const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
23815
- fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
25440
+ // Rewrite field names in the original (first) iteration to use index 0
25441
+ for (const field of fieldsInSection) {
25442
+ const newFieldName = this.rewriteFieldNameWithIndex(field.fieldName, section.fieldPath, 0);
25443
+ fieldManager.updateFieldConfig(field.textIndex, { fieldName: newFieldName });
25444
+ }
25445
+ // Remove the section after expansion
25446
+ sectionManager.remove(section.id);
25447
+ }
25448
+ }
25449
+ /**
25450
+ * Evaluate conditional sections by removing content where predicate is false.
25451
+ * Processes sections from end to start to preserve text indices.
25452
+ */
25453
+ evaluateConditionalSections(flowingContent, data) {
25454
+ const sectionManager = flowingContent.getConditionalSectionManager();
25455
+ // Get sections in descending order (process end-to-start)
25456
+ const sections = sectionManager.getSectionsDescending();
25457
+ for (const section of sections) {
25458
+ const result = PredicateEvaluator.evaluate(section.predicate, data);
25459
+ if (!result) {
25460
+ // Predicate is false — remove the content within this section
25461
+ const deleteStart = section.startIndex;
25462
+ const deleteLength = section.endIndex - section.startIndex;
25463
+ flowingContent.deleteText(deleteStart, deleteLength);
25464
+ }
25465
+ // Remove the conditional section marker regardless
25466
+ sectionManager.remove(section.id);
25467
+ }
25468
+ }
25469
+ /**
25470
+ * Evaluate table row conditionals in embedded tables within a FlowingTextContent.
25471
+ * For each table with row conditionals, removes rows where predicate is false.
25472
+ */
25473
+ evaluateTableRowConditionals(flowingContent, data) {
25474
+ const embeddedObjects = flowingContent.getEmbeddedObjects();
25475
+ for (const [, obj] of embeddedObjects.entries()) {
25476
+ if (obj instanceof TableObject) {
25477
+ this.evaluateTableRowConditionalsInTable(obj, data);
25478
+ }
25479
+ }
25480
+ }
25481
+ /**
25482
+ * Evaluate row conditionals in a single table.
25483
+ * Processes conditionals from end to start to preserve row indices.
25484
+ */
25485
+ evaluateTableRowConditionalsInTable(table, data) {
25486
+ const conditionals = table.getAllRowConditionals();
25487
+ if (conditionals.length === 0)
25488
+ return;
25489
+ // Sort by startRowIndex descending (process end-to-start)
25490
+ const sorted = [...conditionals].sort((a, b) => b.startRowIndex - a.startRowIndex);
25491
+ for (const cond of sorted) {
25492
+ const result = PredicateEvaluator.evaluate(cond.predicate, data);
25493
+ if (!result) {
25494
+ // Predicate is false — remove the rows
25495
+ table.removeRowsInRange(cond.startRowIndex, cond.endRowIndex);
23816
25496
  }
23817
- // Remove the section after expansion
23818
- sectionManager.remove(section.id);
25497
+ // Remove the conditional marker regardless
25498
+ table.removeRowConditional(cond.id);
23819
25499
  }
25500
+ table.markLayoutDirty();
23820
25501
  }
23821
25502
  /**
23822
25503
  * Get a value at a path without array defaulting.
@@ -24065,7 +25746,7 @@ class PCEditor extends EventEmitter {
24065
25746
  toggleBulletList() {
24066
25747
  if (!this._isReady)
24067
25748
  return;
24068
- const flowingContent = this.getActiveFlowingContent();
25749
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24069
25750
  if (!flowingContent)
24070
25751
  return;
24071
25752
  flowingContent.toggleBulletList();
@@ -24078,7 +25759,7 @@ class PCEditor extends EventEmitter {
24078
25759
  toggleNumberedList() {
24079
25760
  if (!this._isReady)
24080
25761
  return;
24081
- const flowingContent = this.getActiveFlowingContent();
25762
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24082
25763
  if (!flowingContent)
24083
25764
  return;
24084
25765
  flowingContent.toggleNumberedList();
@@ -24091,7 +25772,7 @@ class PCEditor extends EventEmitter {
24091
25772
  indentParagraph() {
24092
25773
  if (!this._isReady)
24093
25774
  return;
24094
- const flowingContent = this.getActiveFlowingContent();
25775
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24095
25776
  if (!flowingContent)
24096
25777
  return;
24097
25778
  flowingContent.indentParagraph();
@@ -24104,7 +25785,7 @@ class PCEditor extends EventEmitter {
24104
25785
  outdentParagraph() {
24105
25786
  if (!this._isReady)
24106
25787
  return;
24107
- const flowingContent = this.getActiveFlowingContent();
25788
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24108
25789
  if (!flowingContent)
24109
25790
  return;
24110
25791
  flowingContent.outdentParagraph();
@@ -24117,7 +25798,7 @@ class PCEditor extends EventEmitter {
24117
25798
  getListFormatting() {
24118
25799
  if (!this._isReady)
24119
25800
  return undefined;
24120
- const flowingContent = this.getActiveFlowingContent();
25801
+ const flowingContent = this.getEditingFlowingContent() || this.getActiveFlowingContent();
24121
25802
  if (!flowingContent)
24122
25803
  return undefined;
24123
25804
  return flowingContent.getListFormatting();
@@ -24328,9 +26009,12 @@ class PCEditor extends EventEmitter {
24328
26009
  // If a table is focused, create a row loop instead of a text repeating section
24329
26010
  const focusedTable = this.getFocusedTable();
24330
26011
  if (focusedTable && focusedTable.focusedCell) {
24331
- Logger.log('[pc-editor] createRepeatingSection table row loop', fieldPath);
24332
- const row = focusedTable.focusedCell.row;
24333
- const loop = focusedTable.createRowLoop(row, row, fieldPath);
26012
+ // Use the selected range if multiple rows are selected, otherwise use the focused cell's row
26013
+ const selectedRange = focusedTable.selectedRange;
26014
+ const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
26015
+ const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
26016
+ Logger.log('[pc-editor] createRepeatingSection → table row loop', startRow, endRow, fieldPath);
26017
+ const loop = focusedTable.createRowLoop(startRow, endRow, fieldPath);
24334
26018
  if (loop) {
24335
26019
  this.canvasManager.render();
24336
26020
  this.emit('table-row-loop-added', { table: focusedTable, loop });
@@ -24402,6 +26086,103 @@ class PCEditor extends EventEmitter {
24402
26086
  return this.document.bodyFlowingContent.getRepeatingSectionAtBoundary(textIndex) || null;
24403
26087
  }
24404
26088
  // ============================================
26089
+ // Conditional Section API
26090
+ // ============================================
26091
+ /**
26092
+ * Create a conditional section.
26093
+ *
26094
+ * If a table is currently being edited (focused), creates a table row conditional
26095
+ * based on the focused cell's row.
26096
+ *
26097
+ * Otherwise, creates a body text conditional section at the given paragraph boundaries.
26098
+ *
26099
+ * @param startIndex Text index at paragraph start (ignored for table row conditionals)
26100
+ * @param endIndex Text index at closing paragraph start (ignored for table row conditionals)
26101
+ * @param predicate The predicate expression to evaluate (e.g., "isActive")
26102
+ * @returns The created section, or null if creation failed
26103
+ */
26104
+ addConditionalSection(startIndex, endIndex, predicate) {
26105
+ if (!this._isReady) {
26106
+ throw new Error('Editor is not ready');
26107
+ }
26108
+ // If a table is focused, create a row conditional instead
26109
+ const focusedTable = this.getFocusedTable();
26110
+ if (focusedTable && focusedTable.focusedCell) {
26111
+ const selectedRange = focusedTable.selectedRange;
26112
+ const startRow = selectedRange ? selectedRange.start.row : focusedTable.focusedCell.row;
26113
+ const endRow = selectedRange ? selectedRange.end.row : focusedTable.focusedCell.row;
26114
+ Logger.log('[pc-editor] addConditionalSection → table row conditional', startRow, endRow, predicate);
26115
+ const cond = focusedTable.createRowConditional(startRow, endRow, predicate);
26116
+ if (cond) {
26117
+ this.canvasManager.render();
26118
+ this.emit('table-row-conditional-added', { table: focusedTable, conditional: cond });
26119
+ }
26120
+ return null; // Row conditionals are not ConditionalSections, return null
26121
+ }
26122
+ Logger.log('[pc-editor] addConditionalSection', startIndex, endIndex, predicate);
26123
+ const section = this.document.bodyFlowingContent.createConditionalSection(startIndex, endIndex, predicate);
26124
+ if (section) {
26125
+ this.canvasManager.render();
26126
+ this.emit('conditional-section-added', { section });
26127
+ }
26128
+ return section;
26129
+ }
26130
+ /**
26131
+ * Get a conditional section by ID.
26132
+ */
26133
+ getConditionalSection(id) {
26134
+ if (!this._isReady) {
26135
+ return null;
26136
+ }
26137
+ return this.document.bodyFlowingContent.getConditionalSection(id) || null;
26138
+ }
26139
+ /**
26140
+ * Get all conditional sections.
26141
+ */
26142
+ getConditionalSections() {
26143
+ if (!this._isReady) {
26144
+ return [];
26145
+ }
26146
+ return this.document.bodyFlowingContent.getConditionalSections();
26147
+ }
26148
+ /**
26149
+ * Update a conditional section's predicate.
26150
+ */
26151
+ updateConditionalSectionPredicate(id, predicate) {
26152
+ if (!this._isReady) {
26153
+ return false;
26154
+ }
26155
+ const success = this.document.bodyFlowingContent.updateConditionalSectionPredicate(id, predicate);
26156
+ if (success) {
26157
+ this.canvasManager.render();
26158
+ this.emit('conditional-section-updated', { id, predicate });
26159
+ }
26160
+ return success;
26161
+ }
26162
+ /**
26163
+ * Remove a conditional section by ID.
26164
+ */
26165
+ removeConditionalSection(id) {
26166
+ if (!this._isReady) {
26167
+ return false;
26168
+ }
26169
+ const success = this.document.bodyFlowingContent.removeConditionalSection(id);
26170
+ if (success) {
26171
+ this.canvasManager.render();
26172
+ this.emit('conditional-section-removed', { id });
26173
+ }
26174
+ return success;
26175
+ }
26176
+ /**
26177
+ * Find a conditional section that has a boundary at the given text index.
26178
+ */
26179
+ getConditionalSectionAtBoundary(textIndex) {
26180
+ if (!this._isReady) {
26181
+ return null;
26182
+ }
26183
+ return this.document.bodyFlowingContent.getConditionalSectionAtBoundary(textIndex) || null;
26184
+ }
26185
+ // ============================================
24405
26186
  // Header/Footer API
24406
26187
  // ============================================
24407
26188
  /**
@@ -24752,6 +26533,39 @@ class PCEditor extends EventEmitter {
24752
26533
  setLogging(enabled) {
24753
26534
  Logger.setEnabled(enabled);
24754
26535
  }
26536
+ // ============================================
26537
+ // Font Management
26538
+ // ============================================
26539
+ /**
26540
+ * Register a custom font for use in the editor and PDF export.
26541
+ * The font will be loaded via the FontFace API for canvas rendering
26542
+ * and its raw bytes stored for PDF embedding.
26543
+ * @param options Font registration options (family + url or data)
26544
+ */
26545
+ async registerFont(options) {
26546
+ Logger.log('[pc-editor] registerFont', options.family);
26547
+ await this.fontManager.registerFont(options);
26548
+ this.emit('font-registered', { family: options.family });
26549
+ // Re-render to pick up the new font if it's already in use
26550
+ if (this._isReady) {
26551
+ this.canvasManager.render();
26552
+ }
26553
+ }
26554
+ /**
26555
+ * Get all registered fonts (built-in and custom).
26556
+ */
26557
+ getAvailableFonts() {
26558
+ return this.fontManager.getAvailableFonts().map(f => ({
26559
+ family: f.family,
26560
+ source: f.source
26561
+ }));
26562
+ }
26563
+ /**
26564
+ * Get all available font family names.
26565
+ */
26566
+ getAvailableFontFamilies() {
26567
+ return this.fontManager.getAvailableFontFamilies();
26568
+ }
24755
26569
  destroy() {
24756
26570
  this.disableTextInput();
24757
26571
  if (this.canvasManager) {
@@ -26268,7 +28082,7 @@ class MergeDataPane extends BasePane {
26268
28082
  createContent() {
26269
28083
  const container = document.createElement('div');
26270
28084
  // Textarea for JSON
26271
- const textareaGroup = this.createFormGroup('JSON Data', this.createTextarea());
28085
+ const textareaGroup = this.createFormGroup('JSON Data:', this.createTextarea());
26272
28086
  container.appendChild(textareaGroup);
26273
28087
  // Error hint (hidden by default)
26274
28088
  this.errorHint = this.createHint('');
@@ -26414,17 +28228,29 @@ class FormattingPane extends BasePane {
26414
28228
  attach(options) {
26415
28229
  super.attach(options);
26416
28230
  if (this.editor) {
28231
+ // Populate font list from editor if no explicit list was provided
28232
+ if (this.fontFamilies === DEFAULT_FONT_FAMILIES) {
28233
+ this.fontFamilies = this.editor.getAvailableFontFamilies();
28234
+ this.rebuildFontSelect();
28235
+ }
26417
28236
  // Update on cursor/selection changes
26418
28237
  const updateHandler = () => this.updateFromEditor();
26419
28238
  this.editor.on('cursor-changed', updateHandler);
26420
28239
  this.editor.on('selection-changed', updateHandler);
26421
28240
  this.editor.on('text-changed', updateHandler);
26422
28241
  this.editor.on('formatting-changed', updateHandler);
28242
+ // Update font list when new fonts are registered
28243
+ const fontHandler = () => {
28244
+ this.fontFamilies = this.editor.getAvailableFontFamilies();
28245
+ this.rebuildFontSelect();
28246
+ };
28247
+ this.editor.on('font-registered', fontHandler);
26423
28248
  this.eventCleanup.push(() => {
26424
28249
  this.editor?.off('cursor-changed', updateHandler);
26425
28250
  this.editor?.off('selection-changed', updateHandler);
26426
28251
  this.editor?.off('text-changed', updateHandler);
26427
28252
  this.editor?.off('formatting-changed', updateHandler);
28253
+ this.editor?.off('font-registered', fontHandler);
26428
28254
  });
26429
28255
  // Initial update
26430
28256
  this.updateFromEditor();
@@ -26493,38 +28319,82 @@ class FormattingPane extends BasePane {
26493
28319
  listsGroup.appendChild(this.outdentBtn);
26494
28320
  listsSection.appendChild(listsGroup);
26495
28321
  container.appendChild(listsSection);
26496
- // Font section
28322
+ // Font section - label-value grid with right-aligned labels
26497
28323
  const fontSection = this.createSection('Font');
28324
+ const fontGrid = document.createElement('div');
28325
+ fontGrid.className = 'pc-pane-label-value-grid';
28326
+ // Family row
28327
+ const familyLabel = document.createElement('label');
28328
+ familyLabel.className = 'pc-pane-label pc-pane-margin-label';
28329
+ familyLabel.textContent = 'Family:';
26498
28330
  this.fontFamilySelect = this.createSelect(this.fontFamilies.map(f => ({ value: f, label: f })), 'Arial');
26499
28331
  this.addImmediateApplyListener(this.fontFamilySelect, () => this.applyFontFamily());
26500
- fontSection.appendChild(this.createFormGroup('Family', this.fontFamilySelect));
28332
+ fontGrid.appendChild(familyLabel);
28333
+ fontGrid.appendChild(this.fontFamilySelect);
28334
+ fontGrid.appendChild(document.createElement('div'));
28335
+ // Size row
28336
+ const sizeLabel = document.createElement('label');
28337
+ sizeLabel.className = 'pc-pane-label pc-pane-margin-label';
28338
+ sizeLabel.textContent = 'Size:';
26501
28339
  this.fontSizeSelect = this.createSelect(this.fontSizes.map(s => ({ value: s.toString(), label: s.toString() })), '14');
26502
28340
  this.addImmediateApplyListener(this.fontSizeSelect, () => this.applyFontSize());
26503
- fontSection.appendChild(this.createFormGroup('Size', this.fontSizeSelect));
28341
+ fontGrid.appendChild(sizeLabel);
28342
+ fontGrid.appendChild(this.fontSizeSelect);
28343
+ fontGrid.appendChild(document.createElement('div'));
28344
+ fontSection.appendChild(fontGrid);
26504
28345
  container.appendChild(fontSection);
26505
- // Color section
28346
+ // Color section - label-value grid with right-aligned labels
26506
28347
  const colorSection = this.createSection('Color');
26507
- const colorRow = this.createRow();
26508
- const colorGroup = document.createElement('div');
28348
+ const colorGrid = document.createElement('div');
28349
+ colorGrid.className = 'pc-pane-label-value-grid';
28350
+ // Text color row: label | picker | spacer
28351
+ const textColorLabel = document.createElement('label');
28352
+ textColorLabel.className = 'pc-pane-label pc-pane-margin-label';
28353
+ textColorLabel.textContent = 'Text:';
26509
28354
  this.colorInput = this.createColorInput('#000000');
26510
28355
  this.addImmediateApplyListener(this.colorInput, () => this.applyTextColor());
26511
- colorGroup.appendChild(this.createFormGroup('Text', this.colorInput));
26512
- colorRow.appendChild(colorGroup);
26513
- const highlightGroup = document.createElement('div');
28356
+ colorGrid.appendChild(textColorLabel);
28357
+ colorGrid.appendChild(this.colorInput);
28358
+ colorGrid.appendChild(document.createElement('div'));
28359
+ // Highlight row: label | picker + clear button | spacer
28360
+ const highlightLabel = document.createElement('label');
28361
+ highlightLabel.className = 'pc-pane-label pc-pane-margin-label';
28362
+ highlightLabel.textContent = 'Highlight:';
26514
28363
  this.highlightInput = this.createColorInput('#ffff00');
26515
28364
  this.addImmediateApplyListener(this.highlightInput, () => this.applyHighlight());
26516
- const highlightForm = this.createFormGroup('Highlight', this.highlightInput);
28365
+ const highlightControls = document.createElement('div');
28366
+ highlightControls.style.display = 'flex';
28367
+ highlightControls.style.alignItems = 'center';
28368
+ highlightControls.style.gap = '4px';
28369
+ highlightControls.appendChild(this.highlightInput);
26517
28370
  const clearHighlightBtn = this.createButton('Clear');
26518
28371
  clearHighlightBtn.className = 'pc-pane-button';
26519
- clearHighlightBtn.style.marginLeft = '4px';
26520
28372
  this.addButtonListener(clearHighlightBtn, () => this.clearHighlight());
26521
- highlightForm.appendChild(clearHighlightBtn);
26522
- highlightGroup.appendChild(highlightForm);
26523
- colorRow.appendChild(highlightGroup);
26524
- colorSection.appendChild(colorRow);
28373
+ highlightControls.appendChild(clearHighlightBtn);
28374
+ colorGrid.appendChild(highlightLabel);
28375
+ colorGrid.appendChild(highlightControls);
28376
+ colorGrid.appendChild(document.createElement('div'));
28377
+ colorSection.appendChild(colorGrid);
26525
28378
  container.appendChild(colorSection);
26526
28379
  return container;
26527
28380
  }
28381
+ rebuildFontSelect() {
28382
+ if (!this.fontFamilySelect)
28383
+ return;
28384
+ const currentValue = this.fontFamilySelect.value;
28385
+ this.fontFamilySelect.innerHTML = '';
28386
+ for (const family of this.fontFamilies) {
28387
+ const option = document.createElement('option');
28388
+ option.value = family;
28389
+ option.textContent = family;
28390
+ option.style.fontFamily = family;
28391
+ this.fontFamilySelect.appendChild(option);
28392
+ }
28393
+ // Restore selection if the font still exists
28394
+ if (this.fontFamilies.includes(currentValue)) {
28395
+ this.fontFamilySelect.value = currentValue;
28396
+ }
28397
+ }
26528
28398
  updateFromEditor() {
26529
28399
  if (!this.editor)
26530
28400
  return;
@@ -26748,10 +28618,10 @@ class HyperlinkPane extends BasePane {
26748
28618
  const container = document.createElement('div');
26749
28619
  // URL input
26750
28620
  this.urlInput = this.createTextInput({ placeholder: 'https://example.com' });
26751
- container.appendChild(this.createFormGroup('URL', this.urlInput));
28621
+ container.appendChild(this.createFormGroup('URL:', this.urlInput));
26752
28622
  // Title input
26753
28623
  this.titleInput = this.createTextInput({ placeholder: 'Link title (optional)' });
26754
- container.appendChild(this.createFormGroup('Title', this.titleInput));
28624
+ container.appendChild(this.createFormGroup('Title:', this.titleInput));
26755
28625
  // Apply button
26756
28626
  const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
26757
28627
  this.addButtonListener(applyBtn, () => this.applyChanges());
@@ -26902,10 +28772,10 @@ class SubstitutionFieldPane extends BasePane {
26902
28772
  const container = document.createElement('div');
26903
28773
  // Field name input
26904
28774
  this.fieldNameInput = this.createTextInput({ placeholder: 'Field name' });
26905
- container.appendChild(this.createFormGroup('Field Name', this.fieldNameInput));
28775
+ container.appendChild(this.createFormGroup('Field Name:', this.fieldNameInput));
26906
28776
  // Default value input
26907
28777
  this.fieldDefaultInput = this.createTextInput({ placeholder: 'Default value (optional)' });
26908
- container.appendChild(this.createFormGroup('Default Value', this.fieldDefaultInput));
28778
+ container.appendChild(this.createFormGroup('Default Value:', this.fieldDefaultInput));
26909
28779
  // Value type select
26910
28780
  this.valueTypeSelect = this.createSelect([
26911
28781
  { value: '', label: '(None)' },
@@ -26914,7 +28784,7 @@ class SubstitutionFieldPane extends BasePane {
26914
28784
  { value: 'date', label: 'Date' }
26915
28785
  ]);
26916
28786
  this.addImmediateApplyListener(this.valueTypeSelect, () => this.updateFormatGroups());
26917
- container.appendChild(this.createFormGroup('Value Type', this.valueTypeSelect));
28787
+ container.appendChild(this.createFormGroup('Value Type:', this.valueTypeSelect));
26918
28788
  // Number format group
26919
28789
  this.numberFormatGroup = this.createSection();
26920
28790
  this.numberFormatGroup.style.display = 'none';
@@ -26924,7 +28794,7 @@ class SubstitutionFieldPane extends BasePane {
26924
28794
  { value: '0,0', label: 'Thousands separator (0,0)' },
26925
28795
  { value: '0,0.00', label: 'Thousands + decimals (0,0.00)' }
26926
28796
  ]);
26927
- this.numberFormatGroup.appendChild(this.createFormGroup('Number Format', this.numberFormatSelect));
28797
+ this.numberFormatGroup.appendChild(this.createFormGroup('Number Format:', this.numberFormatSelect));
26928
28798
  container.appendChild(this.numberFormatGroup);
26929
28799
  // Currency format group
26930
28800
  this.currencyFormatGroup = this.createSection();
@@ -26935,7 +28805,7 @@ class SubstitutionFieldPane extends BasePane {
26935
28805
  { value: 'GBP', label: 'GBP' },
26936
28806
  { value: 'JPY', label: 'JPY' }
26937
28807
  ]);
26938
- this.currencyFormatGroup.appendChild(this.createFormGroup('Currency', this.currencyFormatSelect));
28808
+ this.currencyFormatGroup.appendChild(this.createFormGroup('Currency:', this.currencyFormatSelect));
26939
28809
  container.appendChild(this.currencyFormatGroup);
26940
28810
  // Date format group
26941
28811
  this.dateFormatGroup = this.createSection();
@@ -26946,7 +28816,7 @@ class SubstitutionFieldPane extends BasePane {
26946
28816
  { value: 'DD/MM/YYYY', label: '01/01/2026 (EU)' },
26947
28817
  { value: 'YYYY-MM-DD', label: '2026-01-01 (ISO)' }
26948
28818
  ]);
26949
- this.dateFormatGroup.appendChild(this.createFormGroup('Date Format', this.dateFormatSelect));
28819
+ this.dateFormatGroup.appendChild(this.createFormGroup('Date Format:', this.dateFormatSelect));
26950
28820
  container.appendChild(this.dateFormatGroup);
26951
28821
  // Apply button
26952
28822
  const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
@@ -27108,12 +28978,17 @@ class RepeatingSectionPane extends BasePane {
27108
28978
  if (this.editor) {
27109
28979
  // Listen for repeating section selection
27110
28980
  const selectionHandler = (event) => {
27111
- if (event.type === 'repeating-section' && event.sectionId) {
27112
- const section = this.editor?.getRepeatingSection(event.sectionId);
28981
+ const sel = event.selection || event;
28982
+ if (sel.type === 'repeating-section' && sel.sectionId) {
28983
+ const section = this.editor?.getRepeatingSection(sel.sectionId);
27113
28984
  if (section) {
27114
28985
  this.showSection(section);
27115
28986
  }
27116
28987
  }
28988
+ else {
28989
+ // Selection changed away from repeating section — hide pane
28990
+ this.hideSection();
28991
+ }
27117
28992
  };
27118
28993
  const removedHandler = () => {
27119
28994
  this.hideSection();
@@ -27130,7 +29005,7 @@ class RepeatingSectionPane extends BasePane {
27130
29005
  const container = document.createElement('div');
27131
29006
  // Field path input
27132
29007
  this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27133
- container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
29008
+ container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
27134
29009
  hint: 'Path to array in merge data (e.g., "items" or "contact.addresses")'
27135
29010
  }));
27136
29011
  // Apply button
@@ -27230,6 +29105,158 @@ class RepeatingSectionPane extends BasePane {
27230
29105
  }
27231
29106
  }
27232
29107
 
29108
+ /**
29109
+ * ConditionalSectionPane - Edit conditional section properties.
29110
+ *
29111
+ * Shows:
29112
+ * - Predicate input (boolean expression in merge data)
29113
+ * - Position information
29114
+ *
29115
+ * Uses the PCEditor public API:
29116
+ * - editor.getConditionalSection()
29117
+ * - editor.updateConditionalSectionPredicate()
29118
+ * - editor.removeConditionalSection()
29119
+ */
29120
+ class ConditionalSectionPane extends BasePane {
29121
+ constructor(id = 'conditional-section', options = {}) {
29122
+ super(id, { className: 'pc-pane-conditional-section', ...options });
29123
+ this.predicateInput = null;
29124
+ this.positionHint = null;
29125
+ this.currentSection = null;
29126
+ this.onApplyCallback = options.onApply;
29127
+ this.onRemoveCallback = options.onRemove;
29128
+ }
29129
+ attach(options) {
29130
+ super.attach(options);
29131
+ if (this.editor) {
29132
+ // Listen for conditional section selection
29133
+ const selectionHandler = (event) => {
29134
+ const sel = event.selection || event;
29135
+ if (sel.type === 'conditional-section' && sel.sectionId) {
29136
+ const section = this.editor?.getConditionalSection(sel.sectionId);
29137
+ if (section) {
29138
+ this.showSection(section);
29139
+ }
29140
+ }
29141
+ else {
29142
+ // Selection changed away from conditional section — hide pane
29143
+ this.hideSection();
29144
+ }
29145
+ };
29146
+ const removedHandler = () => {
29147
+ this.hideSection();
29148
+ };
29149
+ this.editor.on('selection-change', selectionHandler);
29150
+ this.editor.on('conditional-section-removed', removedHandler);
29151
+ this.eventCleanup.push(() => {
29152
+ this.editor?.off('selection-change', selectionHandler);
29153
+ this.editor?.off('conditional-section-removed', removedHandler);
29154
+ });
29155
+ }
29156
+ }
29157
+ createContent() {
29158
+ const container = document.createElement('div');
29159
+ // Predicate input
29160
+ this.predicateInput = this.createTextInput({ placeholder: 'isActive' });
29161
+ container.appendChild(this.createFormGroup('Condition:', this.predicateInput, {
29162
+ hint: 'Boolean expression evaluated against merge data (e.g., "isActive", "count > 0")'
29163
+ }));
29164
+ // Apply button
29165
+ const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
29166
+ this.addButtonListener(applyBtn, () => this.applyChanges());
29167
+ container.appendChild(applyBtn);
29168
+ // Remove button
29169
+ const removeBtn = this.createButton('Remove Condition', { variant: 'danger' });
29170
+ removeBtn.style.marginTop = '0.5rem';
29171
+ this.addButtonListener(removeBtn, () => this.removeSection());
29172
+ container.appendChild(removeBtn);
29173
+ // Position hint
29174
+ this.positionHint = this.createHint('');
29175
+ container.appendChild(this.positionHint);
29176
+ return container;
29177
+ }
29178
+ /**
29179
+ * Show the pane with the given section.
29180
+ */
29181
+ showSection(section) {
29182
+ this.currentSection = section;
29183
+ if (this.predicateInput) {
29184
+ this.predicateInput.value = section.predicate;
29185
+ }
29186
+ if (this.positionHint) {
29187
+ this.positionHint.textContent = `Condition from position ${section.startIndex} to ${section.endIndex}`;
29188
+ }
29189
+ this.show();
29190
+ }
29191
+ /**
29192
+ * Hide the pane and clear the current section.
29193
+ */
29194
+ hideSection() {
29195
+ this.currentSection = null;
29196
+ this.hide();
29197
+ }
29198
+ applyChanges() {
29199
+ if (!this.editor || !this.currentSection) {
29200
+ this.onApplyCallback?.(false, new Error('No section selected'));
29201
+ return;
29202
+ }
29203
+ const predicate = this.predicateInput?.value.trim();
29204
+ if (!predicate) {
29205
+ this.onApplyCallback?.(false, new Error('Predicate cannot be empty'));
29206
+ return;
29207
+ }
29208
+ if (predicate === this.currentSection.predicate) {
29209
+ return; // No changes
29210
+ }
29211
+ try {
29212
+ const success = this.editor.updateConditionalSectionPredicate(this.currentSection.id, predicate);
29213
+ if (success) {
29214
+ this.currentSection = this.editor.getConditionalSection(this.currentSection.id) || null;
29215
+ if (this.currentSection) {
29216
+ this.showSection(this.currentSection);
29217
+ }
29218
+ this.onApplyCallback?.(true);
29219
+ }
29220
+ else {
29221
+ this.onApplyCallback?.(false, new Error('Failed to update section'));
29222
+ }
29223
+ }
29224
+ catch (error) {
29225
+ this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
29226
+ }
29227
+ }
29228
+ removeSection() {
29229
+ if (!this.editor || !this.currentSection)
29230
+ return;
29231
+ try {
29232
+ this.editor.removeConditionalSection(this.currentSection.id);
29233
+ this.hideSection();
29234
+ this.onRemoveCallback?.(true);
29235
+ }
29236
+ catch {
29237
+ this.onRemoveCallback?.(false);
29238
+ }
29239
+ }
29240
+ /**
29241
+ * Get the currently selected section.
29242
+ */
29243
+ getCurrentSection() {
29244
+ return this.currentSection;
29245
+ }
29246
+ /**
29247
+ * Check if a section is currently selected.
29248
+ */
29249
+ hasSection() {
29250
+ return this.currentSection !== null;
29251
+ }
29252
+ /**
29253
+ * Update the pane from current editor state.
29254
+ */
29255
+ update() {
29256
+ // Section pane doesn't auto-update - it's driven by selection events
29257
+ }
29258
+ }
29259
+
27233
29260
  /**
27234
29261
  * TableRowLoopPane - Edit table row loop properties.
27235
29262
  *
@@ -27254,14 +29281,28 @@ class TableRowLoopPane extends BasePane {
27254
29281
  }
27255
29282
  attach(options) {
27256
29283
  super.attach(options);
27257
- // Table row loop pane is typically shown manually when a table's row loop is selected
27258
- // The consumer is responsible for calling showLoop() with the table and loop
29284
+ if (this.editor) {
29285
+ // Auto-show when a table row loop is clicked
29286
+ const loopClickHandler = (data) => {
29287
+ this.showLoop(data.table, data.loop);
29288
+ };
29289
+ // Hide when selection changes away from a loop
29290
+ const selectionHandler = () => {
29291
+ this.hideLoop();
29292
+ };
29293
+ this.editor.on('table-row-loop-clicked', loopClickHandler);
29294
+ this.editor.on('selection-change', selectionHandler);
29295
+ this.eventCleanup.push(() => {
29296
+ this.editor?.off('table-row-loop-clicked', loopClickHandler);
29297
+ this.editor?.off('selection-change', selectionHandler);
29298
+ });
29299
+ }
27259
29300
  }
27260
29301
  createContent() {
27261
29302
  const container = document.createElement('div');
27262
29303
  // Field path input
27263
29304
  this.fieldPathInput = this.createTextInput({ placeholder: 'items' });
27264
- container.appendChild(this.createFormGroup('Array Field Path', this.fieldPathInput, {
29305
+ container.appendChild(this.createFormGroup('Array Field Path:', this.fieldPathInput, {
27265
29306
  hint: 'Path to array in merge data (e.g., "items" or "orders")'
27266
29307
  }));
27267
29308
  // Apply button
@@ -27421,56 +29462,63 @@ class TextBoxPane extends BasePane {
27421
29462
  }
27422
29463
  createContent() {
27423
29464
  const container = document.createElement('div');
27424
- // Position section
29465
+ // Position section - Type on same row as label
27425
29466
  const positionSection = this.createSection('Position');
27426
29467
  this.positionSelect = this.createSelect([
27427
29468
  { value: 'inline', label: 'Inline' },
27428
29469
  { value: 'block', label: 'Block' },
27429
29470
  { value: 'relative', label: 'Relative' }
27430
29471
  ], 'inline');
27431
- this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27432
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
29472
+ this.addImmediateApplyListener(this.positionSelect, () => {
29473
+ this.updateOffsetVisibility();
29474
+ this.applyChanges();
29475
+ });
29476
+ positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
27433
29477
  // Offset group (only visible for relative positioning)
27434
29478
  this.offsetGroup = document.createElement('div');
27435
29479
  this.offsetGroup.style.display = 'none';
27436
29480
  const offsetRow = this.createRow();
27437
29481
  this.offsetXInput = this.createNumberInput({ value: 0 });
27438
29482
  this.offsetYInput = this.createNumberInput({ value: 0 });
27439
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27440
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
29483
+ this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
29484
+ this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
29485
+ offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
29486
+ offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
27441
29487
  this.offsetGroup.appendChild(offsetRow);
27442
29488
  positionSection.appendChild(this.offsetGroup);
27443
29489
  container.appendChild(positionSection);
27444
- // Background section
27445
- const bgSection = this.createSection('Background');
29490
+ // Background - color on same row as label
29491
+ const bgSection = this.createSection();
27446
29492
  this.bgColorInput = this.createColorInput('#ffffff');
27447
- bgSection.appendChild(this.createFormGroup('Color', this.bgColorInput));
29493
+ this.addImmediateApplyListener(this.bgColorInput, () => this.applyChanges());
29494
+ bgSection.appendChild(this.createFormGroup('Background:', this.bgColorInput, { inline: true }));
27448
29495
  container.appendChild(bgSection);
27449
29496
  // Border section
27450
29497
  const borderSection = this.createSection('Border');
27451
29498
  const borderRow = this.createRow();
27452
29499
  this.borderWidthInput = this.createNumberInput({ min: 0, max: 10, value: 1 });
27453
29500
  this.borderColorInput = this.createColorInput('#cccccc');
27454
- borderRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
27455
- borderRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
29501
+ this.addImmediateApplyListener(this.borderWidthInput, () => this.applyChanges());
29502
+ this.addImmediateApplyListener(this.borderColorInput, () => this.applyChanges());
29503
+ borderRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
29504
+ borderRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
27456
29505
  borderSection.appendChild(borderRow);
29506
+ // Border style on same row as label
27457
29507
  this.borderStyleSelect = this.createSelect([
27458
29508
  { value: 'solid', label: 'Solid' },
27459
29509
  { value: 'dashed', label: 'Dashed' },
27460
29510
  { value: 'dotted', label: 'Dotted' },
27461
29511
  { value: 'none', label: 'None' }
27462
29512
  ], 'solid');
27463
- borderSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
29513
+ this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyChanges());
29514
+ borderSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
27464
29515
  container.appendChild(borderSection);
27465
- // Padding section
27466
- const paddingSection = this.createSection('Padding');
29516
+ // Padding on same row as label
29517
+ const paddingSection = this.createSection();
27467
29518
  this.paddingInput = this.createNumberInput({ min: 0, max: 50, value: 8 });
27468
- paddingSection.appendChild(this.createFormGroup('All sides (px)', this.paddingInput));
29519
+ this.addImmediateApplyListener(this.paddingInput, () => this.applyChanges());
29520
+ paddingSection.appendChild(this.createFormGroup('Padding:', this.paddingInput, { inline: true }));
27469
29521
  container.appendChild(paddingSection);
27470
- // Apply button
27471
- const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27472
- this.addButtonListener(applyBtn, () => this.applyChanges());
27473
- container.appendChild(applyBtn);
27474
29522
  return container;
27475
29523
  }
27476
29524
  updateFromSelection() {
@@ -27546,7 +29594,6 @@ class TextBoxPane extends BasePane {
27546
29594
  }
27547
29595
  applyChanges() {
27548
29596
  if (!this.editor || !this.currentTextBox) {
27549
- this.onApplyCallback?.(false, new Error('No text box selected'));
27550
29597
  return;
27551
29598
  }
27552
29599
  const updates = {};
@@ -27582,12 +29629,7 @@ class TextBoxPane extends BasePane {
27582
29629
  }
27583
29630
  try {
27584
29631
  const success = this.editor.updateTextBox(this.currentTextBox.id, updates);
27585
- if (success) {
27586
- this.onApplyCallback?.(true);
27587
- }
27588
- else {
27589
- this.onApplyCallback?.(false, new Error('Failed to update text box'));
27590
- }
29632
+ this.onApplyCallback?.(success);
27591
29633
  }
27592
29634
  catch (error) {
27593
29635
  this.onApplyCallback?.(false, error instanceof Error ? error : new Error(String(error)));
@@ -27663,28 +29705,35 @@ class ImagePane extends BasePane {
27663
29705
  }
27664
29706
  createContent() {
27665
29707
  const container = document.createElement('div');
27666
- // Position section
29708
+ // Position section — with heading, matching TextBoxPane
27667
29709
  const positionSection = this.createSection('Position');
27668
29710
  this.positionSelect = this.createSelect([
27669
29711
  { value: 'inline', label: 'Inline' },
27670
29712
  { value: 'block', label: 'Block' },
27671
29713
  { value: 'relative', label: 'Relative' }
27672
29714
  ], 'inline');
27673
- this.addImmediateApplyListener(this.positionSelect, () => this.updateOffsetVisibility());
27674
- positionSection.appendChild(this.createFormGroup('Type', this.positionSelect));
29715
+ this.addImmediateApplyListener(this.positionSelect, () => {
29716
+ this.updateOffsetVisibility();
29717
+ this.applyChanges();
29718
+ });
29719
+ positionSection.appendChild(this.createFormGroup('Type:', this.positionSelect, { inline: true }));
27675
29720
  // Offset group (only visible for relative positioning)
27676
29721
  this.offsetGroup = document.createElement('div');
27677
29722
  this.offsetGroup.style.display = 'none';
27678
29723
  const offsetRow = this.createRow();
27679
29724
  this.offsetXInput = this.createNumberInput({ value: 0 });
27680
29725
  this.offsetYInput = this.createNumberInput({ value: 0 });
27681
- offsetRow.appendChild(this.createFormGroup('X', this.offsetXInput, { inline: true }));
27682
- offsetRow.appendChild(this.createFormGroup('Y', this.offsetYInput, { inline: true }));
29726
+ this.addImmediateApplyListener(this.offsetXInput, () => this.applyChanges());
29727
+ this.addImmediateApplyListener(this.offsetYInput, () => this.applyChanges());
29728
+ offsetRow.appendChild(this.createFormGroup('X:', this.offsetXInput, { inline: true }));
29729
+ offsetRow.appendChild(this.createFormGroup('Y:', this.offsetYInput, { inline: true }));
27683
29730
  this.offsetGroup.appendChild(offsetRow);
27684
29731
  positionSection.appendChild(this.offsetGroup);
27685
29732
  container.appendChild(positionSection);
27686
- // Fit mode section
27687
- const fitSection = this.createSection('Display');
29733
+ container.appendChild(document.createElement('hr'));
29734
+ // Display section — Fit Mode and Resize Mode with aligned labels
29735
+ const displaySection = document.createElement('div');
29736
+ displaySection.className = 'pc-pane-image-display';
27688
29737
  this.fitModeSelect = this.createSelect([
27689
29738
  { value: 'contain', label: 'Contain' },
27690
29739
  { value: 'cover', label: 'Cover' },
@@ -27692,34 +29741,31 @@ class ImagePane extends BasePane {
27692
29741
  { value: 'none', label: 'None (original size)' },
27693
29742
  { value: 'tile', label: 'Tile' }
27694
29743
  ], 'contain');
27695
- fitSection.appendChild(this.createFormGroup('Fit Mode', this.fitModeSelect));
29744
+ this.addImmediateApplyListener(this.fitModeSelect, () => this.applyChanges());
29745
+ displaySection.appendChild(this.createFormGroup('Fit Mode:', this.fitModeSelect, { inline: true }));
27696
29746
  this.resizeModeSelect = this.createSelect([
27697
29747
  { value: 'locked-aspect-ratio', label: 'Lock Aspect Ratio' },
27698
29748
  { value: 'free', label: 'Free Resize' }
27699
29749
  ], 'locked-aspect-ratio');
27700
- fitSection.appendChild(this.createFormGroup('Resize Mode', this.resizeModeSelect));
27701
- container.appendChild(fitSection);
27702
- // Alt text section
27703
- const altSection = this.createSection('Accessibility');
29750
+ this.addImmediateApplyListener(this.resizeModeSelect, () => this.applyChanges());
29751
+ displaySection.appendChild(this.createFormGroup('Resize Mode:', this.resizeModeSelect, { inline: true }));
29752
+ container.appendChild(displaySection);
29753
+ container.appendChild(document.createElement('hr'));
29754
+ // Alt Text — inline row
27704
29755
  this.altTextInput = this.createTextInput({ placeholder: 'Description of the image' });
27705
- altSection.appendChild(this.createFormGroup('Alt Text', this.altTextInput));
27706
- container.appendChild(altSection);
27707
- // Source section
27708
- const sourceSection = this.createSection('Source');
29756
+ this.addImmediateApplyListener(this.altTextInput, () => this.applyChanges());
29757
+ container.appendChild(this.createFormGroup('Alt Text:', this.altTextInput, { inline: true }));
29758
+ container.appendChild(document.createElement('hr'));
29759
+ // Source change image button
27709
29760
  this.fileInput = document.createElement('input');
27710
29761
  this.fileInput.type = 'file';
27711
29762
  this.fileInput.accept = 'image/*';
27712
29763
  this.fileInput.style.display = 'none';
27713
29764
  this.fileInput.addEventListener('change', (e) => this.handleFileChange(e));
27714
- sourceSection.appendChild(this.fileInput);
29765
+ container.appendChild(this.fileInput);
27715
29766
  const changeSourceBtn = this.createButton('Change Image...');
27716
29767
  this.addButtonListener(changeSourceBtn, () => this.fileInput?.click());
27717
- sourceSection.appendChild(changeSourceBtn);
27718
- container.appendChild(sourceSection);
27719
- // Apply button
27720
- const applyBtn = this.createButton('Apply Changes', { variant: 'primary' });
27721
- this.addButtonListener(applyBtn, () => this.applyChanges());
27722
- container.appendChild(applyBtn);
29768
+ container.appendChild(changeSourceBtn);
27723
29769
  return container;
27724
29770
  }
27725
29771
  updateFromSelection() {
@@ -27938,60 +29984,57 @@ class TablePane extends BasePane {
27938
29984
  const container = document.createElement('div');
27939
29985
  // Structure section
27940
29986
  const structureSection = this.createSection('Structure');
29987
+ // Rows/Columns info with aligned labels
27941
29988
  const structureInfo = document.createElement('div');
27942
- structureInfo.className = 'pc-pane-info-list';
29989
+ structureInfo.className = 'pc-pane-table-structure-info';
27943
29990
  this.rowCountDisplay = document.createElement('span');
29991
+ this.rowCountDisplay.className = 'pc-pane-info-value';
27944
29992
  this.colCountDisplay = document.createElement('span');
27945
- const rowInfo = document.createElement('div');
27946
- rowInfo.className = 'pc-pane-info';
27947
- rowInfo.innerHTML = '<span class="pc-pane-info-label">Rows</span>';
27948
- rowInfo.appendChild(this.rowCountDisplay);
27949
- const colInfo = document.createElement('div');
27950
- colInfo.className = 'pc-pane-info';
27951
- colInfo.innerHTML = '<span class="pc-pane-info-label">Columns</span>';
27952
- colInfo.appendChild(this.colCountDisplay);
27953
- structureInfo.appendChild(rowInfo);
27954
- structureInfo.appendChild(colInfo);
29993
+ this.colCountDisplay.className = 'pc-pane-info-value';
29994
+ structureInfo.appendChild(this.createFormGroup('Rows:', this.rowCountDisplay, { inline: true }));
29995
+ structureInfo.appendChild(this.createFormGroup('Columns:', this.colCountDisplay, { inline: true }));
27955
29996
  structureSection.appendChild(structureInfo);
27956
- // Row/column buttons
27957
- const structureBtns = this.createButtonGroup();
29997
+ // Row buttons
29998
+ const rowBtns = this.createButtonGroup();
27958
29999
  const addRowBtn = this.createButton('+ Row');
27959
30000
  this.addButtonListener(addRowBtn, () => this.insertRow());
27960
30001
  const removeRowBtn = this.createButton('- Row');
27961
30002
  this.addButtonListener(removeRowBtn, () => this.removeRow());
30003
+ rowBtns.appendChild(addRowBtn);
30004
+ rowBtns.appendChild(removeRowBtn);
30005
+ structureSection.appendChild(rowBtns);
30006
+ // Column buttons (separate row)
30007
+ const colBtns = this.createButtonGroup();
27962
30008
  const addColBtn = this.createButton('+ Column');
27963
30009
  this.addButtonListener(addColBtn, () => this.insertColumn());
27964
30010
  const removeColBtn = this.createButton('- Column');
27965
30011
  this.addButtonListener(removeColBtn, () => this.removeColumn());
27966
- structureBtns.appendChild(addRowBtn);
27967
- structureBtns.appendChild(removeRowBtn);
27968
- structureBtns.appendChild(addColBtn);
27969
- structureBtns.appendChild(removeColBtn);
27970
- structureSection.appendChild(structureBtns);
27971
- container.appendChild(structureSection);
27972
- // Headers section
27973
- const headersSection = this.createSection('Headers');
27974
- const headerRow = this.createRow();
30012
+ colBtns.appendChild(addColBtn);
30013
+ colBtns.appendChild(removeColBtn);
30014
+ structureSection.appendChild(colBtns);
30015
+ // Header rows/cols (with separator and aligned labels)
30016
+ structureSection.appendChild(document.createElement('hr'));
30017
+ const headersGroup = document.createElement('div');
30018
+ headersGroup.className = 'pc-pane-table-headers';
27975
30019
  this.headerRowInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
30020
+ this.addImmediateApplyListener(this.headerRowInput, () => this.applyHeaders());
30021
+ headersGroup.appendChild(this.createFormGroup('Header Rows:', this.headerRowInput, { inline: true }));
27976
30022
  this.headerColInput = this.createNumberInput({ min: 0, max: 10, value: 0 });
27977
- headerRow.appendChild(this.createFormGroup('Header Rows', this.headerRowInput, { inline: true }));
27978
- headerRow.appendChild(this.createFormGroup('Header Cols', this.headerColInput, { inline: true }));
27979
- headersSection.appendChild(headerRow);
27980
- const applyHeadersBtn = this.createButton('Apply Headers');
27981
- this.addButtonListener(applyHeadersBtn, () => this.applyHeaders());
27982
- headersSection.appendChild(applyHeadersBtn);
27983
- container.appendChild(headersSection);
27984
- // Defaults section
30023
+ this.addImmediateApplyListener(this.headerColInput, () => this.applyHeaders());
30024
+ headersGroup.appendChild(this.createFormGroup('Header Cols:', this.headerColInput, { inline: true }));
30025
+ structureSection.appendChild(headersGroup);
30026
+ container.appendChild(structureSection);
30027
+ // Defaults section (aligned labels)
27985
30028
  const defaultsSection = this.createSection('Defaults');
27986
- const defaultsRow = this.createRow();
30029
+ const defaultsGroup = document.createElement('div');
30030
+ defaultsGroup.className = 'pc-pane-table-defaults';
27987
30031
  this.defaultPaddingInput = this.createNumberInput({ min: 0, max: 20, value: 8 });
30032
+ this.addImmediateApplyListener(this.defaultPaddingInput, () => this.applyDefaults());
30033
+ defaultsGroup.appendChild(this.createFormGroup('Padding:', this.defaultPaddingInput, { inline: true }));
27988
30034
  this.defaultBorderColorInput = this.createColorInput('#cccccc');
27989
- defaultsRow.appendChild(this.createFormGroup('Padding', this.defaultPaddingInput, { inline: true }));
27990
- defaultsRow.appendChild(this.createFormGroup('Border', this.defaultBorderColorInput, { inline: true }));
27991
- defaultsSection.appendChild(defaultsRow);
27992
- const applyDefaultsBtn = this.createButton('Apply Defaults');
27993
- this.addButtonListener(applyDefaultsBtn, () => this.applyDefaults());
27994
- defaultsSection.appendChild(applyDefaultsBtn);
30035
+ this.addImmediateApplyListener(this.defaultBorderColorInput, () => this.applyDefaults());
30036
+ defaultsGroup.appendChild(this.createFormGroup('Border:', this.defaultBorderColorInput, { inline: true }));
30037
+ defaultsSection.appendChild(defaultsGroup);
27995
30038
  container.appendChild(defaultsSection);
27996
30039
  // Cell formatting section
27997
30040
  const cellSection = this.createSection('Cell Formatting');
@@ -28008,9 +30051,11 @@ class TablePane extends BasePane {
28008
30051
  mergeBtnGroup.appendChild(this.mergeCellsBtn);
28009
30052
  mergeBtnGroup.appendChild(this.splitCellBtn);
28010
30053
  cellSection.appendChild(mergeBtnGroup);
28011
- // Background
30054
+ cellSection.appendChild(document.createElement('hr'));
30055
+ // Background — inline
28012
30056
  this.cellBgColorInput = this.createColorInput('#ffffff');
28013
- cellSection.appendChild(this.createFormGroup('Background', this.cellBgColorInput));
30057
+ this.addImmediateApplyListener(this.cellBgColorInput, () => this.applyCellFormatting());
30058
+ cellSection.appendChild(this.createFormGroup('Background:', this.cellBgColorInput, { inline: true }));
28014
30059
  // Border checkboxes
28015
30060
  const borderChecks = document.createElement('div');
28016
30061
  borderChecks.className = 'pc-pane-row';
@@ -28042,24 +30087,29 @@ class TablePane extends BasePane {
28042
30087
  checkLabels[2].replaceChild(this.borderBottomCheck, checkLabels[2].querySelector('input'));
28043
30088
  if (checkLabels[3])
28044
30089
  checkLabels[3].replaceChild(this.borderLeftCheck, checkLabels[3].querySelector('input'));
28045
- cellSection.appendChild(this.createFormGroup('Borders', borderChecks));
30090
+ // Add change listeners for immediate apply on checkboxes
30091
+ for (const check of [this.borderTopCheck, this.borderRightCheck, this.borderBottomCheck, this.borderLeftCheck]) {
30092
+ check.addEventListener('change', () => this.applyCellFormatting());
30093
+ }
30094
+ cellSection.appendChild(this.createFormGroup('Borders:', borderChecks));
28046
30095
  // Border properties
28047
30096
  const borderPropsRow = this.createRow();
28048
30097
  this.borderWidthInput = this.createNumberInput({ min: 0, max: 5, value: 1 });
28049
30098
  this.borderColorInput = this.createColorInput('#cccccc');
28050
- borderPropsRow.appendChild(this.createFormGroup('Width', this.borderWidthInput, { inline: true }));
28051
- borderPropsRow.appendChild(this.createFormGroup('Color', this.borderColorInput, { inline: true }));
30099
+ this.addImmediateApplyListener(this.borderWidthInput, () => this.applyCellFormatting());
30100
+ this.addImmediateApplyListener(this.borderColorInput, () => this.applyCellFormatting());
30101
+ borderPropsRow.appendChild(this.createFormGroup('Width:', this.borderWidthInput, { inline: true }));
30102
+ borderPropsRow.appendChild(this.createFormGroup('Color:', this.borderColorInput, { inline: true }));
28052
30103
  cellSection.appendChild(borderPropsRow);
30104
+ // Style — inline
28053
30105
  this.borderStyleSelect = this.createSelect([
28054
30106
  { value: 'solid', label: 'Solid' },
28055
30107
  { value: 'dashed', label: 'Dashed' },
28056
30108
  { value: 'dotted', label: 'Dotted' },
28057
30109
  { value: 'none', label: 'None' }
28058
30110
  ], 'solid');
28059
- cellSection.appendChild(this.createFormGroup('Style', this.borderStyleSelect));
28060
- const applyCellBtn = this.createButton('Apply to Cell(s)', { variant: 'primary' });
28061
- this.addButtonListener(applyCellBtn, () => this.applyCellFormatting());
28062
- cellSection.appendChild(applyCellBtn);
30111
+ this.addImmediateApplyListener(this.borderStyleSelect, () => this.applyCellFormatting());
30112
+ cellSection.appendChild(this.createFormGroup('Style:', this.borderStyleSelect, { inline: true }));
28063
30113
  container.appendChild(cellSection);
28064
30114
  return container;
28065
30115
  }
@@ -28319,6 +30369,8 @@ exports.BasePane = BasePane;
28319
30369
  exports.BaseTextRegion = BaseTextRegion;
28320
30370
  exports.BodyTextRegion = BodyTextRegion;
28321
30371
  exports.ClipboardManager = ClipboardManager;
30372
+ exports.ConditionalSectionManager = ConditionalSectionManager;
30373
+ exports.ConditionalSectionPane = ConditionalSectionPane;
28322
30374
  exports.ContentAnalyzer = ContentAnalyzer;
28323
30375
  exports.DEFAULT_IMPORT_OPTIONS = DEFAULT_IMPORT_OPTIONS;
28324
30376
  exports.Document = Document;
@@ -28329,6 +30381,7 @@ exports.EmbeddedObjectFactory = EmbeddedObjectFactory;
28329
30381
  exports.EmbeddedObjectManager = EmbeddedObjectManager;
28330
30382
  exports.EventEmitter = EventEmitter;
28331
30383
  exports.FlowingTextContent = FlowingTextContent;
30384
+ exports.FontManager = FontManager;
28332
30385
  exports.FooterTextRegion = FooterTextRegion;
28333
30386
  exports.FormattingPane = FormattingPane;
28334
30387
  exports.HeaderTextRegion = HeaderTextRegion;
@@ -28344,6 +30397,7 @@ exports.PDFImportError = PDFImportError;
28344
30397
  exports.PDFImporter = PDFImporter;
28345
30398
  exports.PDFParser = PDFParser;
28346
30399
  exports.Page = Page;
30400
+ exports.PredicateEvaluator = PredicateEvaluator;
28347
30401
  exports.RegionManager = RegionManager;
28348
30402
  exports.RepeatingSectionManager = RepeatingSectionManager;
28349
30403
  exports.RepeatingSectionPane = RepeatingSectionPane;