66 CONTENT_FORMAT_VERSION ,
77 type ContentDocument ,
88 ContentDocumentSchema ,
9+ type ContentEmbeddedObject ,
910 ContentRunSchema ,
1011 ContentShapeSchema ,
1112 type ContentTable ,
@@ -181,6 +182,140 @@ function presentationDocument(): ContentDocument {
181182 } ;
182183}
183184
185+ function spreadsheetDocument ( ) : ContentDocument {
186+ return {
187+ kind : 'spreadsheet' ,
188+ formatVersion : CONTENT_FORMAT_VERSION ,
189+ metadata : { title : 'Quarterly figures' } ,
190+ sheets : [
191+ {
192+ name : 'Sheet1' ,
193+ cells : [
194+ { row : 0 , column : 0 , value : { kind : 'string' , value : 'Revenue' } , displayText : 'Revenue' } ,
195+ {
196+ row : 0 ,
197+ column : 1 ,
198+ value : { kind : 'currency' , value : 125000 , currency : 'USD' } ,
199+ formula : '=SUM(B2:B10)' ,
200+ displayText : '$125,000.00' ,
201+ } ,
202+ { row : 1 , column : 1 , value : { kind : 'percentage' , value : 0.235 } , displayText : '23.5%' } ,
203+ { row : 2 , column : 1 , value : { kind : 'boolean' , value : true } , displayText : 'TRUE' } ,
204+ { row : 3 , column : 1 , value : { kind : 'date' , value : '2026-07-30' } , displayText : '30/07/2026' } ,
205+ { row : 4 , column : 1 , value : { kind : 'time' , value : '13:30:00' } , displayText : '1:30 PM' } ,
206+ { row : 5 , column : 1 , value : { kind : 'error' , value : '#DIV/0!' } , displayText : '#DIV/0!' } ,
207+ { row : 6 , column : 1 , value : { kind : 'empty' } , displayText : '' , colSpan : 2 } ,
208+ {
209+ row : 7 ,
210+ column : 0 ,
211+ value : { kind : 'string' , value : 'Mixed formatting' } ,
212+ displayText : 'Mixed formatting' ,
213+ runs : [ { text : 'Mixed ' } , { text : 'formatting' , bold : true } ] ,
214+ } ,
215+ ] ,
216+ columns : [
217+ { index : 0 , widthPt : 120 } ,
218+ { index : 1 , widthPt : 80 , hidden : false } ,
219+ ] ,
220+ rows : [
221+ { index : 0 , heightPt : 15 } ,
222+ { index : 1 , heightPt : 15 , hidden : true } ,
223+ ] ,
224+ images : [
225+ {
226+ kind : 'image' ,
227+ format : 'png' ,
228+ base64 : 'AA==' ,
229+ widthPt : 40 ,
230+ heightPt : 40 ,
231+ anchorRow : 0 ,
232+ anchorColumn : 3 ,
233+ offsetXPt : 2 ,
234+ offsetYPt : 2 ,
235+ } ,
236+ ] ,
237+ printSettings : {
238+ pageSize : { widthPt : 612 , heightPt : 792 } ,
239+ margins : { topPt : 36 , rightPt : 36 , bottomPt : 36 , leftPt : 36 } ,
240+ printRange : { startRow : 0 , startColumn : 0 , endRow : 10 , endColumn : 5 } ,
241+ scale : 100 ,
242+ fitToPages : { width : 1 , height : 1 } ,
243+ repeatRows : { start : 0 , end : 0 } ,
244+ repeatColumns : { start : 0 , end : 0 } ,
245+ gridlines : true ,
246+ headers : false ,
247+ pageOrder : 'downThenOver' ,
248+ manualBreaks : { rows : [ 20 ] , columns : [ ] } ,
249+ } ,
250+ } ,
251+ ] ,
252+ } ;
253+ }
254+
255+ function drawingDocument ( ) : ContentDocument {
256+ return {
257+ kind : 'drawing' ,
258+ formatVersion : CONTENT_FORMAT_VERSION ,
259+ metadata : { title : 'Org chart' } ,
260+ pages : [
261+ {
262+ size : { widthPt : 842 , heightPt : 595 } ,
263+ shapes : [
264+ {
265+ frame : { xPt : 50 , yPt : 50 , widthPt : 200 , heightPt : 60 } ,
266+ insetLeftPt : 3.6 ,
267+ insetTopPt : 3.6 ,
268+ insetRightPt : 3.6 ,
269+ insetBottomPt : 3.6 ,
270+ blocks : [ paragraph ] ,
271+ } ,
272+ ] ,
273+ vectors : [
274+ {
275+ kind : 'rect' ,
276+ frame : { xPt : 50 , yPt : 50 , widthPt : 200 , heightPt : 60 } ,
277+ fill : { r : 0.9 , g : 0.9 , b : 1 } ,
278+ stroke : { color : COLOR_BLACK , widthPt : 1 } ,
279+ } ,
280+ {
281+ kind : 'ellipse' ,
282+ frame : { xPt : 300 , yPt : 50 , widthPt : 100 , heightPt : 100 } ,
283+ fill : { r : 1 , g : 1 , b : 0.8 } ,
284+ } ,
285+ {
286+ kind : 'line' ,
287+ from : { xPt : 250 , yPt : 80 } ,
288+ to : { xPt : 300 , yPt : 100 } ,
289+ stroke : { color : COLOR_BLACK , widthPt : 2 } ,
290+ } ,
291+ {
292+ kind : 'path' ,
293+ frame : { xPt : 400 , yPt : 200 , widthPt : 100 , heightPt : 100 } ,
294+ subpaths : [
295+ {
296+ start : { xPt : 0 , yPt : 0 } ,
297+ segments : [
298+ { kind : 'line' , to : { xPt : 100 , yPt : 0 } } ,
299+ {
300+ kind : 'cubic' ,
301+ control1 : { xPt : 100 , yPt : 50 } ,
302+ control2 : { xPt : 50 , yPt : 100 } ,
303+ to : { xPt : 0 , yPt : 100 } ,
304+ } ,
305+ ] ,
306+ closed : true ,
307+ } ,
308+ ] ,
309+ fill : { r : 0.2 , g : 0.8 , b : 0.2 } ,
310+ fillRule : 'nonzero' ,
311+ stroke : { color : COLOR_BLACK , widthPt : 0.5 } ,
312+ } ,
313+ ] ,
314+ } ,
315+ ] ,
316+ } ;
317+ }
318+
184319describe ( 'sourcePath' , ( ) => {
185320 it ( 'survives a JSON round trip when set on every block kind that carries it' , ( ) => {
186321 const runWithSourcePath : ContentBlock = {
@@ -256,8 +391,22 @@ describe('ContentDocumentSchema round trips', () => {
256391 expect ( ContentDocumentSchema . parse ( roundTripped ) ) . toEqual ( original ) ;
257392 } ) ;
258393
394+ it ( 'deep-equals the original spreadsheet document after a JSON round trip' , ( ) => {
395+ const original = spreadsheetDocument ( ) ;
396+ const parsed = ContentDocumentSchema . parse ( original ) ;
397+ const roundTripped : unknown = JSON . parse ( JSON . stringify ( parsed ) ) ;
398+ expect ( ContentDocumentSchema . parse ( roundTripped ) ) . toEqual ( original ) ;
399+ } ) ;
400+
401+ it ( 'deep-equals the original drawing document after a JSON round trip' , ( ) => {
402+ const original = drawingDocument ( ) ;
403+ const parsed = ContentDocumentSchema . parse ( original ) ;
404+ const roundTripped : unknown = JSON . parse ( JSON . stringify ( parsed ) ) ;
405+ expect ( ContentDocumentSchema . parse ( roundTripped ) ) . toEqual ( original ) ;
406+ } ) ;
407+
259408 it ( 'rejects an unknown discriminant' , ( ) => {
260- expect ( ContentDocumentSchema . safeParse ( { kind : 'spreadsheet ' } ) . success ) . toBe ( false ) ;
409+ expect ( ContentDocumentSchema . safeParse ( { kind : 'bogus ' } ) . success ) . toBe ( false ) ;
261410 } ) ;
262411
263412 it ( 'rejects a mismatched formatVersion' , ( ) => {
@@ -271,3 +420,195 @@ describe('ContentDocumentSchema round trips', () => {
271420 ) . toBe ( false ) ;
272421 } ) ;
273422} ) ;
423+
424+ // Deliberately deep nesting for ContentEmbeddedObjectSchema's own recursive guard, mirroring the discipline already applied to ContentTable's three-level recursion test above: a formula embedded inside a drawing embedded inside a spreadsheet, three levels deep, exercising both anchoring mechanisms (ContentSheetSchema.embeddedObjects at level 1->2, and the ContentBlock 'embeddedObject' variant at level 2->3) in the same structure.
425+ const formulaDocument : ContentDocument = {
426+ kind : 'wordprocessing' ,
427+ formatVersion : CONTENT_FORMAT_VERSION ,
428+ metadata : { title : 'Formula' } ,
429+ sections : [
430+ {
431+ pageSize : { widthPt : 200 , heightPt : 50 } ,
432+ margins : { topPt : 0 , rightPt : 0 , bottomPt : 0 , leftPt : 0 } ,
433+ blocks : [ { kind : 'paragraph' , runs : [ { text : 'x^2 + y^2 = z^2' } ] } ] ,
434+ } ,
435+ ] ,
436+ } ;
437+
438+ const formulaEmbeddedBlock : ContentBlock = {
439+ kind : 'embeddedObject' ,
440+ objectKind : 'formula' ,
441+ document : formulaDocument ,
442+ frame : { xPt : 10 , yPt : 10 , widthPt : 80 , heightPt : 20 } ,
443+ } ;
444+
445+ const drawingWithFormula : ContentDocument = {
446+ kind : 'drawing' ,
447+ formatVersion : CONTENT_FORMAT_VERSION ,
448+ metadata : { } ,
449+ pages : [
450+ {
451+ size : { widthPt : 400 , heightPt : 300 } ,
452+ shapes : [
453+ {
454+ frame : { xPt : 0 , yPt : 0 , widthPt : 100 , heightPt : 40 } ,
455+ insetLeftPt : 0 ,
456+ insetTopPt : 0 ,
457+ insetRightPt : 0 ,
458+ insetBottomPt : 0 ,
459+ blocks : [ formulaEmbeddedBlock ] ,
460+ } ,
461+ ] ,
462+ vectors : [ ] ,
463+ } ,
464+ ] ,
465+ } ;
466+
467+ const drawingEmbeddedObject : ContentEmbeddedObject = {
468+ objectKind : 'drawing' ,
469+ document : drawingWithFormula ,
470+ frame : { xPt : 100 , yPt : 100 , widthPt : 200 , heightPt : 150 } ,
471+ } ;
472+
473+ const spreadsheetWithDrawing : ContentDocument = {
474+ kind : 'spreadsheet' ,
475+ formatVersion : CONTENT_FORMAT_VERSION ,
476+ metadata : { } ,
477+ sheets : [
478+ {
479+ name : 'Sheet1' ,
480+ cells : [ ] ,
481+ columns : [ ] ,
482+ rows : [ ] ,
483+ images : [ ] ,
484+ printSettings : {
485+ pageSize : { widthPt : 612 , heightPt : 792 } ,
486+ margins : { topPt : 36 , rightPt : 36 , bottomPt : 36 , leftPt : 36 } ,
487+ gridlines : true ,
488+ headers : true ,
489+ pageOrder : 'downThenOver' ,
490+ } ,
491+ embeddedObjects : [ drawingEmbeddedObject ] ,
492+ } ,
493+ ] ,
494+ } ;
495+
496+ describe ( 'ContentEmbeddedObjectSchema deep recursion' , ( ) => {
497+ it ( 'accepts a formula embedded inside a drawing embedded inside a spreadsheet, three levels deep' , ( ) => {
498+ expect ( isContentBlock ( formulaEmbeddedBlock ) ) . toBe ( true ) ;
499+ expect ( ContentDocumentSchema . safeParse ( spreadsheetWithDrawing ) . success ) . toBe ( true ) ;
500+ } ) ;
501+
502+ it ( 'genuinely walks every level, not just the outermost shell' , ( ) => {
503+ const parsed = ContentDocumentSchema . parse ( spreadsheetWithDrawing ) ;
504+ if ( parsed . kind !== 'spreadsheet' ) {
505+ throw new Error ( 'expected a spreadsheet document' ) ;
506+ }
507+ const sheet = parsed . sheets [ 0 ] ;
508+ if ( sheet === undefined ) {
509+ throw new Error ( 'expected a sheet' ) ;
510+ }
511+ const embeddedDrawing = sheet . embeddedObjects ?. [ 0 ] ;
512+ if ( embeddedDrawing ?. document . kind !== 'drawing' ) {
513+ throw new Error ( 'expected the level-2 embedded object to be a drawing document' ) ;
514+ }
515+ const page = embeddedDrawing . document . pages [ 0 ] ;
516+ if ( page === undefined ) {
517+ throw new Error ( 'expected a drawing page' ) ;
518+ }
519+ const shape = page . shapes [ 0 ] ;
520+ if ( shape === undefined ) {
521+ throw new Error ( 'expected a shape' ) ;
522+ }
523+ const embeddedFormulaBlock = shape . blocks [ 0 ] ;
524+ if ( embeddedFormulaBlock ?. kind !== 'embeddedObject' ) {
525+ throw new Error ( 'expected the level-3 block to be an embedded object' ) ;
526+ }
527+ expect ( embeddedFormulaBlock . objectKind ) . toBe ( 'formula' ) ;
528+ if ( embeddedFormulaBlock . document . kind !== 'wordprocessing' ) {
529+ throw new Error ( 'expected the level-3 embedded document to be wordprocessing' ) ;
530+ }
531+ const formulaParagraph = embeddedFormulaBlock . document . sections [ 0 ] ?. blocks [ 0 ] ;
532+ if ( formulaParagraph ?. kind !== 'paragraph' ) {
533+ throw new Error ( 'expected a paragraph' ) ;
534+ }
535+ expect ( formulaParagraph . runs [ 0 ] ?. text ) . toBe ( 'x^2 + y^2 = z^2' ) ;
536+ } ) ;
537+
538+ it ( 'survives a JSON round trip at full depth' , ( ) => {
539+ const parsed = ContentDocumentSchema . parse ( spreadsheetWithDrawing ) ;
540+ const roundTripped : unknown = JSON . parse ( JSON . stringify ( parsed ) ) ;
541+ expect ( ContentDocumentSchema . parse ( roundTripped ) ) . toEqual ( spreadsheetWithDrawing ) ;
542+ } ) ;
543+
544+ it ( 'rejects a malformed embedded object buried three levels deep, not just at the outermost shell' , ( ) => {
545+ const deeplyMalformed : unknown = {
546+ kind : 'spreadsheet' ,
547+ formatVersion : CONTENT_FORMAT_VERSION ,
548+ metadata : { } ,
549+ sheets : [
550+ {
551+ name : 'Sheet1' ,
552+ cells : [ ] ,
553+ columns : [ ] ,
554+ rows : [ ] ,
555+ images : [ ] ,
556+ printSettings : {
557+ pageSize : { widthPt : 612 , heightPt : 792 } ,
558+ margins : { topPt : 36 , rightPt : 36 , bottomPt : 36 , leftPt : 36 } ,
559+ gridlines : true ,
560+ headers : true ,
561+ pageOrder : 'downThenOver' ,
562+ } ,
563+ embeddedObjects : [
564+ {
565+ objectKind : 'drawing' ,
566+ frame : { xPt : 100 , yPt : 100 , widthPt : 200 , heightPt : 150 } ,
567+ document : {
568+ kind : 'drawing' ,
569+ formatVersion : CONTENT_FORMAT_VERSION ,
570+ metadata : { } ,
571+ pages : [
572+ {
573+ size : { widthPt : 400 , heightPt : 300 } ,
574+ vectors : [ ] ,
575+ shapes : [
576+ {
577+ frame : { xPt : 0 , yPt : 0 , widthPt : 100 , heightPt : 40 } ,
578+ insetLeftPt : 0 ,
579+ insetTopPt : 0 ,
580+ insetRightPt : 0 ,
581+ insetBottomPt : 0 ,
582+ blocks : [
583+ {
584+ kind : 'embeddedObject' ,
585+ objectKind : 'formula' ,
586+ frame : { xPt : 10 , yPt : 10 , widthPt : 80 , heightPt : 20 } ,
587+ document : {
588+ kind : 'wordprocessing' ,
589+ formatVersion : CONTENT_FORMAT_VERSION ,
590+ metadata : { } ,
591+ sections : [
592+ {
593+ pageSize : { widthPt : 200 , heightPt : 50 } ,
594+ margins : { topPt : 0 , rightPt : 0 , bottomPt : 0 , leftPt : 0 } ,
595+ // malformed: a run's text must be a string, not a number -- must still fail even though every ancestor around it is well-formed.
596+ blocks : [ { kind : 'paragraph' , runs : [ { text : 1 } ] } ] ,
597+ } ,
598+ ] ,
599+ } ,
600+ } ,
601+ ] ,
602+ } ,
603+ ] ,
604+ } ,
605+ ] ,
606+ } ,
607+ } ,
608+ ] ,
609+ } ,
610+ ] ,
611+ } ;
612+ expect ( ContentDocumentSchema . safeParse ( deeplyMalformed ) . success ) . toBe ( false ) ;
613+ } ) ;
614+ } ) ;
0 commit comments