返回 presentation-ai
slide-serializer.ts
根目录 / src / components / notebook / presentation / utils / slide-serializer.ts
1 import {
2 KEYS,
3 type Descendant,
4 type TColumnElement,
5 type TColumnGroupElement,
6 type TElement,
7 type TTableCellElement,
8 type TTableElement,
9 type TTableRowElement,
10 type TText,
11 } from "platejs";
12
13 import {
14 ANTV_INFOGRAPHIC,
15 AREA_CHART_ELEMENT,
16 BAR_CHART_ELEMENT,
17 BOX_PLOT_CHART_ELEMENT,
18 BUBBLE_CHART_ELEMENT,
19 CANDLESTICK_CHART_ELEMENT,
20 CHORD_CHART_ELEMENT,
21 CIRCULAR_GRID_GROUP,
22 COMPOSED_CHART_ELEMENT,
23 CONE_FUNNEL_CHART_ELEMENT,
24 CONNECTED_CIRCLES_GROUP,
25 CONTRIBUTOR_ELEMENT,
26 DONUT_CHART_ELEMENT,
27 FUNNEL_CHART_ELEMENT,
28 HEATMAP_CHART_ELEMENT,
29 HISTOGRAM_CHART_ELEMENT,
30 LABEL_ELEMENT,
31 LINE_CHART_ELEMENT,
32 LINEAR_GAUGE_ELEMENT,
33 NIGHTINGALE_CHART_ELEMENT,
34 OHLC_CHART_ELEMENT,
35 PIE_CHART_ELEMENT,
36 PRESENTATION_TITLE_ELEMENT,
37 PYRAMID_CHART_ELEMENT,
38 QUOTE_ELEMENT,
39 RADAR_CHART_ELEMENT,
40 RADIAL_BAR_CHART_ELEMENT,
41 RADIAL_COLUMN_CHART_ELEMENT,
42 RADIAL_GAUGE_ELEMENT,
43 RANGE_AREA_CHART_ELEMENT,
44 RANGE_BAR_CHART_ELEMENT,
45 SANKEY_CHART_ELEMENT,
46 SCATTER_CHART_ELEMENT,
47 SLOPE_GROUP,
48 SNAKE_GROUP,
49 STEPS_GROUP,
50 SUNBURST_CHART_ELEMENT,
51 TREEMAP_CHART_ELEMENT,
52 WATERFALL_CHART_ELEMENT,
53 type TContributorElement,
54 type TLabelElement,
55 type TPresentationTitleElement,
56 } from "../editor/lib";
57 import { type TAntvInfographicElement } from "../editor/plugins/antv-infographic-plugin";
58 import {
59 type TArrowListElement,
60 type TArrowListItemElement,
61 } from "../editor/plugins/arrow-plugin";
62 import {
63 type TBeforeAfterGroupElement,
64 type TBeforeAfterSideElement,
65 } from "../editor/plugins/before-after-plugin";
66 import {
67 type TBoxGroupElement,
68 type TBoxItemElement,
69 } from "../editor/plugins/box-plugin";
70 import {
71 type TBulletGroupElement,
72 type TBulletItemElement,
73 } from "../editor/plugins/bullet-plugin";
74 import { type TButtonElement } from "../editor/plugins/button-plugin";
75 import { type TChartNode } from "../editor/plugins/chart-plugin";
76 import {
77 type TCompareGroupElement,
78 type TCompareSideElement,
79 } from "../editor/plugins/compare-plugin";
80 import {
81 type TCycleGroupElement,
82 type TCycleItemElement,
83 } from "../editor/plugins/cycle-plugin";
84 import {
85 type TCircularGridGroupElement,
86 type TCircularGridItemElement,
87 type TConnectedCirclesGroupElement,
88 type TConnectedCirclesItemElement,
89 type TSlopeGroupElement,
90 type TSlopeItemElement,
91 type TSnakeGroupElement,
92 type TSnakeItemElement,
93 } from "../editor/plugins/diagram-components-plugin";
94 import {
95 type TIconListElement,
96 type TIconListItemElement,
97 } from "../editor/plugins/icon-list-plugin";
98 import { type TIconElement } from "../editor/plugins/icon-plugin";
99 import {
100 type TConsItemElement,
101 type TProsConsGroupElement,
102 type TProsItemElement,
103 } from "../editor/plugins/pros-cons-plugin";
104 import {
105 type TPyramidGroupElement,
106 type TPyramidItemElement,
107 } from "../editor/plugins/pyramid-plugin";
108 import { type TQuoteElement } from "../editor/plugins/quote-plugin";
109 import {
110 type TSequenceArrowGroupElement,
111 type TSequenceArrowItemElement,
112 } from "../editor/plugins/sequence-arrow-plugin";
113 import {
114 type TStairGroupElement,
115 type TStairItemElement,
116 } from "../editor/plugins/staircase-plugin";
117 import {
118 type TStatsGroupElement,
119 type TStatsItemElement,
120 } from "../editor/plugins/stats-plugin";
121 import {
122 type TStepsGroupElement,
123 type TStepsItemElement,
124 } from "../editor/plugins/steps-plugin";
125 import {
126 type TTimelineGroupElement,
127 type TTimelineItemElement,
128 } from "../editor/plugins/timeline-plugin";
129 import { type PlateNode, type PlateSlide, type RootImage } from "./parser";
130 import {
131 type HeadingElement,
132 type ImageElement,
133 type ParagraphElement,
134 } from "./types";
135
136 function parseColumnWidth(width: unknown): number | null {
137 if (width === undefined || width === null) return null;
138
139 const parsed = Number.parseFloat(String(width));
140
141 if (!Number.isFinite(parsed) || parsed <= 0) return null;
142
143 return Math.round(parsed * 100) / 100;
144 }
145
146 const CHART_ELEMENT_TO_XML_TYPE: Record<string, string> = {
147 [PIE_CHART_ELEMENT]: "pie",
148 [BAR_CHART_ELEMENT]: "bar",
149 [AREA_CHART_ELEMENT]: "area",
150 [RADAR_CHART_ELEMENT]: "radar",
151 [SCATTER_CHART_ELEMENT]: "scatter",
152 [LINE_CHART_ELEMENT]: "line",
153 [RADIAL_BAR_CHART_ELEMENT]: "radial-bar",
154 [COMPOSED_CHART_ELEMENT]: "composed",
155 [TREEMAP_CHART_ELEMENT]: "treemap",
156 [BUBBLE_CHART_ELEMENT]: "bubble",
157 [DONUT_CHART_ELEMENT]: "donut",
158 [HISTOGRAM_CHART_ELEMENT]: "histogram",
159 [HEATMAP_CHART_ELEMENT]: "heatmap",
160 [RANGE_BAR_CHART_ELEMENT]: "range-bar",
161 [RANGE_AREA_CHART_ELEMENT]: "range-area",
162 [WATERFALL_CHART_ELEMENT]: "waterfall",
163 [BOX_PLOT_CHART_ELEMENT]: "box-plot",
164 [CANDLESTICK_CHART_ELEMENT]: "candlestick",
165 [OHLC_CHART_ELEMENT]: "ohlc",
166 [NIGHTINGALE_CHART_ELEMENT]: "nightingale",
167 [RADIAL_COLUMN_CHART_ELEMENT]: "radial-column",
168 [SUNBURST_CHART_ELEMENT]: "sunburst",
169 [SANKEY_CHART_ELEMENT]: "sankey",
170 [CHORD_CHART_ELEMENT]: "chord",
171 [FUNNEL_CHART_ELEMENT]: "funnel",
172 [CONE_FUNNEL_CHART_ELEMENT]: "cone-funnel",
173 [PYRAMID_CHART_ELEMENT]: "pyramid",
174 [RADIAL_GAUGE_ELEMENT]: "radial-gauge",
175 [LINEAR_GAUGE_ELEMENT]: "linear-gauge",
176 };
177
178 const CHART_OPTION_EXCLUDED_KEYS = new Set([
179 "type",
180 "children",
181 "data",
182 "chartType",
183 "charttype",
184 ]);
185
186 const BASIC_BLOCK_ATTRIBUTE_KEYS = new Set([
187 "alignment",
188 "backgroundColor",
189 "color",
190 "textColor",
191 ]);
192
193 type SlideSerializerMode = "content" | "layoutPrompt";
194
195 export interface SlideSerializerOptions {
196 mode?: SlideSerializerMode;
197 }
198
199 const LAYOUT_PROMPT_PLACEHOLDERS = {
200 author: "Name or role if relevant",
201 body: "Write slide-specific supporting text.",
202 centerText: "short slide-specific center label",
203 heading: "Write a slide-specific heading.",
204 icon: "relevant-keyword",
205 imageQuery: "write an English image query for the slide-specific visual",
206 infographic:
207 "Describe the exact slide-specific visual: labels, entities, sequence, relationships, values, orientation, and takeaway.",
208 itemHeading: "Write a concise item heading.",
209 itemLabel: "Write a concise slide-specific item label.",
210 listItem: "Write a concise slide-specific list item.",
211 quote: "Write a slide-specific quote or testimonial.",
212 stat: "slide-specific metric",
213 title: "Write a slide-specific title.",
214 } as const;
215
216 const LAYOUT_PROMPT_CHART_DATA = `| label | value |
217 | --- | --- |
218 | Slide-specific category | numeric value |
219 | Slide-specific category | numeric value |`;
220
221 function getChartXmlType(elementType: string): string {
222 return (
223 CHART_ELEMENT_TO_XML_TYPE[elementType] ?? elementType.replace(/^chart-/, "")
224 );
225 }
226
227 function isPrimitiveXmlValue(
228 value: unknown,
229 ): value is string | number | boolean {
230 return (
231 typeof value === "string" ||
232 typeof value === "number" ||
233 typeof value === "boolean"
234 );
235 }
236
237 function stringifyJson(value: unknown): string | null {
238 if (value === undefined) return null;
239
240 try {
241 return JSON.stringify(value);
242 } catch {
243 return null;
244 }
245 }
246
247 function isRecord(value: unknown): value is Record<string, unknown> {
248 return typeof value === "object" && value !== null && !Array.isArray(value);
249 }
250
251 function isMarkdownChartDataValue(
252 value: unknown,
253 ): value is string | number | boolean | null | undefined {
254 return (
255 value === null ||
256 value === undefined ||
257 typeof value === "string" ||
258 typeof value === "number" ||
259 typeof value === "boolean"
260 );
261 }
262
263 /**
264 * Class to serialize PlateSlide objects back to XML format
265 */
266 class SlideSerializer {
267 private readonly mode: SlideSerializerMode;
268
269 public constructor(options: SlideSerializerOptions = {}) {
270 this.mode = options.mode ?? "content";
271 }
272
273 private get isLayoutPrompt(): boolean {
274 return this.mode === "layoutPrompt";
275 }
276
277 /**
278 * Serialize an array of PlateSlide objects to XML string
279 * @param slides Array of PlateSlide objects
280 * @param includePresentationWrapper Whether to wrap output in PRESENTATION tag
281 * @returns XML string
282 */
283 public serializeSlides(
284 slides: PlateSlide[],
285 includePresentationWrapper = true,
286 ): string {
287 const sections = slides.map((slide) => this.serializeSlide(slide));
288
289 if (includePresentationWrapper) {
290 return `<PRESENTATION>\n${sections.join("\n")}\n</PRESENTATION>`;
291 }
292
293 return sections.join("\n");
294 }
295
296 /**
297 * Serialize a single PlateSlide to XML SECTION
298 */
299 private serializeSlide(slide: PlateSlide): string {
300 const attributes: Record<string, string> = {};
301
302 // Add layout type if present
303 if (slide.layoutType) {
304 attributes.layout = slide.layoutType;
305 }
306
307 // Add alignment if present and not default
308 if (slide.alignment && slide.alignment !== "center") {
309 attributes.alignment = slide.alignment;
310 }
311
312 // Add bgColor if present
313 if (slide.bgColor) {
314 attributes.bgColor = slide.bgColor;
315 }
316
317 // Add width if present
318 if (slide.width) {
319 attributes.width = slide.width;
320 }
321
322 if (slide.id && !this.isLayoutPrompt) {
323 attributes.id = slide.id;
324 }
325
326 if (slide.isImageSlide) {
327 attributes.isImageSlide = "true";
328 }
329
330 const attrString = this.serializeAttributes(attributes);
331 const openTag = `<SECTION${attrString}>`;
332
333 const contentParts: string[] = [];
334
335 // Serialize content nodes
336 if (!slide.isImageSlide) {
337 for (const node of slide.content) {
338 const serialized = this.serializeNode(node, 1);
339 if (serialized) {
340 contentParts.push(serialized);
341 }
342 }
343 }
344
345 if (slide.rootImage) {
346 contentParts.push(this.serializeRootImage(slide.rootImage, 1));
347 }
348
349 return `${openTag}\n${contentParts.join("\n")}\n</SECTION>`;
350 }
351
352 /**
353 * Serialize attributes object to string
354 */
355 private serializeAttributes(attributes: Record<string, string>): string {
356 const entries = Object.entries(attributes);
357 if (entries.length === 0) return "";
358
359 return (
360 " " +
361 entries
362 .map(([key, value]) => `${key}="${this.escapeXml(value)}"`)
363 .join(" ")
364 );
365 }
366
367 private getLayoutPromptAttributeValue(key: string): string | undefined {
368 if (!this.isLayoutPrompt) {
369 return undefined;
370 }
371
372 switch (key.toLowerCase()) {
373 case "author":
374 return LAYOUT_PROMPT_PLACEHOLDERS.author;
375 case "centertext":
376 return LAYOUT_PROMPT_PLACEHOLDERS.centerText;
377 case "icon":
378 return LAYOUT_PROMPT_PLACEHOLDERS.icon;
379 case "prompt":
380 case "query":
381 return LAYOUT_PROMPT_PLACEHOLDERS.imageQuery;
382 case "stat":
383 return LAYOUT_PROMPT_PLACEHOLDERS.stat;
384 default:
385 return undefined;
386 }
387 }
388
389 private setAttribute(
390 attributes: Record<string, string>,
391 key: string,
392 value: unknown,
393 ): void {
394 if (!isPrimitiveXmlValue(value)) {
395 return;
396 }
397
398 if (this.isLayoutPrompt && (key === "id" || key === "url")) {
399 return;
400 }
401
402 attributes[key] = this.getLayoutPromptAttributeValue(key) ?? String(value);
403 }
404
405 private getHeadingLayoutPromptPlaceholder(tag: string): string {
406 if (tag === "H1" || tag === "H2") {
407 return LAYOUT_PROMPT_PLACEHOLDERS.heading;
408 }
409
410 return LAYOUT_PROMPT_PLACEHOLDERS.itemHeading;
411 }
412
413 /**
414 * Escape special XML characters
415 */
416 private escapeXml(text: string): string {
417 return text
418 ?.replace(/&/g, "&amp;")
419 ?.replace(/</g, "&lt;")
420 ?.replace(/>/g, "&gt;")
421 ?.replace(/"/g, "&quot;")
422 ?.replace(/'/g, "&apos;");
423 }
424
425 private serializeJsonChild(
426 tagName: string,
427 value: unknown,
428 indent: number,
429 ): string | null {
430 const json = stringifyJson(value);
431 if (!json) return null;
432
433 const indentStr = " ".repeat(indent);
434 return `${indentStr}<${tagName}>${this.escapeXml(json)}</${tagName}>`;
435 }
436
437 private serializeMarkdownCell(value: unknown): string {
438 return String(value ?? "")
439 .replace(/\r?\n/g, " ")
440 .replace(/\|/g, "\\|");
441 }
442
443 private serializeMarkdownChartData(
444 value: unknown,
445 indent: number,
446 ): string | null {
447 if (!Array.isArray(value) || value.length === 0) {
448 return null;
449 }
450
451 const rows = value.filter(isRecord);
452 if (rows.length !== value.length) {
453 return null;
454 }
455
456 const keys = Array.from(
457 new Set(rows.flatMap((row) => Object.keys(row))),
458 ).filter((key) => rows.every((row) => isMarkdownChartDataValue(row[key])));
459
460 if (keys.length === 0) {
461 return null;
462 }
463
464 const indentStr = " ".repeat(indent);
465 const header = `| ${keys.map((key) => this.serializeMarkdownCell(key)).join(" | ")} |`;
466 const separator = `| ${keys.map(() => "---").join(" | ")} |`;
467 const tableRows = rows.map(
468 (row) =>
469 `| ${keys.map((key) => this.serializeMarkdownCell(row[key])).join(" | ")} |`,
470 );
471
472 return [header, separator, ...tableRows]
473 .map((line) => `${indentStr}${this.escapeXml(line)}`)
474 .join("\n");
475 }
476
477 private serializeChartDataChild(
478 value: unknown,
479 indent: number,
480 ): string | null {
481 return this.serializeMarkdownChartData(value, indent);
482 }
483
484 private serializeRootImage(image: RootImage, indent: number): string {
485 const indentStr = " ".repeat(indent);
486 const attributes: Record<string, string> = {};
487
488 this.setAttribute(attributes, "query", image.query);
489 this.setAttribute(attributes, "url", image.url);
490 this.setAttribute(attributes, "embedType", image.embedType);
491 this.setAttribute(attributes, "imageSource", image.imageSource);
492 this.setAttribute(
493 attributes,
494 "stockImageProvider",
495 image.stockImageProvider,
496 );
497 this.setAttribute(attributes, "layoutType", image.layoutType);
498 this.setAttribute(attributes, "width", image.size?.w);
499 this.setAttribute(attributes, "height", image.size?.h);
500 this.setAttribute(
501 attributes,
502 "paletteDropMutable",
503 image.paletteDropMutable,
504 );
505 this.setAttribute(
506 attributes,
507 "charttype",
508 image.chartType ? getChartXmlType(image.chartType) : undefined,
509 );
510
511 const childParts = this.isLayoutPrompt
512 ? []
513 : [
514 this.serializeJsonChild("CROP", image.cropSettings, indent + 1),
515 this.serializeChartDataChild(image.chartData, indent + 1),
516 this.serializeJsonChild("OPTIONS", image.chartOptions, indent + 1),
517 ].filter((part): part is string => Boolean(part));
518
519 if (childParts.length === 0) {
520 return `${indentStr}<IMG${this.serializeAttributes(attributes)} />`;
521 }
522
523 return `${indentStr}<IMG${this.serializeAttributes(attributes)}>\n${childParts.join("\n")}\n${indentStr}</IMG>`;
524 }
525
526 private serializeBasicBlockAttributes(
527 node: Record<string, unknown>,
528 extraKeys: readonly string[] = [],
529 ): Record<string, string> {
530 const attributes: Record<string, string> = {};
531 const allowedKeys = new Set([...BASIC_BLOCK_ATTRIBUTE_KEYS, ...extraKeys]);
532
533 for (const key of allowedKeys) {
534 const value = node[key];
535 if (isPrimitiveXmlValue(value)) {
536 this.setAttribute(attributes, key, value);
537 }
538 }
539
540 return attributes;
541 }
542
543 /**
544 * Serialize a PlateNode to XML
545 */
546 private serializeNode(node: PlateNode, indent = 0): string | null {
547 if (!node || typeof node !== "object") return null;
548
549 const nodeType = (node as { type?: string }).type;
550
551 if (!nodeType) return null;
552
553 switch (nodeType) {
554 case "h1":
555 case "h2":
556 case "h3":
557 case "h4":
558 case "h5":
559 case "h6":
560 return this.serializeHeading(
561 node as HeadingElement,
562 nodeType.toUpperCase(),
563 indent,
564 );
565
566 case PRESENTATION_TITLE_ELEMENT:
567 return this.serializePresentationTitle(
568 node as TPresentationTitleElement,
569 indent,
570 );
571
572 case LABEL_ELEMENT:
573 return this.serializeLabel(node as TLabelElement, indent);
574
575 case CONTRIBUTOR_ELEMENT:
576 return this.serializeContributor(node as TContributorElement, indent);
577
578 case KEYS.blockquote:
579 return this.serializeBlockquote(node as TElement, indent);
580
581 case KEYS.callout:
582 return this.serializeCallout(node as TElement, indent);
583
584 case KEYS.codeBlock:
585 return this.serializeCodeBlock(node as TElement, indent);
586
587 case "p":
588 return this.serializeParagraph(node as ParagraphElement, indent);
589
590 case "img":
591 return this.serializeImage(node as ImageElement, indent);
592
593 case "column_group":
594 return this.serializeColumns(node as TColumnGroupElement, indent);
595
596 case "bullets":
597 return this.serializeBullets(node as TBulletGroupElement, indent);
598
599 case "icons":
600 return this.serializeIcons(node as TIconListElement, indent);
601
602 case "cycle":
603 return this.serializeCycle(node as TCycleGroupElement, indent);
604
605 case STEPS_GROUP:
606 return this.serializeSteps(node as TStepsGroupElement, indent);
607
608 case "staircase":
609 return this.serializeStaircase(node as TStairGroupElement, indent);
610
611 case "arrows":
612 return this.serializeArrows(node as TArrowListElement, indent);
613
614 case "pyramid":
615 return this.serializePyramid(node as TPyramidGroupElement, indent);
616
617 case "timeline":
618 return this.serializeTimeline(node as TTimelineGroupElement, indent);
619
620 case SLOPE_GROUP:
621 return this.serializeSlope(node as TSlopeGroupElement, indent);
622
623 case SNAKE_GROUP:
624 return this.serializeSnake(node as TSnakeGroupElement, indent);
625
626 case CONNECTED_CIRCLES_GROUP:
627 return this.serializeConnectedCircles(
628 node as TConnectedCirclesGroupElement,
629 indent,
630 );
631
632 case CIRCULAR_GRID_GROUP:
633 return this.serializeCircularGrid(
634 node as TCircularGridGroupElement,
635 indent,
636 );
637
638 case "boxes":
639 return this.serializeBoxes(node as TBoxGroupElement, indent);
640
641 case "compare":
642 return this.serializeCompare(node as TCompareGroupElement, indent);
643
644 case "before-after":
645 return this.serializeBeforeAfter(
646 node as TBeforeAfterGroupElement,
647 indent,
648 );
649
650 case "pros-cons":
651 return this.serializeProsCons(node as TProsConsGroupElement, indent);
652
653 case "arrow-vertical":
654 return this.serializeArrowVertical(
655 node as TSequenceArrowGroupElement,
656 indent,
657 );
658
659 case "table":
660 return this.serializeTable(node as TTableElement, indent);
661
662 case "button":
663 return this.serializeButton(node as TButtonElement, indent);
664
665 case "stats":
666 return this.serializeStats(node as TStatsGroupElement, indent);
667
668 case PIE_CHART_ELEMENT:
669 case BAR_CHART_ELEMENT:
670 case AREA_CHART_ELEMENT:
671 case RADAR_CHART_ELEMENT:
672 case SCATTER_CHART_ELEMENT:
673 case LINE_CHART_ELEMENT:
674 case RADIAL_BAR_CHART_ELEMENT:
675 case COMPOSED_CHART_ELEMENT:
676 case TREEMAP_CHART_ELEMENT:
677 case BUBBLE_CHART_ELEMENT:
678 case DONUT_CHART_ELEMENT:
679 case HISTOGRAM_CHART_ELEMENT:
680 case HEATMAP_CHART_ELEMENT:
681 case RANGE_BAR_CHART_ELEMENT:
682 case RANGE_AREA_CHART_ELEMENT:
683 case WATERFALL_CHART_ELEMENT:
684 case BOX_PLOT_CHART_ELEMENT:
685 case CANDLESTICK_CHART_ELEMENT:
686 case OHLC_CHART_ELEMENT:
687 case NIGHTINGALE_CHART_ELEMENT:
688 case RADIAL_COLUMN_CHART_ELEMENT:
689 case SUNBURST_CHART_ELEMENT:
690 case SANKEY_CHART_ELEMENT:
691 case CHORD_CHART_ELEMENT:
692 case FUNNEL_CHART_ELEMENT:
693 case CONE_FUNNEL_CHART_ELEMENT:
694 case PYRAMID_CHART_ELEMENT:
695 case RADIAL_GAUGE_ELEMENT:
696 case LINEAR_GAUGE_ELEMENT:
697 return this.serializeChart(node as TChartNode, nodeType, indent);
698
699 case QUOTE_ELEMENT:
700 return this.serializeQuote(node as TQuoteElement, indent);
701
702 case ANTV_INFOGRAPHIC:
703 return this.serializeInfographic(
704 node as TAntvInfographicElement,
705 indent,
706 );
707
708 default:
709 console.warn(`Unknown node type: ${nodeType}`);
710 return null;
711 }
712 }
713
714 /**
715 * Serialize heading element
716 */
717 private serializeHeading(
718 node: HeadingElement,
719 tag: string,
720 indent: number,
721 ): string {
722 const indentStr = " ".repeat(indent);
723 const content = this.isLayoutPrompt
724 ? this.getHeadingLayoutPromptPlaceholder(tag)
725 : this.serializeDescendants(node.children);
726 return `${indentStr}<${tag}>${content}</${tag}>`;
727 }
728
729 private serializePresentationTitle(
730 node: TPresentationTitleElement,
731 indent: number,
732 ): string {
733 const indentStr = " ".repeat(indent);
734 const attributes = this.serializeBasicBlockAttributes(
735 node as unknown as Record<string, unknown>,
736 ["variant"],
737 );
738 const content = this.isLayoutPrompt
739 ? LAYOUT_PROMPT_PLACEHOLDERS.title
740 : this.serializeDescendants(node.children as Descendant[]);
741
742 return `${indentStr}<TITLE${this.serializeAttributes(attributes)}>${content}</TITLE>`;
743 }
744
745 private serializeLabel(node: TLabelElement, indent: number): string {
746 const indentStr = " ".repeat(indent);
747 const attributes = this.serializeBasicBlockAttributes(
748 node as unknown as Record<string, unknown>,
749 );
750 const content = this.isLayoutPrompt
751 ? LAYOUT_PROMPT_PLACEHOLDERS.itemLabel
752 : this.serializeDescendants(node.children as Descendant[]);
753
754 return `${indentStr}<LABEL${this.serializeAttributes(attributes)}>${content}</LABEL>`;
755 }
756
757 private serializeContributor(
758 _node: TContributorElement,
759 indent: number,
760 ): string {
761 const indentStr = " ".repeat(indent);
762
763 return `${indentStr}<CONTRIBUTOR />`;
764 }
765
766 private serializeBlockquote(node: TElement, indent: number): string {
767 const indentStr = " ".repeat(indent);
768 const attributes = this.serializeBasicBlockAttributes(
769 node as unknown as Record<string, unknown>,
770 ["author"],
771 );
772 const content = this.isLayoutPrompt
773 ? LAYOUT_PROMPT_PLACEHOLDERS.quote
774 : this.serializeDescendants(node.children as Descendant[]);
775
776 return `${indentStr}<BLOCKQUOTE${this.serializeAttributes(attributes)}>${content}</BLOCKQUOTE>`;
777 }
778
779 private serializeCallout(node: TElement, indent: number): string {
780 const indentStr = " ".repeat(indent);
781 const attributes = this.serializeBasicBlockAttributes(
782 node as unknown as Record<string, unknown>,
783 ["icon", "variant"],
784 );
785 const content = this.isLayoutPrompt
786 ? LAYOUT_PROMPT_PLACEHOLDERS.body
787 : (node.children as Descendant[])
788 .map((child) => this.serializeDescendant(child, indent + 1))
789 .filter(Boolean)
790 .join("\n");
791
792 return `${indentStr}<CALLOUT${this.serializeAttributes(attributes)}>\n${content}\n${indentStr}</CALLOUT>`;
793 }
794
795 private serializeCodeBlock(node: TElement, indent: number): string {
796 const indentStr = " ".repeat(indent);
797 const nodeRecord = node as unknown as Record<string, unknown>;
798 const attributes: Record<string, string> = {};
799 const language = nodeRecord.lang ?? nodeRecord.language;
800
801 if (isPrimitiveXmlValue(language)) {
802 attributes.language = String(language);
803 }
804
805 const lines = (node.children as Descendant[]).map((child) => {
806 if (
807 typeof child === "object" &&
808 child !== null &&
809 "children" in child &&
810 Array.isArray(child.children)
811 ) {
812 return this.serializeDescendants(child.children as Descendant[]);
813 }
814
815 return this.serializeDescendant(child, 0) ?? "";
816 });
817
818 return `${indentStr}<CODE${this.serializeAttributes(attributes)}>${lines.join("\n")}</CODE>`;
819 }
820
821 /**
822 * Serialize paragraph element
823 */
824 private serializeParagraph(node: ParagraphElement, indent: number): string {
825 const indentStr = " ".repeat(indent);
826
827 // Check if this is a list item (has indent and listStyleType)
828 const nodeWithList = node as ParagraphElement & {
829 indent?: number;
830 listStyleType?: string;
831 };
832
833 if (nodeWithList.indent && nodeWithList.listStyleType) {
834 const content = this.isLayoutPrompt
835 ? LAYOUT_PROMPT_PLACEHOLDERS.listItem
836 : this.serializeDescendants(node.children);
837 return `${indentStr}<LI>${content}</LI>`;
838 }
839
840 const content = this.isLayoutPrompt
841 ? LAYOUT_PROMPT_PLACEHOLDERS.body
842 : this.serializeDescendants(node.children);
843 return `${indentStr}<P>${content}</P>`;
844 }
845
846 /**
847 * Serialize image element
848 */
849 private serializeImage(node: ImageElement, indent: number): string {
850 const indentStr = " ".repeat(indent);
851 const attributes: Record<string, string> = {};
852
853 this.setAttribute(attributes, "query", node.query);
854 this.setAttribute(attributes, "url", node.url);
855
856 // Add any additional properties as attributes
857 const nodeKeys = Object.keys(node) as (keyof ImageElement)[];
858 for (const key of nodeKeys) {
859 if (
860 key !== "type" &&
861 key !== "children" &&
862 key !== "query" &&
863 key !== "url"
864 ) {
865 const value = node[key];
866 if (value !== undefined && value !== null) {
867 this.setAttribute(attributes, key, value);
868 }
869 }
870 }
871
872 return `${indentStr}<IMG${this.serializeAttributes(attributes)} />`;
873 }
874
875 /**
876 * Serialize columns layout
877 */
878 private serializeColumns(node: TColumnGroupElement, indent: number): string {
879 const indentStr = " ".repeat(indent);
880 const childIndent = indent + 1;
881 const childIndentStr = " ".repeat(childIndent);
882
883 const columns = (node.children as TColumnElement[])
884 .map((col) => {
885 const content = (col.children as Descendant[])
886 .map((child) => this.serializeDescendant(child, childIndent + 1))
887 .filter(Boolean)
888 .join("\n");
889
890 const attrs: Record<string, string> = {};
891 const width = parseColumnWidth(col.width);
892 if (width !== null) {
893 attrs.width = String(width);
894 }
895
896 return `${childIndentStr}<DIV${this.serializeAttributes(attrs)}>\n${content}\n${childIndentStr}</DIV>`;
897 })
898 .join("\n");
899
900 return `${indentStr}<COLUMNS>\n${columns}\n${indentStr}</COLUMNS>`;
901 }
902
903 /**
904 * Serialize bullets layout
905 */
906 private serializeBullets(node: TBulletGroupElement, indent: number): string {
907 const indentStr = " ".repeat(indent);
908 const childIndent = indent + 1;
909 const childIndentStr = " ".repeat(childIndent);
910 const nodeAttrs: Record<string, string> = {};
911
912 if (node.bulletType) {
913 nodeAttrs.bulletType = node.bulletType;
914 }
915 if (node.columnSize) {
916 nodeAttrs.columnSize = node.columnSize;
917 }
918 if (node.alignment) {
919 nodeAttrs.alignment = node.alignment;
920 }
921
922 const bullets = (node.children as TBulletItemElement[])
923 .map((bullet) => {
924 const content = (bullet.children as Descendant[])
925 .map((child) => this.serializeDescendant(child, childIndent + 1))
926 .filter(Boolean)
927 .join("\n");
928 const itemAttrs: Record<string, string> = {};
929
930 this.setAttribute(itemAttrs, "icon", bullet.icon);
931
932 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
933 })
934 .join("\n");
935
936 return `${indentStr}<BULLETS${this.serializeAttributes(nodeAttrs)}>\n${bullets}\n${indentStr}</BULLETS>`;
937 }
938
939 /**
940 * Serialize icons layout
941 */
942 private serializeIcons(node: TIconListElement, indent: number): string {
943 const indentStr = " ".repeat(indent);
944 const childIndent = indent + 1;
945 const childIndentStr = " ".repeat(childIndent);
946 const nodeAttrs: Record<string, string> = {};
947
948 if (node.variant) nodeAttrs.variant = node.variant;
949 if (node.orientation) nodeAttrs.orientation = node.orientation;
950 if (node.columnSize) nodeAttrs.columnSize = node.columnSize;
951 if (node.alignment) nodeAttrs.alignment = node.alignment;
952 if (typeof node.mediaSize === "number" && Number.isFinite(node.mediaSize)) {
953 nodeAttrs.mediaSize = String(node.mediaSize);
954 }
955
956 const icons = (node.children as TIconListItemElement[])
957 .map((item) => {
958 const itemWithIcon = item as TIconListItemElement & { icon?: string };
959 const itemIcon =
960 itemWithIcon.icon ??
961 this.getLegacyIconValue(item.children as Descendant[]);
962 const parts: string[] = [];
963
964 for (const child of item.children as Descendant[]) {
965 if (
966 typeof child === "object" &&
967 "type" in child &&
968 child.type === "icon"
969 ) {
970 } else {
971 const serialized = this.serializeDescendant(child, childIndent + 1);
972 if (serialized) parts.push(serialized);
973 }
974 }
975
976 const itemAttrs: Record<string, string> = {};
977
978 if (node.variant === "image") {
979 const prompt = item.prompt ?? item.query;
980
981 this.setAttribute(itemAttrs, "prompt", prompt);
982 this.setAttribute(itemAttrs, "url", item.url);
983 this.setAttribute(itemAttrs, "imageSource", item.imageSource);
984 this.setAttribute(
985 itemAttrs,
986 "stockImageProvider",
987 item.stockImageProvider,
988 );
989 } else if (itemIcon) {
990 this.setAttribute(itemAttrs, "icon", itemIcon);
991 }
992
993 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${parts.join("\n")}\n${childIndentStr}</DIV>`;
994 })
995 .join("\n");
996
997 return `${indentStr}<ICONS${this.serializeAttributes(nodeAttrs)}>\n${icons}\n${indentStr}</ICONS>`;
998 }
999
1000 private getLegacyIconValue(children: Descendant[]): string | undefined {
1001 for (const child of children) {
1002 if (
1003 typeof child === "object" &&
1004 "type" in child &&
1005 child.type === "icon"
1006 ) {
1007 const iconNode = child as TIconElement;
1008 return iconNode.name || iconNode.query || undefined;
1009 }
1010 }
1011
1012 return undefined;
1013 }
1014
1015 /**
1016 * Serialize cycle layout
1017 */
1018 private serializeCycle(node: TCycleGroupElement, indent: number): string {
1019 const indentStr = " ".repeat(indent);
1020 const childIndent = indent + 1;
1021 const childIndentStr = " ".repeat(childIndent);
1022
1023 const items = (node.children as TCycleItemElement[])
1024 .map((item) => {
1025 const content = (item.children as Descendant[])
1026 .map((child) => this.serializeDescendant(child, childIndent + 1))
1027 .filter(Boolean)
1028 .join("\n");
1029 const itemAttrs: Record<string, string> = {};
1030
1031 this.setAttribute(itemAttrs, "icon", item.icon);
1032
1033 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1034 })
1035 .join("\n");
1036
1037 return `${indentStr}<CYCLE>\n${items}\n${indentStr}</CYCLE>`;
1038 }
1039
1040 private serializeSteps(node: TStepsGroupElement, indent: number): string {
1041 const attrs: Record<string, string> = {};
1042
1043 if (node.orientation) {
1044 attrs.orientation = node.orientation;
1045 }
1046 if (node.variant) {
1047 attrs.variant = node.variant;
1048 }
1049 if (node.columns !== undefined) {
1050 attrs.columns = String(node.columns);
1051 }
1052 if (node.columnSize) {
1053 attrs.columnSize = node.columnSize;
1054 }
1055 if (typeof node.color === "string") {
1056 attrs.color = node.color;
1057 }
1058
1059 return this.serializeDivItemGroup<TStepsItemElement>({
1060 attrs,
1061 indent,
1062 items: node.children as TStepsItemElement[],
1063 tagName: "STEPS",
1064 });
1065 }
1066
1067 private serializeSlope(node: TSlopeGroupElement, indent: number): string {
1068 const attrs: Record<string, string> = {};
1069
1070 if (node.alignment) {
1071 attrs.alignment = node.alignment;
1072 }
1073
1074 return this.serializeDivItemGroup<TSlopeItemElement>({
1075 attrs,
1076 indent,
1077 items: node.children as TSlopeItemElement[],
1078 tagName: "SLOPE",
1079 });
1080 }
1081
1082 private serializeSnake(node: TSnakeGroupElement, indent: number): string {
1083 const attrs: Record<string, string> = {};
1084
1085 if (node.alignment) {
1086 attrs.alignment = node.alignment;
1087 }
1088
1089 return this.serializeDivItemGroup<TSnakeItemElement>({
1090 attrs,
1091 indent,
1092 items: node.children as TSnakeItemElement[],
1093 tagName: "SNAKE",
1094 });
1095 }
1096
1097 private serializeConnectedCircles(
1098 node: TConnectedCirclesGroupElement,
1099 indent: number,
1100 ): string {
1101 const attrs: Record<string, string> = {};
1102
1103 if (node.alignment) {
1104 attrs.alignment = node.alignment;
1105 }
1106
1107 return this.serializeDivItemGroup<TConnectedCirclesItemElement>({
1108 attrs,
1109 indent,
1110 items: node.children as TConnectedCirclesItemElement[],
1111 tagName: "CONNECTED-CIRCLES",
1112 });
1113 }
1114
1115 private serializeCircularGrid(
1116 node: TCircularGridGroupElement,
1117 indent: number,
1118 ): string {
1119 const attrs: Record<string, string> = {};
1120
1121 if (node.alignment) {
1122 attrs.alignment = node.alignment;
1123 }
1124 this.setAttribute(attrs, "centerText", node.centerText);
1125
1126 return this.serializeDivItemGroup<TCircularGridItemElement>({
1127 attrs,
1128 indent,
1129 items: node.children as TCircularGridItemElement[],
1130 tagName: "CIRCULAR-GRID",
1131 });
1132 }
1133
1134 private serializeDivItemGroup<TItem extends TElement & { icon?: string }>({
1135 attrs,
1136 indent,
1137 items,
1138 tagName,
1139 }: {
1140 attrs?: Record<string, string>;
1141 indent: number;
1142 items: TItem[];
1143 tagName: string;
1144 }): string {
1145 const indentStr = " ".repeat(indent);
1146 const childIndent = indent + 1;
1147 const childIndentStr = " ".repeat(childIndent);
1148
1149 const serializedItems = items
1150 .map((item) => {
1151 const content = (item.children as Descendant[])
1152 .map((child) => this.serializeDescendant(child, childIndent + 1))
1153 .filter(Boolean)
1154 .join("\n");
1155 const itemAttrs: Record<string, string> = {};
1156
1157 this.setAttribute(itemAttrs, "icon", item.icon);
1158
1159 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1160 })
1161 .join("\n");
1162
1163 return `${indentStr}<${tagName}${this.serializeAttributes(attrs ?? {})}>\n${serializedItems}\n${indentStr}</${tagName}>`;
1164 }
1165
1166 /**
1167 * Serialize staircase layout
1168 */
1169 private serializeStaircase(node: TStairGroupElement, indent: number): string {
1170 const indentStr = " ".repeat(indent);
1171 const childIndent = indent + 1;
1172 const childIndentStr = " ".repeat(childIndent);
1173
1174 const items = (node.children as TStairItemElement[])
1175 .map((item) => {
1176 const content = (item.children as Descendant[])
1177 .map((child) => this.serializeDescendant(child, childIndent + 1))
1178 .filter(Boolean)
1179 .join("\n");
1180 const itemAttrs: Record<string, string> = {};
1181
1182 this.setAttribute(itemAttrs, "icon", item.icon);
1183
1184 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1185 })
1186 .join("\n");
1187
1188 return `${indentStr}<STAIRCASE>\n${items}\n${indentStr}</STAIRCASE>`;
1189 }
1190
1191 /**
1192 * Serialize arrows layout
1193 */
1194 private serializeArrows(node: TArrowListElement, indent: number): string {
1195 const indentStr = " ".repeat(indent);
1196 const childIndent = indent + 1;
1197 const childIndentStr = " ".repeat(childIndent);
1198
1199 const items = (node.children as TArrowListItemElement[])
1200 .map((item) => {
1201 const content = (item.children as Descendant[])
1202 .map((child) => this.serializeDescendant(child, childIndent + 1))
1203 .filter(Boolean)
1204 .join("\n");
1205 const itemAttrs: Record<string, string> = {};
1206
1207 this.setAttribute(itemAttrs, "icon", item.icon);
1208
1209 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1210 })
1211 .join("\n");
1212
1213 return `${indentStr}<ARROWS>\n${items}\n${indentStr}</ARROWS>`;
1214 }
1215
1216 /**
1217 * Serialize pyramid layout
1218 */
1219 private serializePyramid(node: TPyramidGroupElement, indent: number): string {
1220 const indentStr = " ".repeat(indent);
1221 const childIndent = indent + 1;
1222 const childIndentStr = " ".repeat(childIndent);
1223
1224 const items = (node.children as TPyramidItemElement[])
1225 .map((item) => {
1226 const content = (item.children as Descendant[])
1227 .map((child) => this.serializeDescendant(child, childIndent + 1))
1228 .filter(Boolean)
1229 .join("\n");
1230 const itemAttrs: Record<string, string> = {};
1231
1232 this.setAttribute(itemAttrs, "icon", item.icon);
1233
1234 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1235 })
1236 .join("\n");
1237
1238 return `${indentStr}<PYRAMID>\n${items}\n${indentStr}</PYRAMID>`;
1239 }
1240
1241 /**
1242 * Serialize timeline layout
1243 */
1244 private serializeTimeline(
1245 node: TTimelineGroupElement,
1246 indent: number,
1247 ): string {
1248 const indentStr = " ".repeat(indent);
1249 const childIndent = indent + 1;
1250 const childIndentStr = " ".repeat(childIndent);
1251 const attrs: Record<string, string> = {};
1252
1253 if (node.orientation) {
1254 attrs.orientation = node.orientation;
1255 }
1256
1257 if (node.sidedness) {
1258 attrs.sidedness = node.sidedness;
1259 }
1260
1261 if (node.numbered !== undefined) {
1262 attrs.numbered = String(node.numbered);
1263 }
1264
1265 if (node.showLine !== undefined) {
1266 attrs.showLine = String(node.showLine);
1267 }
1268
1269 if (node.alignment) {
1270 attrs.alignment = node.alignment;
1271 }
1272
1273 if (node.variant) {
1274 attrs.variant = node.variant;
1275 }
1276
1277 if (typeof node.color === "string") {
1278 attrs.color = node.color;
1279 }
1280
1281 const items = (node.children as TTimelineItemElement[])
1282 .map((item) => {
1283 const content = (item.children as Descendant[])
1284 .map((child) => this.serializeDescendant(child, childIndent + 1))
1285 .filter(Boolean)
1286 .join("\n");
1287 const itemAttrs: Record<string, string> = {};
1288
1289 this.setAttribute(itemAttrs, "icon", item.icon);
1290
1291 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1292 })
1293 .join("\n");
1294
1295 return `${indentStr}<TIMELINE${this.serializeAttributes(attrs)}>\n${items}\n${indentStr}</TIMELINE>`;
1296 }
1297
1298 /**
1299 * Serialize boxes layout
1300 */
1301 private serializeBoxes(node: TBoxGroupElement, indent: number): string {
1302 const indentStr = " ".repeat(indent);
1303 const childIndent = indent + 1;
1304 const childIndentStr = " ".repeat(childIndent);
1305 const groupAttrs: Record<string, string> = {};
1306
1307 if (node.boxType) {
1308 groupAttrs.boxType = node.boxType;
1309 }
1310
1311 if (node.orientation) {
1312 groupAttrs.orientation = node.orientation;
1313 }
1314
1315 if (node.columnSize) {
1316 groupAttrs.columnSize = node.columnSize;
1317 }
1318
1319 const items = (node.children as TBoxItemElement[])
1320 .map((item) => {
1321 const content = (item.children as Descendant[])
1322 .map((child) => this.serializeDescendant(child, childIndent + 1))
1323 .filter(Boolean)
1324 .join("\n");
1325 const itemAttrs: Record<string, string> = {};
1326
1327 this.setAttribute(itemAttrs, "icon", item.icon);
1328
1329 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1330 })
1331 .join("\n");
1332
1333 return `${indentStr}<BOXES${this.serializeAttributes(groupAttrs)}>\n${items}\n${indentStr}</BOXES>`;
1334 }
1335
1336 /**
1337 * Serialize compare layout
1338 */
1339 private serializeCompare(node: TCompareGroupElement, indent: number): string {
1340 const indentStr = " ".repeat(indent);
1341 const childIndent = indent + 1;
1342 const childIndentStr = " ".repeat(childIndent);
1343 const groupAttrs: Record<string, string> = {};
1344
1345 if (node.columnSize) {
1346 groupAttrs.columnSize = node.columnSize;
1347 }
1348
1349 const sides = (node.children as TCompareSideElement[])
1350 .map((side) => {
1351 const content = (side.children as Descendant[])
1352 .map((child) => this.serializeDescendant(child, childIndent + 1))
1353 .filter(Boolean)
1354 .join("\n");
1355
1356 return `${childIndentStr}<DIV>\n${content}\n${childIndentStr}</DIV>`;
1357 })
1358 .join("\n");
1359
1360 return `${indentStr}<COMPARE${this.serializeAttributes(groupAttrs)}>\n${sides}\n${indentStr}</COMPARE>`;
1361 }
1362
1363 /**
1364 * Serialize before/after layout
1365 */
1366 private serializeBeforeAfter(
1367 node: TBeforeAfterGroupElement,
1368 indent: number,
1369 ): string {
1370 const indentStr = " ".repeat(indent);
1371 const childIndent = indent + 1;
1372 const childIndentStr = " ".repeat(childIndent);
1373 const groupAttrs: Record<string, string> = {};
1374
1375 if (node.columnSize) {
1376 groupAttrs.columnSize = node.columnSize;
1377 }
1378
1379 const sides = (node.children as TBeforeAfterSideElement[])
1380 .map((side) => {
1381 const content = (side.children as Descendant[])
1382 .map((child) => this.serializeDescendant(child, childIndent + 1))
1383 .filter(Boolean)
1384 .join("\n");
1385
1386 return `${childIndentStr}<DIV>\n${content}\n${childIndentStr}</DIV>`;
1387 })
1388 .join("\n");
1389
1390 return `${indentStr}<BEFORE-AFTER${this.serializeAttributes(groupAttrs)}>\n${sides}\n${indentStr}</BEFORE-AFTER>`;
1391 }
1392
1393 /**
1394 * Serialize pros/cons layout
1395 */
1396 private serializeProsCons(
1397 node: TProsConsGroupElement,
1398 indent: number,
1399 ): string {
1400 const indentStr = " ".repeat(indent);
1401 const childIndent = indent + 1;
1402 const childIndentStr = " ".repeat(childIndent);
1403
1404 const items = (node.children as (TProsItemElement | TConsItemElement)[])
1405 .map((item) => {
1406 const isPros = item.type === "pros-item";
1407 const tag = isPros ? "PROS" : "CONS";
1408
1409 const content = (item.children as Descendant[])
1410 .map((child) => this.serializeDescendant(child, childIndent + 1))
1411 .filter(Boolean)
1412 .join("\n");
1413
1414 return `${childIndentStr}<${tag}>\n${content}\n${childIndentStr}</${tag}>`;
1415 })
1416 .join("\n");
1417
1418 return `${indentStr}<PROS-CONS>\n${items}\n${indentStr}</PROS-CONS>`;
1419 }
1420
1421 /**
1422 * Serialize sequence arrow layout.
1423 */
1424 private serializeArrowVertical(
1425 node: TSequenceArrowGroupElement,
1426 indent: number,
1427 ): string {
1428 const indentStr = " ".repeat(indent);
1429 const childIndent = indent + 1;
1430 const childIndentStr = " ".repeat(childIndent);
1431 const attrs: Record<string, string> = {};
1432
1433 if (node.orientation) {
1434 attrs.orientation = node.orientation;
1435 }
1436
1437 if (node.alignment) {
1438 attrs.alignment = node.alignment;
1439 }
1440
1441 const items = (node.children as TSequenceArrowItemElement[])
1442 .map((item) => {
1443 const content = (item.children as Descendant[])
1444 .map((child) => this.serializeDescendant(child, childIndent + 1))
1445 .filter(Boolean)
1446 .join("\n");
1447
1448 return `${childIndentStr}<DIV>\n${content}\n${childIndentStr}</DIV>`;
1449 })
1450 .join("\n");
1451
1452 return `${indentStr}<ARROW-SEQUENCE${this.serializeAttributes(attrs)}>\n${items}\n${indentStr}</ARROW-SEQUENCE>`;
1453 }
1454
1455 /**
1456 * Serialize stats layout
1457 */
1458 private serializeStats(node: TStatsGroupElement, indent: number): string {
1459 const indentStr = " ".repeat(indent);
1460 const childIndent = indent + 1;
1461 const childIndentStr = " ".repeat(childIndent);
1462
1463 const attrs: Record<string, string> = {};
1464 if (node.statsType) {
1465 attrs.statstype = node.statsType;
1466 }
1467
1468 const items = (node.children as TStatsItemElement[])
1469 .map((item) => {
1470 const itemAttrs: Record<string, string> = {};
1471 this.setAttribute(itemAttrs, "stat", item.stat);
1472
1473 const content = (item.children as Descendant[])
1474 .map((child) => this.serializeDescendant(child, childIndent + 1))
1475 .filter(Boolean)
1476 .join("\n");
1477
1478 return `${childIndentStr}<DIV${this.serializeAttributes(itemAttrs)}>\n${content}\n${childIndentStr}</DIV>`;
1479 })
1480 .join("\n");
1481
1482 return `${indentStr}<STATS${this.serializeAttributes(attrs)}>\n${items}\n${indentStr}</STATS>`;
1483 }
1484
1485 /**
1486 * Serialize table
1487 */
1488 private serializeTable(node: TTableElement, indent: number): string {
1489 const indentStr = " ".repeat(indent);
1490 const rowIndent = indent + 1;
1491 const rowIndentStr = " ".repeat(rowIndent);
1492 const cellIndent = indent + 2;
1493 const cellIndentStr = " ".repeat(cellIndent);
1494
1495 const rows = (node.children as TTableRowElement[])
1496 .map((row) => {
1497 const cells = (row.children as TTableCellElement[])
1498 .map((cell) => {
1499 const isHeader = cell.type === "th";
1500 const tag = isHeader ? "TH" : "TD";
1501
1502 const attrs: Record<string, string> = {};
1503
1504 // Add colspan, rowspan, background as attributes
1505 const cellWithProps = cell as TTableCellElement & {
1506 colSpan?: number;
1507 rowSpan?: number;
1508 background?: string;
1509 };
1510
1511 if (cellWithProps.colSpan && cellWithProps.colSpan > 1) {
1512 attrs.colspan = String(cellWithProps.colSpan);
1513 }
1514 if (cellWithProps.rowSpan && cellWithProps.rowSpan > 1) {
1515 attrs.rowspan = String(cellWithProps.rowSpan);
1516 }
1517 if (cellWithProps.background) {
1518 attrs.background = cellWithProps.background;
1519 }
1520
1521 const content = (cell.children as Descendant[])
1522 .map((child) => this.serializeDescendant(child, 0))
1523 .filter(Boolean)
1524 .join("");
1525
1526 return `${cellIndentStr}<${tag}${this.serializeAttributes(attrs)}>${content}</${tag}>`;
1527 })
1528 .join("\n");
1529
1530 return `${rowIndentStr}<TR>\n${cells}\n${rowIndentStr}</TR>`;
1531 })
1532 .join("\n");
1533
1534 return `${indentStr}<TABLE>\n${rows}\n${indentStr}</TABLE>`;
1535 }
1536
1537 /**
1538 * Serialize button
1539 */
1540 private serializeButton(node: TButtonElement, indent: number): string {
1541 const indentStr = " ".repeat(indent);
1542 const attrs = this.serializeBasicBlockAttributes(
1543 node as unknown as Record<string, unknown>,
1544 );
1545
1546 const buttonWithProps = node as TButtonElement & {
1547 variant?: "filled" | "outline" | "ghost";
1548 size?: "sm" | "md" | "lg";
1549 };
1550
1551 if (buttonWithProps.variant) {
1552 attrs.variant = buttonWithProps.variant;
1553 }
1554 if (buttonWithProps.size) {
1555 attrs.size = buttonWithProps.size;
1556 }
1557
1558 const content = this.serializeDescendants(node.children);
1559
1560 return `${indentStr}<BUTTON${this.serializeAttributes(attrs)}>${content}</BUTTON>`;
1561 }
1562
1563 /**
1564 * Serialize chart
1565 */
1566 private serializeChart(
1567 node: TChartNode,
1568 elementType: string,
1569 indent: number,
1570 ): string {
1571 const indentStr = " ".repeat(indent);
1572 const dataIndent = indent + 1;
1573
1574 const attrs: Record<string, string> = {};
1575 const chartOptions: Record<string, unknown> = {};
1576
1577 this.setAttribute(attrs, "charttype", getChartXmlType(elementType));
1578
1579 for (const [key, value] of Object.entries(
1580 node as unknown as Record<string, unknown>,
1581 )) {
1582 if (CHART_OPTION_EXCLUDED_KEYS.has(key) || value === undefined) {
1583 continue;
1584 }
1585
1586 if (isPrimitiveXmlValue(value)) {
1587 this.setAttribute(attrs, key, value);
1588 }
1589
1590 if (!this.isLayoutPrompt) {
1591 chartOptions[key] = value;
1592 }
1593 }
1594
1595 const structuredParts = this.isLayoutPrompt
1596 ? [
1597 LAYOUT_PROMPT_CHART_DATA.split("\n")
1598 .map((line) => `${" ".repeat(dataIndent)}${line}`)
1599 .join("\n"),
1600 ]
1601 : [
1602 this.serializeChartDataChild(node.data, dataIndent),
1603 Object.keys(chartOptions).length > 0
1604 ? this.serializeJsonChild("OPTIONS", chartOptions, dataIndent)
1605 : null,
1606 ].filter((part): part is string => Boolean(part));
1607
1608 const childParts = structuredParts;
1609
1610 if (childParts.length === 0) {
1611 return `${indentStr}<CHART${this.serializeAttributes(attrs)} />`;
1612 }
1613
1614 return `${indentStr}<CHART${this.serializeAttributes(attrs)}>\n${childParts.join("\n")}\n${indentStr}</CHART>`;
1615 }
1616
1617 private serializeInfographicData(
1618 node: TAntvInfographicElement,
1619 indent: number,
1620 ): string[] {
1621 const parts: string[] = [];
1622 const prompt = node.generationPrompt?.trim();
1623 const sourceText = node.sourceText?.trim();
1624 const syntax = node.syntax?.trim();
1625
1626 if (prompt) {
1627 parts.push(
1628 `${" ".repeat(indent)}<PROMPT>${this.escapeXml(prompt)}</PROMPT>`,
1629 );
1630 }
1631
1632 if (sourceText) {
1633 parts.push(
1634 `${" ".repeat(indent)}<SOURCE>${this.escapeXml(sourceText)}</SOURCE>`,
1635 );
1636 }
1637
1638 if (syntax) {
1639 parts.push(
1640 `${" ".repeat(indent)}<SYNTAX>${this.escapeXml(syntax)}</SYNTAX>`,
1641 );
1642 }
1643
1644 const data = this.serializeJsonChild("DATA", node.data, indent);
1645 if (data) {
1646 parts.push(data);
1647 }
1648
1649 return parts;
1650 }
1651
1652 /**
1653 * Serialize a descendant (could be text or element)
1654 */
1655 private serializeDescendant(
1656 descendant: Descendant,
1657 indent: number,
1658 ): string | null {
1659 if (!descendant || typeof descendant !== "object") return null;
1660
1661 // Check if it's a text node
1662 if ("text" in descendant) {
1663 return this.serializeTextNode(descendant as TText, indent);
1664 }
1665
1666 // It's an element node
1667 return this.serializeNode(descendant as PlateNode, indent);
1668 }
1669
1670 /**
1671 * Serialize descendants (array)
1672 */
1673 private serializeDescendants(descendants: Descendant[]): string {
1674 return descendants
1675 .map((d) => this.serializeDescendant(d, 0))
1676 .filter(Boolean)
1677 .join("");
1678 }
1679
1680 /**
1681 * Serialize text node with formatting
1682 */
1683 private serializeTextNode(node: TText, indent: number): string {
1684 const indentStr = indent > 0 ? " ".repeat(indent) : "";
1685 let text = node.text;
1686
1687 if (this.isLayoutPrompt) {
1688 text = LAYOUT_PROMPT_PLACEHOLDERS.itemLabel;
1689 return indent > 0 ? indentStr + text : text;
1690 }
1691
1692 // Escape the text
1693 text = this.escapeXml(text);
1694
1695 const textWithFormat = node as TText & {
1696 bold?: boolean;
1697 italic?: boolean;
1698 underline?: boolean;
1699 strikethrough?: boolean;
1700 };
1701
1702 // Apply formatting tags
1703 if (textWithFormat.bold) {
1704 text = `<B>${text}</B>`;
1705 }
1706 if (textWithFormat.italic) {
1707 text = `<I>${text}</I>`;
1708 }
1709 if (textWithFormat.underline) {
1710 text = `<U>${text}</U>`;
1711 }
1712 if (textWithFormat.strikethrough) {
1713 text = `<S>${text}</S>`;
1714 }
1715
1716 return indent > 0 ? indentStr + text : text;
1717 }
1718
1719 /**
1720 * Serialize quote element
1721 */
1722 private serializeQuote(node: TQuoteElement, indent: number): string {
1723 const indentStr = " ".repeat(indent);
1724 const attrs: Record<string, string> = {};
1725
1726 if (node.variant && node.variant !== "large") {
1727 this.setAttribute(attrs, "variant", node.variant);
1728 }
1729 this.setAttribute(attrs, "author", node.author);
1730
1731 const content = this.isLayoutPrompt
1732 ? LAYOUT_PROMPT_PLACEHOLDERS.quote
1733 : this.serializeDescendants(node.children);
1734 return `${indentStr}<QUOTE${this.serializeAttributes(attrs)}>${content}</QUOTE>`;
1735 }
1736
1737 private serializeInfographic(
1738 node: TAntvInfographicElement,
1739 indent: number,
1740 ): string {
1741 const indentStr = " ".repeat(indent);
1742 const attrs: Record<string, string> = {};
1743
1744 this.setAttribute(attrs, "id", node.id);
1745 this.setAttribute(attrs, "isLoading", node.isLoading);
1746 this.setAttribute(attrs, "slideLayoutType", node.slideLayoutType);
1747 this.setAttribute(attrs, "width", node.width);
1748 this.setAttribute(attrs, "align", node.align);
1749
1750 if (this.isLayoutPrompt) {
1751 return `${indentStr}<INFOGRAPHIC${this.serializeAttributes(attrs)}>${LAYOUT_PROMPT_PLACEHOLDERS.infographic}</INFOGRAPHIC>`;
1752 }
1753
1754 const childParts = this.serializeInfographicData(node, indent + 1);
1755
1756 if (childParts.length === 0) {
1757 const content =
1758 node.generationPrompt?.trim() ?? "Generate an infographic";
1759 return `${indentStr}<INFOGRAPHIC${this.serializeAttributes(attrs)}>${this.escapeXml(content)}</INFOGRAPHIC>`;
1760 }
1761
1762 return `${indentStr}<INFOGRAPHIC${this.serializeAttributes(attrs)}>\n${childParts.join("\n")}\n${indentStr}</INFOGRAPHIC>`;
1763 }
1764 }
1765
1766 /**
1767 * Helper function to serialize slides to XML
1768 * @param slides Array of PlateSlide objects
1769 * @param includePresentationWrapper Whether to wrap in PRESENTATION tag
1770 * @returns XML string
1771 */
1772 export function serializeSlidesToXml(
1773 slides: PlateSlide[],
1774 includePresentationWrapper = true,
1775 options?: SlideSerializerOptions,
1776 ): string {
1777 const serializer = new SlideSerializer(options);
1778 return serializer.serializeSlides(slides, includePresentationWrapper);
1779 }
1780
1781 /**
1782 * Helper function to serialize a single slide to XML
1783 * @param slide PlateSlide object
1784 * @returns XML string
1785 */
1786 export function serializeSlideToXml(
1787 slide: PlateSlide,
1788 options?: SlideSerializerOptions,
1789 ): string {
1790 const serializer = new SlideSerializer(options);
1791 return serializer.serializeSlides([slide], false);
1792 }
1793
1793 lines TYPESCRIPT