pptx-angular-viewer 1.14.1 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20766,6 +20766,51 @@ function defaultTextAdvancedState() {
20766
20766
  rtl: false,
20767
20767
  };
20768
20768
  }
20769
+ // ==========================================================================
20770
+ // Text wrap + autofit mode (standalone; not part of TextAdvancedState so
20771
+ // existing consumers of that interface are unaffected by this addition).
20772
+ // ==========================================================================
20773
+ /** Available text-wrap options (`a:bodyPr/@wrap`). */
20774
+ const TEXT_WRAP_OPTIONS = [
20775
+ ['square', 'Wrap text in shape'],
20776
+ ['none', "Don't wrap text"],
20777
+ ];
20778
+ /**
20779
+ * Available autofit-mode options. Per this codebase's {@link TextStyle.autoFitMode}
20780
+ * mapping: `'shrink'` is `a:spAutoFit` (resize the shape to fit the text) and
20781
+ * `'normal'` is `a:normAutofit` (shrink the text on overflow).
20782
+ */
20783
+ const AUTOFIT_MODE_OPTIONS = [
20784
+ ['none', 'Do not autofit'],
20785
+ ['normal', 'Shrink text on overflow'],
20786
+ ['shrink', 'Resize shape to fit text'],
20787
+ ];
20788
+ /** Read the effective text-wrap mode, defaulting to `'square'` (wrap on). */
20789
+ function textWrapOf(el) {
20790
+ if (!hasTextProperties(el)) {
20791
+ return 'square';
20792
+ }
20793
+ return el.textStyle?.textWrap ?? 'square';
20794
+ }
20795
+ /** Read the effective autofit mode, defaulting to `'none'`. */
20796
+ function autoFitModeOf(el) {
20797
+ if (!hasTextProperties(el)) {
20798
+ return 'none';
20799
+ }
20800
+ return el.textStyle?.autoFitMode ?? 'none';
20801
+ }
20802
+ /** Patch to update the text-wrap mode, preserving the rest of `textStyle`. */
20803
+ function textWrapPatch(el, wrap) {
20804
+ const base = hasTextProperties(el) ? (el.textStyle ?? {}) : {};
20805
+ return { textStyle: { ...base, textWrap: wrap } };
20806
+ }
20807
+ /** Patch to update the autofit mode, preserving the rest of `textStyle`. */
20808
+ function autoFitModePatch(el, mode) {
20809
+ const base = hasTextProperties(el) ? (el.textStyle ?? {}) : {};
20810
+ return {
20811
+ textStyle: { ...base, autoFitMode: mode, autoFit: mode !== 'none' },
20812
+ };
20813
+ }
20769
20814
 
20770
20815
  /**
20771
20816
  * PPTX document-theme font + colour resolution helpers.
@@ -22822,7 +22867,7 @@ function assignUserColor(seed, palette = CURSOR_PALETTE) {
22822
22867
  * truncating, the result is exactly `maxChars` characters including a trailing
22823
22868
  * `...` ellipsis.
22824
22869
  */
22825
- function formatCursorLabel$1(userName, maxChars = MAX_LABEL_CHARS) {
22870
+ function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
22826
22871
  if (userName.length <= maxChars) {
22827
22872
  return userName;
22828
22873
  }
@@ -22953,6 +22998,175 @@ function asSelectionIds(value) {
22953
22998
  return value.filter((entry) => typeof entry === 'string');
22954
22999
  }
22955
23000
 
23001
+ /**
23002
+ * Create a throttled presence publisher bound to an awareness instance. The
23003
+ * initial presence is announced synchronously so peers learn of us at once.
23004
+ */
23005
+ function createPresencePublisher(awareness, identity) {
23006
+ const local = { activeSlideIndex: 0, cursorX: 0, cursorY: 0 };
23007
+ // Starts at 0 so the first real update publishes immediately (the initial
23008
+ // announce below does not consume the throttle budget).
23009
+ let lastBroadcast = 0;
23010
+ let pending = null;
23011
+ function publish() {
23012
+ awareness.setLocalStateField('presence', {
23013
+ userName: identity.userName,
23014
+ userColor: identity.userColor,
23015
+ userAvatar: identity.userAvatar,
23016
+ role: identity.role,
23017
+ activeSlideIndex: local.activeSlideIndex,
23018
+ cursorX: local.cursorX,
23019
+ cursorY: local.cursorY,
23020
+ selectedElementId: local.selectedElementId,
23021
+ lastUpdated: new Date().toISOString(),
23022
+ });
23023
+ }
23024
+ function update(patch) {
23025
+ Object.assign(local, patch);
23026
+ const elapsed = Date.now() - lastBroadcast;
23027
+ if (elapsed >= BROADCAST_THROTTLE_MS) {
23028
+ if (pending !== null) {
23029
+ clearTimeout(pending);
23030
+ pending = null;
23031
+ }
23032
+ lastBroadcast = Date.now();
23033
+ publish();
23034
+ }
23035
+ else if (pending === null) {
23036
+ pending = setTimeout(() => {
23037
+ pending = null;
23038
+ lastBroadcast = Date.now();
23039
+ publish();
23040
+ }, BROADCAST_THROTTLE_MS - elapsed);
23041
+ }
23042
+ }
23043
+ function flush() {
23044
+ if (pending !== null) {
23045
+ clearTimeout(pending);
23046
+ pending = null;
23047
+ }
23048
+ lastBroadcast = Date.now();
23049
+ publish();
23050
+ }
23051
+ function dispose() {
23052
+ if (pending !== null) {
23053
+ clearTimeout(pending);
23054
+ pending = null;
23055
+ }
23056
+ }
23057
+ // Announce initial presence immediately (without consuming the throttle).
23058
+ publish();
23059
+ return { update, flush, dispose };
23060
+ }
23061
+
23062
+ /**
23063
+ * collaboration-assets.ts: binary payload sync for the pptx-viewer
23064
+ * collaboration stack.
23065
+ *
23066
+ * Large binary element fields (media/OLE/3D-model payloads) are synced once
23067
+ * via a separate `pptx:assets` Y.Map keyed by `${elementId}:${fieldName}`,
23068
+ * instead of being re-embedded as base64 inside every element write. This
23069
+ * avoids re-transmitting multi-MB payloads on every unrelated edit to the
23070
+ * same element: `writeAssetFields` only touches the assets map when the
23071
+ * stored payload actually differs from the incoming value.
23072
+ *
23073
+ * Known, accepted cost: asset entries are NOT garbage-collected when the
23074
+ * element that referenced them is deleted outright (only an explicit
23075
+ * field-clear removes its entry, via the `else` branch below). A long-lived
23076
+ * room's `pptx:assets` map can only grow. No sweep is implemented in this
23077
+ * pass; correct GC across concurrent/offline peers is a separate problem.
23078
+ */
23079
+ const YDOC_ASSETS_KEY = 'pptx:assets';
23080
+ /**
23081
+ * Element fields large/binary enough to route through the asset map instead
23082
+ * of being embedded inline as a scalar or complex JSON-blob field.
23083
+ */
23084
+ const ASSET_ELEMENT_FIELDS = new Set([
23085
+ 'mediaData',
23086
+ 'posterFrameData',
23087
+ 'oleEmbeddedData',
23088
+ 'previewImageData',
23089
+ 'modelData',
23090
+ ]);
23091
+ const ASSET_REF_KEYS = {
23092
+ mediaData: '_mdRef',
23093
+ posterFrameData: '_pfdRef',
23094
+ oleEmbeddedData: '_oedRef',
23095
+ previewImageData: '_pidRef',
23096
+ modelData: '_moRef',
23097
+ };
23098
+ const REV_ASSET_REF_KEYS = Object.fromEntries(Object.entries(ASSET_REF_KEYS).map(([field, ref]) => [ref, field]));
23099
+ function assetKey(elementId, fieldName) {
23100
+ return `${elementId}:${fieldName}`;
23101
+ }
23102
+ function getAssetsMap(ydoc) {
23103
+ return ydoc.getMap(YDOC_ASSETS_KEY);
23104
+ }
23105
+ /** True when `key` is one of the element-map ref-pointer keys this module owns. */
23106
+ function isAssetRefKey(key) {
23107
+ return key in REV_ASSET_REF_KEYS;
23108
+ }
23109
+ /**
23110
+ * Writes any present asset-routed fields from `rec` into `assets`, storing
23111
+ * only a small ref pointer (not the payload) on the element's own `ymap`.
23112
+ * Never reads from `ymap` itself, so it's safe to call on a brand-new,
23113
+ * not-yet-integrated map (Yjs throws on reading an unintegrated type) - use
23114
+ * this from a create/write path. `assets` is always a root-level (already
23115
+ * integrated) map, so the identical-payload skip-write check is still safe.
23116
+ */
23117
+ function writeAssetFields(elementId, rec, ymap, assets) {
23118
+ for (const field of ASSET_ELEMENT_FIELDS) {
23119
+ const value = rec[field];
23120
+ if (typeof value === 'string' && value.length > 0) {
23121
+ const key = assetKey(elementId, field);
23122
+ if (assets.get(key) !== value) {
23123
+ assets.set(key, value);
23124
+ }
23125
+ ymap.set(ASSET_REF_KEYS[field], key);
23126
+ }
23127
+ }
23128
+ }
23129
+ /**
23130
+ * Reconcile variant of {@link writeAssetFields} for an `ymap` that is
23131
+ * already integrated (an existing live element being updated in place):
23132
+ * additionally reads the current ref to skip a redundant set, and clears
23133
+ * the ref + its `assets` entry when the field was cleared.
23134
+ */
23135
+ function reconcileAssetFields(elementId, rec, ymap, assets) {
23136
+ for (const field of ASSET_ELEMENT_FIELDS) {
23137
+ const refKey = ASSET_REF_KEYS[field];
23138
+ const value = rec[field];
23139
+ if (typeof value === 'string' && value.length > 0) {
23140
+ const key = assetKey(elementId, field);
23141
+ if (assets.get(key) !== value) {
23142
+ assets.set(key, value);
23143
+ }
23144
+ if (ymap.get(refKey) !== key) {
23145
+ ymap.set(refKey, key);
23146
+ }
23147
+ }
23148
+ else {
23149
+ const existingRef = ymap.get(refKey);
23150
+ if (typeof existingRef === 'string') {
23151
+ ymap.delete(refKey);
23152
+ assets.delete(existingRef);
23153
+ }
23154
+ }
23155
+ }
23156
+ }
23157
+ /** Rehydrates any asset-routed fields referenced by `ymap` onto `element`. */
23158
+ function readAssetFields(ymap, assets, element) {
23159
+ for (const [refKey, field] of Object.entries(REV_ASSET_REF_KEYS)) {
23160
+ const ref = ymap.get(refKey);
23161
+ if (typeof ref === 'string') {
23162
+ const value = assets.get(ref);
23163
+ if (typeof value === 'string') {
23164
+ element[field] = value;
23165
+ }
23166
+ }
23167
+ }
23168
+ }
23169
+
22956
23170
  /**
22957
23171
  * collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
22958
23172
  * collaboration sync layer. Split out of collaboration-sync.ts to keep both
@@ -23221,6 +23435,9 @@ const SCALAR_ELEMENT_KEYS = new Set([
23221
23435
  'width',
23222
23436
  'height',
23223
23437
  'rotation',
23438
+ 'shapeId',
23439
+ 'skewX',
23440
+ 'skewY',
23224
23441
  'flipHorizontal',
23225
23442
  'flipVertical',
23226
23443
  'hidden',
@@ -23229,14 +23446,66 @@ const SCALAR_ELEMENT_KEYS = new Set([
23229
23446
  'name',
23230
23447
  'altText',
23231
23448
  'shapeType',
23232
- 'placeholder',
23233
23449
  'imagePath',
23234
23450
  'imageData',
23235
- 'svgContent',
23236
- 'inkSvg',
23237
- 'sourceSlideId',
23451
+ 'svgData',
23452
+ 'svgPath',
23453
+ 'cropLeft',
23454
+ 'cropTop',
23455
+ 'cropRight',
23456
+ 'cropBottom',
23457
+ 'tileOffsetX',
23458
+ 'tileOffsetY',
23459
+ 'tileScaleX',
23460
+ 'tileScaleY',
23461
+ 'tileFlip',
23462
+ 'tileAlignment',
23463
+ 'pathData',
23464
+ 'pathWidth',
23465
+ 'pathHeight',
23238
23466
  'mediaType',
23239
23467
  'mediaPath',
23468
+ 'mediaMimeType',
23469
+ 'trimStartMs',
23470
+ 'trimEndMs',
23471
+ 'posterFramePath',
23472
+ 'fullScreen',
23473
+ 'loop',
23474
+ 'fadeInDuration',
23475
+ 'fadeOutDuration',
23476
+ 'volume',
23477
+ 'autoPlay',
23478
+ 'playAcrossSlides',
23479
+ 'hideWhenNotPlaying',
23480
+ 'playbackSpeed',
23481
+ 'mediaMissing',
23482
+ 'isLinked',
23483
+ 'oleTarget',
23484
+ 'oleProgId',
23485
+ 'oleName',
23486
+ 'oleClsId',
23487
+ 'oleObjectType',
23488
+ 'oleFileExtension',
23489
+ 'fileName',
23490
+ 'externalPath',
23491
+ 'previewImage',
23492
+ 'oleShowAsIcon',
23493
+ 'oleImgW',
23494
+ 'oleImgH',
23495
+ 'oleEmbeddedFileName',
23496
+ 'oleEmbeddedMimeType',
23497
+ 'oleEmbeddedByteSize',
23498
+ 'inkPaths',
23499
+ 'inkColors',
23500
+ 'inkWidths',
23501
+ 'inkOpacities',
23502
+ 'inkTool',
23503
+ 'zoomType',
23504
+ 'targetSlideIndex',
23505
+ 'targetSectionId',
23506
+ 'modelPath',
23507
+ 'modelMimeType',
23508
+ 'posterImage',
23240
23509
  'linkedTxbxId',
23241
23510
  'linkedTxbxSeq',
23242
23511
  'promptText',
@@ -23249,26 +23518,35 @@ const COMPLEX_ELEMENT_FIELDS = {
23249
23518
  tableData: '_td',
23250
23519
  chartData: '_cd',
23251
23520
  smartArtData: '_smad',
23252
- connectionStart: '_cs',
23253
- connectionEnd: '_ce',
23254
- animations: '_an',
23255
- nativeAnimations: '_na',
23256
23521
  children: '_ch',
23257
23522
  paragraphIndents: '_pi',
23258
23523
  rawXml: '_rx',
23524
+ extLstXml: '_elx',
23259
23525
  actionClick: '_ac',
23260
23526
  actionHover: '_av',
23261
23527
  locks: '_lk',
23262
23528
  imageEffects: '_ie',
23263
23529
  cropShape: '_cr',
23264
- mediaBookmarks: '_mb',
23530
+ bookmarks: '_mb',
23265
23531
  captionTracks: '_ct',
23532
+ metadata: '_md',
23533
+ groupFill: '_gf',
23534
+ inkPointPressures: '_ipp',
23535
+ inkStrokes: '_cis',
23536
+ extensionXml: '_ext',
23537
+ customGeometryPaths: '_cgp',
23538
+ customGeometryRawData: '_cgr',
23539
+ customGeometryAdjustHandlesXY: '_cgx',
23540
+ customGeometryAdjustHandlesPolar: '_cgo',
23541
+ customGeometryConnectionSites: '_cgc',
23542
+ customGeometryTextRect: '_cgt',
23266
23543
  };
23267
23544
  const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
23268
23545
  const SCALAR_SLIDE_KEYS = new Set([
23269
23546
  'id',
23270
23547
  'rId',
23271
23548
  'sourceSlideId',
23549
+ 'name',
23272
23550
  'layoutPath',
23273
23551
  'layoutName',
23274
23552
  'slideNumber',
@@ -23278,7 +23556,9 @@ const SCALAR_SLIDE_KEYS = new Set([
23278
23556
  'backgroundColor',
23279
23557
  'backgroundImage',
23280
23558
  'backgroundGradient',
23559
+ 'backgroundShadeToTitle',
23281
23560
  'notes',
23561
+ 'notesCSldName',
23282
23562
  'backgroundShowAnimation',
23283
23563
  'showMasterShapes',
23284
23564
  'isDirty',
@@ -23289,6 +23569,8 @@ const COMPLEX_SLIDE_FIELDS = {
23289
23569
  nativeAnimations: '_na',
23290
23570
  rawTiming: '_rt',
23291
23571
  notesSegments: '_ns',
23572
+ notesShapes: '_nsh',
23573
+ notesClrMapOverride: '_ncm',
23292
23574
  comments: '_cm',
23293
23575
  warnings: '_wa',
23294
23576
  rawXml: '_rx',
@@ -23296,15 +23578,17 @@ const COMPLEX_SLIDE_FIELDS = {
23296
23578
  guides: '_gu',
23297
23579
  customerData: '_cu',
23298
23580
  activeXControls: '_ax',
23581
+ backgroundPattern: '_bp',
23582
+ headerFooterFlags: '_hff',
23299
23583
  };
23300
23584
  const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
23301
23585
  // ---------------------------------------------------------------------------
23302
23586
  // Element serialization
23303
23587
  // ---------------------------------------------------------------------------
23304
- function writeElementToYMap(element, ymap, factories) {
23588
+ function writeElementToYMap(element, ymap, factories, assets) {
23305
23589
  const rec = element;
23306
23590
  for (const [key, value] of Object.entries(rec)) {
23307
- if (value === undefined) {
23591
+ if (value === undefined || ASSET_ELEMENT_FIELDS.has(key)) {
23308
23592
  continue;
23309
23593
  }
23310
23594
  if (SCALAR_ELEMENT_KEYS.has(key)) {
@@ -23321,8 +23605,9 @@ function writeElementToYMap(element, ymap, factories) {
23321
23605
  ymap.set(COMPLEX_ELEMENT_FIELDS[key], JSON.stringify(value));
23322
23606
  }
23323
23607
  }
23608
+ writeAssetFields(rec.id, rec, ymap, assets);
23324
23609
  }
23325
- function readElementFromYMap(ymap) {
23610
+ function readElementFromYMap(ymap, assets) {
23326
23611
  const element = {};
23327
23612
  ymap.forEach((value, key) => {
23328
23613
  if (key === 'textBody') {
@@ -23330,6 +23615,9 @@ function readElementFromYMap(ymap) {
23330
23615
  element.textSegments = decodeTextBody(value);
23331
23616
  }
23332
23617
  }
23618
+ else if (isAssetRefKey(key)) {
23619
+ // handled by readAssetFields below; not a literal PptxElement field
23620
+ }
23333
23621
  else if (REV_COMPLEX_ELEMENT[key]) {
23334
23622
  try {
23335
23623
  element[REV_COMPLEX_ELEMENT[key]] = JSON.parse(value);
@@ -23342,12 +23630,13 @@ function readElementFromYMap(ymap) {
23342
23630
  element[key] = value;
23343
23631
  }
23344
23632
  });
23633
+ readAssetFields(ymap, assets, element);
23345
23634
  return element;
23346
23635
  }
23347
23636
  // ---------------------------------------------------------------------------
23348
23637
  // Slide serialization
23349
23638
  // ---------------------------------------------------------------------------
23350
- function writeSlideToYMap(slide, ymap, factories) {
23639
+ function writeSlideToYMap(slide, ymap, factories, assets) {
23351
23640
  const rec = slide;
23352
23641
  for (const key of SCALAR_SLIDE_KEYS) {
23353
23642
  if (rec[key] !== undefined) {
@@ -23362,12 +23651,12 @@ function writeSlideToYMap(slide, ymap, factories) {
23362
23651
  const elemArr = factories.createArray();
23363
23652
  for (const el of slide.elements) {
23364
23653
  const elemMap = factories.createMap();
23365
- writeElementToYMap(el, elemMap, factories);
23654
+ writeElementToYMap(el, elemMap, factories, assets);
23366
23655
  elemArr.push([elemMap]);
23367
23656
  }
23368
23657
  ymap.set('elements', elemArr);
23369
23658
  }
23370
- function readSlideFromYMap(ymap) {
23659
+ function readSlideFromYMap(ymap, assets) {
23371
23660
  const slide = {};
23372
23661
  for (const key of SCALAR_SLIDE_KEYS) {
23373
23662
  const v = ymap.get(key);
@@ -23390,7 +23679,7 @@ function readSlideFromYMap(ymap) {
23390
23679
  const elements = [];
23391
23680
  if (elemArr) {
23392
23681
  for (let i = 0; i < elemArr.length; i++) {
23393
- elements.push(readElementFromYMap(elemArr.get(i)));
23682
+ elements.push(readElementFromYMap(elemArr.get(i), assets));
23394
23683
  }
23395
23684
  }
23396
23685
  slide.elements = elements;
@@ -23405,6 +23694,7 @@ function readSlideFromYMap(ymap) {
23405
23694
  * one-shot seeding of an empty document.
23406
23695
  */
23407
23696
  function writeSlidesToYDoc(slides, ydoc, factories, origin) {
23697
+ const assets = getAssetsMap(ydoc);
23408
23698
  ydoc.transact(() => {
23409
23699
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23410
23700
  if (arr.length > 0) {
@@ -23412,16 +23702,17 @@ function writeSlidesToYDoc(slides, ydoc, factories, origin) {
23412
23702
  }
23413
23703
  for (const slide of slides) {
23414
23704
  const ymap = factories.createMap();
23415
- writeSlideToYMap(slide, ymap, factories);
23705
+ writeSlideToYMap(slide, ymap, factories, assets);
23416
23706
  arr.push([ymap]);
23417
23707
  }
23418
23708
  }, origin);
23419
23709
  }
23420
23710
  function readSlidesFromYDoc(ydoc) {
23711
+ const assets = getAssetsMap(ydoc);
23421
23712
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23422
23713
  const slides = [];
23423
23714
  for (let i = 0; i < arr.length; i++) {
23424
- slides.push(readSlideFromYMap(arr.get(i)));
23715
+ slides.push(readSlideFromYMap(arr.get(i), assets));
23425
23716
  }
23426
23717
  return slides;
23427
23718
  }
@@ -23673,11 +23964,12 @@ function reconcileTextBody(ymap, rec, factories) {
23673
23964
  encodeTextBody(segments, ytext);
23674
23965
  ymap.set('textBody', ytext);
23675
23966
  }
23676
- function reconcileElementYMap(ymap, element, factories) {
23967
+ function reconcileElementYMap(ymap, element, factories, assets) {
23677
23968
  const rec = element;
23678
23969
  reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
23679
23970
  reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
23680
23971
  reconcileTextBody(ymap, rec, factories);
23972
+ reconcileAssetFields(rec.id, rec, ymap, assets);
23681
23973
  }
23682
23974
  function mapIdAt(arr, index) {
23683
23975
  const entry = arr.get(index);
@@ -23754,7 +24046,7 @@ const isYArrayLike = (value) => typeof value === 'object' &&
23754
24046
  value !== null &&
23755
24047
  typeof value.insert === 'function' &&
23756
24048
  typeof value.toArray === 'function';
23757
- function reconcileSlideYMap(ymap, slide, factories) {
24049
+ function reconcileSlideYMap(ymap, slide, factories, assets) {
23758
24050
  const rec = slide;
23759
24051
  reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
23760
24052
  reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
@@ -23767,10 +24059,10 @@ function reconcileSlideYMap(ymap, slide, factories) {
23767
24059
  idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
23768
24060
  create: (el) => {
23769
24061
  const map = factories.createMap();
23770
- writeElementToYMap(el, map, factories);
24062
+ writeElementToYMap(el, map, factories, assets);
23771
24063
  return map;
23772
24064
  },
23773
- update: (map, el) => reconcileElementYMap(map, el, factories),
24065
+ update: (map, el) => reconcileElementYMap(map, el, factories, assets),
23774
24066
  });
23775
24067
  }
23776
24068
  /**
@@ -23779,16 +24071,17 @@ function reconcileSlideYMap(ymap, slide, factories) {
23779
24071
  * caller's own observer can skip the resulting events.
23780
24072
  */
23781
24073
  function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
24074
+ const assets = getAssetsMap(ydoc);
23782
24075
  ydoc.transact(() => {
23783
24076
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23784
24077
  reconcileYArrayById(arr, slides, {
23785
24078
  idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
23786
24079
  create: (slide) => {
23787
24080
  const map = factories.createMap();
23788
- writeSlideToYMap(slide, map, factories);
24081
+ writeSlideToYMap(slide, map, factories, assets);
23789
24082
  return map;
23790
24083
  },
23791
- update: (map, slide) => reconcileSlideYMap(map, slide, factories),
24084
+ update: (map, slide) => reconcileSlideYMap(map, slide, factories, assets),
23792
24085
  });
23793
24086
  }, origin);
23794
24087
  }
@@ -23845,6 +24138,50 @@ function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
23845
24138
  };
23846
24139
  }
23847
24140
 
24141
+ const DEFAULT_DEBOUNCE_MS = 5_000;
24142
+ function createWriteBackScheduler(deps) {
24143
+ let timer = null;
24144
+ function cancel() {
24145
+ if (timer !== null) {
24146
+ clearTimeout(timer);
24147
+ timer = null;
24148
+ }
24149
+ }
24150
+ function schedule(config) {
24151
+ if (!config.onWriteBack || config.role !== 'owner' || !deps.getYDoc()) {
24152
+ return;
24153
+ }
24154
+ cancel();
24155
+ const debounceMs = config.writeBackDebounceMs ?? DEFAULT_DEBOUNCE_MS;
24156
+ timer = setTimeout(async () => {
24157
+ timer = null;
24158
+ const ydoc = deps.getYDoc();
24159
+ if (!ydoc || !config.onWriteBack) {
24160
+ return;
24161
+ }
24162
+ const sourceBytes = deps.getSourceBytes?.();
24163
+ if (!sourceBytes) {
24164
+ return;
24165
+ }
24166
+ try {
24167
+ const { PptxHandler } = await import('pptx-viewer-core');
24168
+ const handler = new PptxHandler();
24169
+ await handler.load(sourceBytes.buffer);
24170
+ const slides = readSlidesFromYDoc(ydoc);
24171
+ const merged = deps.mergeTemplateElements
24172
+ ? deps.mergeTemplateElements(slides, deps.getTemplateElements?.() ?? {})
24173
+ : slides;
24174
+ const bytes = await handler.save(merged);
24175
+ config.onWriteBack(bytes);
24176
+ }
24177
+ catch {
24178
+ /* non-fatal */
24179
+ }
24180
+ }, debounceMs);
24181
+ }
24182
+ return { schedule, cancel };
24183
+ }
24184
+
23848
24185
  // ---------------------------------------------------------------------------
23849
24186
  // Tuning constants (px tolerances for position / size changes)
23850
24187
  // ---------------------------------------------------------------------------
@@ -31881,6 +32218,155 @@ function sanitizeStops(raw) {
31881
32218
  return sortStops(mapped);
31882
32219
  }
31883
32220
 
32221
+ /**
32222
+ * Pure (framework-agnostic) helpers for the image adjustments + crop inspector
32223
+ * panel. Readers extract current values from a PptxElement (falling back to
32224
+ * sensible defaults); patch-builders produce shallow-merge-ready
32225
+ * `Partial<PptxElement>` objects safe to pass to each binding's element-update
32226
+ * action. Mirrors the pattern used by `gradient-picker.ts` / `text-advanced.ts`.
32227
+ *
32228
+ * Scope: brightness/contrast/saturation (`PptxImageEffects`) and the four crop
32229
+ * insets (`PptxImageProperties.cropLeft/Top/Right/Bottom`). Other image effects
32230
+ * (duotone, artistic filters, alpha primitives) are out of scope for the
32231
+ * inspector's "highest-traffic" surface; see `image-effects.ts` for the full
32232
+ * read-side effect model.
32233
+ */
32234
+ /** Read the current brightness/contrast/saturation off an element (0 when unset). */
32235
+ function imageAdjustmentsStateOf(el) {
32236
+ const fx = isImageLikeElement(el) ? el.imageEffects : undefined;
32237
+ return {
32238
+ brightness: fx?.brightness ?? 0,
32239
+ contrast: fx?.contrast ?? 0,
32240
+ saturation: fx?.saturation ?? 0,
32241
+ };
32242
+ }
32243
+ /**
32244
+ * Build a Partial<PptxElement> that merges the given adjustment changes into
32245
+ * the element's existing `imageEffects` without dropping other effect fields.
32246
+ * No-op (returns `{}`) for non-image elements.
32247
+ */
32248
+ function imageAdjustmentsPatch(el, changes) {
32249
+ if (!isImageLikeElement(el)) {
32250
+ return {};
32251
+ }
32252
+ return {
32253
+ imageEffects: {
32254
+ ...el.imageEffects,
32255
+ ...changes,
32256
+ },
32257
+ };
32258
+ }
32259
+ /** Read the current crop insets off an element (0 on every edge when unset). */
32260
+ function imageCropStateOf(el) {
32261
+ if (!isImageLikeElement(el)) {
32262
+ return { cropLeft: 0, cropTop: 0, cropRight: 0, cropBottom: 0 };
32263
+ }
32264
+ return {
32265
+ cropLeft: el.cropLeft ?? 0,
32266
+ cropTop: el.cropTop ?? 0,
32267
+ cropRight: el.cropRight ?? 0,
32268
+ cropBottom: el.cropBottom ?? 0,
32269
+ };
32270
+ }
32271
+ /** Clamp a crop fraction to the sane `[0, 0.9]` range (never crop past 90% of an edge). */
32272
+ function clampCropFraction(value) {
32273
+ if (!Number.isFinite(value)) {
32274
+ return 0;
32275
+ }
32276
+ return Math.min(0.9, Math.max(0, value));
32277
+ }
32278
+ /**
32279
+ * Build a Partial<PptxElement> that merges the given crop-inset changes onto
32280
+ * the element. No-op (returns `{}`) for non-image elements.
32281
+ */
32282
+ function imageCropPatch(el, changes) {
32283
+ if (!isImageLikeElement(el)) {
32284
+ return {};
32285
+ }
32286
+ const patch = {};
32287
+ for (const [key, value] of Object.entries(changes)) {
32288
+ if (value !== undefined) {
32289
+ patch[key] = clampCropFraction(value);
32290
+ }
32291
+ }
32292
+ return patch;
32293
+ }
32294
+
32295
+ /**
32296
+ * Pure (framework-agnostic) helpers for the table-level inspector panel
32297
+ * (header row / banded rows toggles + a uniform default cell padding). Mirrors
32298
+ * the reader + patch-builder pattern used by `gradient-picker.ts` /
32299
+ * `text-advanced.ts` / `image-adjustments.ts`.
32300
+ *
32301
+ * Scope note: this binding has no per-cell selection model, so cell-level
32302
+ * formatting (background/border for one specific cell) is out of scope here;
32303
+ * `applyUniformCellPaddingPatch` instead writes the same margin onto every
32304
+ * cell uniformly, which is the closest table-level equivalent.
32305
+ */
32306
+ /** Read table-level flags off a table element (all false/0 for non-table elements). */
32307
+ function tableInspectorStateOf(el) {
32308
+ if (el.type !== 'table' || !el.tableData) {
32309
+ return { firstRowHeader: false, bandedRows: false, bandedColumns: false, cellPadding: 0 };
32310
+ }
32311
+ const data = el.tableData;
32312
+ return {
32313
+ firstRowHeader: data.firstRowHeader ?? false,
32314
+ bandedRows: data.bandedRows ?? false,
32315
+ bandedColumns: data.bandedColumns ?? false,
32316
+ cellPadding: firstCellPadding(data),
32317
+ };
32318
+ }
32319
+ function firstCellPadding(data) {
32320
+ const firstCell = data.rows[0]?.cells[0];
32321
+ return firstCell?.style?.marginLeft ?? 0;
32322
+ }
32323
+ /**
32324
+ * Build a Partial<PptxElement> that merges the given flag changes into the
32325
+ * element's existing `tableData`. No-op (returns `{}`) for non-table elements
32326
+ * or a table with no parsed data.
32327
+ */
32328
+ function tableInspectorPatch(el, changes) {
32329
+ if (el.type !== 'table' || !el.tableData) {
32330
+ return {};
32331
+ }
32332
+ return {
32333
+ tableData: {
32334
+ ...el.tableData,
32335
+ ...changes,
32336
+ },
32337
+ };
32338
+ }
32339
+ /**
32340
+ * Build a Partial<PptxElement> that sets the same left/right/top/bottom margin
32341
+ * (in px) on every cell of every row, so the table gets uniform default
32342
+ * padding. No-op (returns `{}`) for non-table elements or a table with no rows.
32343
+ */
32344
+ function applyUniformCellPaddingPatch(el, padding) {
32345
+ if (el.type !== 'table' || !el.tableData) {
32346
+ return {};
32347
+ }
32348
+ const clamped = Math.max(0, Math.round(padding));
32349
+ const rows = el.tableData.rows.map((row) => ({
32350
+ ...row,
32351
+ cells: row.cells.map((cell) => ({
32352
+ ...cell,
32353
+ style: {
32354
+ ...cell.style,
32355
+ marginLeft: clamped,
32356
+ marginRight: clamped,
32357
+ marginTop: clamped,
32358
+ marginBottom: clamped,
32359
+ },
32360
+ })),
32361
+ }));
32362
+ return {
32363
+ tableData: {
32364
+ ...el.tableData,
32365
+ rows,
32366
+ },
32367
+ };
32368
+ }
32369
+
31884
32370
  /**
31885
32371
  * Pure, framework-agnostic comment-array transforms, shared by every binding.
31886
32372
  *
@@ -33088,6 +33574,51 @@ const CHANGE_CASE_OPTIONS$1 = [
33088
33574
  { value: 'toggle', i18nKey: 'pptx.text.changeCaseToggle' },
33089
33575
  ];
33090
33576
 
33577
+ /**
33578
+ * color-swatches.ts: the canonical "Office Standard Colors" swatch catalogue
33579
+ * shared by every binding's colour pickers (font colour, highlight colour,
33580
+ * and any future fill/line colour picker that wants the same 10-swatch grid).
33581
+ *
33582
+ * Mirrors the "Standard Colors" row PowerPoint itself ships. Before this
33583
+ * module existed, the VanillaJS (`swatch-picker.ts`) and Svelte
33584
+ * (`SwatchColorPicker.svelte`) bindings each independently hardcoded an
33585
+ * identical 10-colour list; this module is the single source of truth going
33586
+ * forward so no binding drifts from the others.
33587
+ *
33588
+ * There is no shared-i18n dictionary key for individual colour names yet
33589
+ * (only the `pptx.ribbon.customColour` "Custom colour..." row label exists),
33590
+ * so {@link OfficeColorSwatch.label} is a plain English fallback rather than
33591
+ * an `i18nKey`, matching the optional-label convention used elsewhere in this
33592
+ * module family (e.g. `LineSpacingOption` in `text-format-presets.ts`).
33593
+ *
33594
+ * @module render/color-swatches
33595
+ */
33596
+ /**
33597
+ * The canonical 10-swatch "Office Standard Colors" catalogue, in PowerPoint's
33598
+ * on-screen order. Consumed by the font-colour and highlight-colour pickers
33599
+ * (and any other picker that wants the same standard set) across every
33600
+ * binding.
33601
+ */
33602
+ const OFFICE_COLOR_SWATCHES = [
33603
+ { hex: '#000000', label: 'Black' },
33604
+ { hex: '#ffffff', label: 'White' },
33605
+ { hex: '#ff0000', label: 'Red' },
33606
+ { hex: '#00aa00', label: 'Green' },
33607
+ { hex: '#0000ff', label: 'Blue' },
33608
+ { hex: '#ff8800', label: 'Orange' },
33609
+ { hex: '#8800cc', label: 'Purple' },
33610
+ { hex: '#00cccc', label: 'Cyan' },
33611
+ { hex: '#ff69b4', label: 'Pink' },
33612
+ { hex: '#808080', label: 'Gray' },
33613
+ ];
33614
+ /**
33615
+ * The same catalogue flattened to bare `#rrggbb` hex strings, in the same
33616
+ * order as {@link OFFICE_COLOR_SWATCHES}. A drop-in replacement for the
33617
+ * `readonly string[]` swatch lists the vanilla and Svelte bindings each
33618
+ * hardcoded locally before this module existed.
33619
+ */
33620
+ const OFFICE_COLOR_SWATCH_HEXES = OFFICE_COLOR_SWATCHES.map((swatch) => swatch.hex);
33621
+
33091
33622
  /**
33092
33623
  * PowerPoint-style title bar: shared pure logic + Tailwind class tokens.
33093
33624
  *
@@ -37508,25 +38039,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
37508
38039
  * surface so `collaboration.service.ts`, `collaboration-cursors.component.ts`,
37509
38040
  * the viewer barrel, and the colocated tests are unchanged.
37510
38041
  *
37511
- * Two pieces stay Angular-local because they diverge from the shared copy:
37512
- * - `RemotePresence` is the Angular name for shared's `SanitizedPresence`
37513
- * (re-aliased here).
37514
- * - `formatCursorLabel` keeps the historical single-character ellipsis (`…`)
37515
- * truncation, which differs from shared's three-dot (`...`) variant that the
37516
- * React binding expects. Kept local so the rendered label and the colocated
37517
- * test are unchanged.
37518
- *
37519
- * `mapAwarenessCursors` (the foundational bare-`{ cursor, user }` mapping) now
37520
- * lives in shared and is re-exported here so existing Angular imports are
37521
- * unchanged.
37522
- */
37523
- /**
37524
- * Clamp/format a cursor label so long names don't overflow the chip. Uses a
37525
- * single-character ellipsis (`…`); the total length is exactly `maxChars`.
38042
+ * `RemotePresence` is the Angular name for shared's `SanitizedPresence`
38043
+ * (re-aliased here). `formatCursorLabel` and `mapAwarenessCursors` are now
38044
+ * re-exported as-is from shared: Angular's `formatCursorLabel` used to keep
38045
+ * its own single-character ellipsis (`…`) truncation that differed from
38046
+ * shared's three-dot (`...`) variant, so two peers viewing the same presence
38047
+ * record saw differently-truncated names depending on which binding hosted
38048
+ * the session - a real cross-binding consistency bug, not a style choice.
37526
38049
  */
37527
- function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
37528
- return userName.length > maxChars ? `${userName.slice(0, maxChars - 1)}…` : userName;
37529
- }
37530
38050
 
37531
38051
  /**
37532
38052
  * collaboration-cursors.component.ts: Angular port of the Vue
@@ -37668,8 +38188,6 @@ class LocalPresencePublisher {
37668
38188
  setCursor(x, y, activeSlideIndex = this.activeSlide) {
37669
38189
  this.cursor = { x, y };
37670
38190
  this.activeSlide = activeSlideIndex;
37671
- // A bare `cursor` field mirrors the flat awareness shape some peers read.
37672
- this.awareness.setLocalStateField('cursor', { x, y });
37673
38191
  this.publish();
37674
38192
  }
37675
38193
  setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
@@ -37689,14 +38207,14 @@ class LocalPresencePublisher {
37689
38207
  *
37690
38208
  * Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
37691
38209
  * construction so the service focuses on lifecycle + reactive state. The Yjs
37692
- * packages are dynamically imported so they are fully tree-shaken when
37693
- * collaboration is unused; each specifier is read from a variable so bundlers
37694
- * do not eagerly follow it (mirrors the historical `/* @vite-ignore *\/`
37695
- * pattern).
38210
+ * packages are dynamically imported (plain string-literal specifiers, matching
38211
+ * React/Vue) so they are fully tree-shaken when collaboration is unused, while
38212
+ * still resolving correctly under a consuming app's Vite dev server: an
38213
+ * `@vite-ignore`d or variable specifier skips Vite's dev-time bare-specifier
38214
+ * rewrite, which then fails to resolve natively in the browser.
37696
38215
  */
37697
38216
  async function createDoc() {
37698
- const yModule = 'yjs';
37699
- const Y = (await import(/* @vite-ignore */ yModule));
38217
+ const Y = (await import('yjs'));
37700
38218
  const doc = new Y.Doc();
37701
38219
  const factories = {
37702
38220
  createMap: () => new Y.Map(),
@@ -37707,10 +38225,9 @@ async function createDoc() {
37707
38225
  }
37708
38226
  /** Create a y-websocket transport bundle for `config`. */
37709
38227
  async function createWebsocketBundle(config) {
37710
- const wsSpecifier = 'y-websocket';
37711
38228
  const [{ doc, factories }, yws] = await Promise.all([
37712
38229
  createDoc(),
37713
- import(/* @vite-ignore */ wsSpecifier),
38230
+ import('y-websocket'),
37714
38231
  ]);
37715
38232
  const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
37716
38233
  return { doc, provider, awareness: provider.awareness, factories };
@@ -37722,10 +38239,9 @@ async function createWebsocketBundle(config) {
37722
38239
  * `authToken` is passed as the room `password`.
37723
38240
  */
37724
38241
  async function createWebrtcBundle(config) {
37725
- const rtcSpecifier = 'y-webrtc';
37726
38242
  const [{ doc, factories }, yrtc] = await Promise.all([
37727
38243
  createDoc(),
37728
- import(/* @vite-ignore */ rtcSpecifier),
38244
+ import('y-webrtc'),
37729
38245
  ]);
37730
38246
  const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
37731
38247
  signaling: config.signaling?.length ? config.signaling : undefined,
@@ -37950,9 +38466,14 @@ class CollaborationService {
37950
38466
  this.status.set('error');
37951
38467
  return;
37952
38468
  }
38469
+ // Falls back from a blank serverUrl the same way Vue's session layer
38470
+ // already does, so a bare CollaborationConfig behaves identically
38471
+ // regardless of which binding's session layer receives it directly (not
38472
+ // just via the Share/Broadcast dialogs, which already pre-resolve it).
38473
+ const transport = config.transport ?? resolveTransportForServerUrl(config.serverUrl);
37953
38474
  // Fail fast on mixed content (websocket only): an https page cannot open a
37954
38475
  // ws:// socket, so surface the error rather than hanging until the timeout.
37955
- if (config.transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
38476
+ if (transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
37956
38477
  this.status.set('error');
37957
38478
  return;
37958
38479
  }
@@ -37965,7 +38486,7 @@ class CollaborationService {
37965
38486
  this.activeRole.set(config.role);
37966
38487
  this.status.set('connecting');
37967
38488
  try {
37968
- const bundle = config.transport === 'webrtc'
38489
+ const bundle = transport === 'webrtc'
37969
38490
  ? await createWebrtcBundle(config)
37970
38491
  : await createWebsocketBundle(config);
37971
38492
  this.ydoc = bundle.doc;
@@ -37982,7 +38503,7 @@ class CollaborationService {
37982
38503
  this.localPresence.publish();
37983
38504
  this.awareness.on('change', this.refreshPresence);
37984
38505
  this.awareness.on('update', this.refreshPresence);
37985
- this.wireStatus(config);
38506
+ this.wireStatus(transport);
37986
38507
  this.syncGate.reset();
37987
38508
  this.wireSynced();
37988
38509
  this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
@@ -38001,18 +38522,23 @@ class CollaborationService {
38001
38522
  }
38002
38523
  }
38003
38524
  /** Wire the provider status events + (websocket-only) connection timeout. */
38004
- wireStatus(config) {
38525
+ wireStatus(transport) {
38005
38526
  const provider = this.provider;
38006
38527
  if (!provider) {
38007
38528
  return;
38008
38529
  }
38009
- if (config.transport === 'webrtc') {
38530
+ if (transport === 'webrtc') {
38010
38531
  // P2P: no server round-trip to wait on. Treat "created" as connected,
38011
38532
  // and reflect explicit disconnect events.
38012
38533
  this.status.set('connected');
38013
38534
  provider.on('status', (payload) => {
38014
38535
  if (payload.connected === false && this.active()) {
38015
38536
  this.status.set('disconnected');
38537
+ // Re-arm on (re)connect: without this, a peer that drops and
38538
+ // rejoins keeps the gate permanently open from the first
38539
+ // connection and can clobber the room with a stale local doc.
38540
+ this.syncGate.reset();
38541
+ this.syncGate.arm();
38016
38542
  }
38017
38543
  else if (payload.connected === true) {
38018
38544
  this.status.set('connected');
@@ -38027,6 +38553,8 @@ class CollaborationService {
38027
38553
  }
38028
38554
  else if (payload.status === 'disconnected' && this.active()) {
38029
38555
  this.status.set('disconnected');
38556
+ this.syncGate.reset();
38557
+ this.syncGate.arm();
38030
38558
  }
38031
38559
  });
38032
38560
  if (provider.wsconnected) {
@@ -40044,6 +40572,7 @@ const translationsEn = {
40044
40572
  'pptx.table.bandRowCycle': 'Row band size',
40045
40573
  'pptx.table.cell': 'Cell R{{row}} C{{col}}',
40046
40574
  'pptx.table.cellBorders': 'Cell borders',
40575
+ 'pptx.table.cellPadding': 'Cell padding',
40047
40576
  'pptx.table.color': 'Text',
40048
40577
  'pptx.table.columnWidths': 'Column widths',
40049
40578
  'pptx.table.even': 'Even',
@@ -40585,6 +41114,9 @@ const translationsEn = {
40585
41114
  'pptx.ribbon.strokeWidth': 'Stroke width',
40586
41115
  'pptx.ribbon.browseThemes': 'Browse Themes',
40587
41116
  'pptx.ribbon.browseThemesTitle': 'Browse and apply built-in themes',
41117
+ 'pptx.ribbon.theme.default': 'Default',
41118
+ 'pptx.ribbon.theme.light': 'Light (Vermilion)',
41119
+ 'pptx.ribbon.theme.dark': 'Dark (Vermilion)',
40588
41120
  'pptx.ribbon.editTheme': 'Edit Theme',
40589
41121
  'pptx.ribbon.editThemeTitle': 'Edit theme - theme editor not yet ported',
40590
41122
  'pptx.ribbon.slideSize': 'Slide Size',
@@ -41337,6 +41869,10 @@ const translationsEn = {
41337
41869
  'pptx.text.characterSpacingTight': 'Tight',
41338
41870
  'pptx.text.characterSpacingVeryLoose': 'Very Loose',
41339
41871
  'pptx.text.characterSpacingVeryTight': 'Very Tight',
41872
+ 'pptx.textAdvanced.autoFit': 'AutoFit',
41873
+ 'pptx.textAdvanced.autoFitNone': 'Do not autofit',
41874
+ 'pptx.textAdvanced.autoFitResize': 'Resize shape to fit text',
41875
+ 'pptx.textAdvanced.autoFitShrink': 'Shrink text on overflow',
41340
41876
  'pptx.textAdvanced.autoPlaceholder': 'auto',
41341
41877
  'pptx.textAdvanced.indent': 'Indent',
41342
41878
  'pptx.textAdvanced.indentMargin': 'Indent & Margin',
@@ -41350,6 +41886,7 @@ const translationsEn = {
41350
41886
  'pptx.textAdvanced.spaceBefore': 'Space Before',
41351
41887
  'pptx.textAdvanced.spacing': 'Spacing',
41352
41888
  'pptx.textAdvanced.vAlign': 'V-Align',
41889
+ 'pptx.textAdvanced.wrapText': 'Wrap text in shape',
41353
41890
  'pptx.textFormatting.bulleted': 'Bulleted',
41354
41891
  'pptx.textFormatting.numbered': 'Numbered',
41355
41892
  'pptx.textFormatting.paragraphAfter': 'Paragraph After',