@@ -57,26 +57,103 @@ function walkCloud(value: unknown, nodes: DeckIrNode[], hash: string, locator: s
5757 if ( Array . isArray ( value ) ) { value . forEach ( ( item , index ) => walkCloud ( item , nodes , hash , `${ locator } /${ index } ` , page , parentId ) ) ; return ; }
5858 const record = value as Record < string , unknown > ;
5959 const text = cloudText ( record ) ;
60- const kind = String ( record . type ?? ( record . txBody ? 'shape' : record . table ? 'table' : text ? 'text' : 'object' ) ) . toLowerCase ( ) ;
60+ const kind = cloudKind ( record , text ) ;
6161 let nextParent = parentId ;
62+ const skipKeys = new Set < string > ( WALK_SKIP_KEYS ) ;
6263 if ( text || [ 'shape' , 'picture' , 'image' , 'table' , 'group' , 'chart' ] . some ( ( token ) => kind . includes ( token ) ) ) {
6364 const id = stableId ( hash , `cloud:${ locator } ` ) ;
6465 const xfrm = object ( record . xfrm ) ;
65- const assetPath = string ( record . assetPath ?? record . suggestedPath ?? record . path ) ;
66+ // 只认解析器明确给出的资产指针。`path` 曾经也在这条兜底链上,但在 pptx 里它是自选
67+ // 图形的几何路径(`"M 0 0 L 10 10 Z"`),任何 Freeform 都会因此被判成「指向不存在的
68+ // 资产」,凭空产出一条 cloud_asset_unavailable 并把 quality 顶成 degraded。
69+ const assetPath = string ( record . assetPath ?? record . suggestedPath ) ;
6670 const externalUrl = string ( record . accessURL ?? record . url ) ;
6771 const node : DeckIrNode = { id, type : kind , parentId, children : [ ] , order : nodes . length , ...( text ? { text } : { } ) ,
6872 ...( page ? { page } : { } ) , sourceRef : { path : locator , ...( page ? { page } : { } ) } ,
6973 ...( xfrm && [ xfrm . x , xfrm . y , xfrm . cx , xfrm . cy ] . every ( ( item ) => typeof item === 'number' ) ?
7074 { bbox : [ xfrm . x as number , xfrm . y as number , ( xfrm . x as number ) + ( xfrm . cx as number ) , ( xfrm . y as number ) + ( xfrm . cy as number ) ] } : { } ) ,
7175 extensions : { cloud : { id : record . id , name : record . name , style : record . style } , ...( assetPath ? { assetPath } : { } ) , ...( externalUrl ? { externalUrl } : { } ) } } ;
7276 nodes . push ( node ) ; if ( parentId ) nodes . find ( ( item ) => item . id === parentId ) ?. children . push ( id ) ; nextParent = id ;
77+ // 表格必须按 table → table_row → table_cell 发,和 local/pptx、local/docx、result3 三个
78+ // 适配器一致:Markdown 渲染器只认这三种类型,让通用遍历把单元格摊成一串 shape 子节点,
79+ // 整张表会在渲染时被静默丢掉——IR 里看得见,产物里一个字都没有。
80+ if ( kind . includes ( 'table' ) && emitCloudTable ( record , node , nodes , hash , locator , page ) ) {
81+ skipKeys . add ( 'table' ) ;
82+ }
7383 }
7484 for ( const [ key , child ] of Object . entries ( record ) ) {
75- if ( [ 'style' , 'xfrm' , 'txBody' , 'text' , 't' , 'name' , 'id' , 'type' ] . includes ( key ) ) continue ;
85+ if ( skipKeys . has ( key ) ) continue ;
7686 if ( child && typeof child === 'object' ) walkCloud ( child , nodes , hash , `${ locator } /${ key } ` , page , nextParent ) ;
7787 }
7888}
7989
90+ /** 遍历时不再下钻的键:要么已在节点上表达,要么由更专门的分支接管。 */
91+ const WALK_SKIP_KEYS = [ 'style' , 'xfrm' , 'txBody' , 'text' , 't' , 'name' , 'id' , 'type' ] as const ;
92+
93+ /**
94+ * 把云端表格结构展开成行列节点;认不出行列就返回 false,交回通用遍历。
95+ *
96+ * 行与单元格的键名各格式不同(pptx 是 `table.trs[].cells[]`),所以按候选键取第一个命中的,
97+ * 而不是写死某一种格式。
98+ */
99+ function emitCloudTable (
100+ record : Record < string , unknown > ,
101+ table : DeckIrNode ,
102+ nodes : DeckIrNode [ ] ,
103+ hash : string ,
104+ locator : string ,
105+ page ?: number ,
106+ ) : boolean {
107+ const container = object ( record . table ) ?? record ;
108+ const rows = array ( container . trs ?? container . rows ) . map ( ( row ) =>
109+ array ( object ( row ) ?. cells ?? object ( row ) ?. tcs ) . flatMap ( ( cell ) => {
110+ const parsed = object ( cell ) ;
111+ return parsed ? [ parsed ] : [ ] ;
112+ } ) ) ;
113+ if ( ! rows . some ( ( cells ) => cells . length > 0 ) ) return false ;
114+ table . extensions = { ...table . extensions , rows : rows . length ,
115+ columns : Math . max ( ...rows . map ( ( cells ) => cells . length ) ) } ;
116+ rows . forEach ( ( cells , rowIndex ) => {
117+ const rowLocator = `${ locator } /row/${ rowIndex } ` ;
118+ const rowId = stableId ( hash , `cloud:${ rowLocator } ` ) ;
119+ const row : DeckIrNode = { id : rowId , type : 'table_row' , parentId : table . id , children : [ ] , order : nodes . length ,
120+ ...( page ? { page } : { } ) , sourceRef : { path : rowLocator , ...( page ? { page } : { } ) } } ;
121+ nodes . push ( row ) ; table . children . push ( rowId ) ;
122+ cells . forEach ( ( cell , column ) => {
123+ const cellLocator = `${ rowLocator } /cell/${ column } ` ;
124+ const cellId = stableId ( hash , `cloud:${ cellLocator } ` ) ;
125+ const text = cloudText ( cell ) ;
126+ nodes . push ( { id : cellId , type : 'table_cell' , parentId : rowId , children : [ ] , order : nodes . length ,
127+ ...( text ? { text } : { } ) , ...( page ? { page } : { } ) ,
128+ sourceRef : { path : cellLocator , ...( page ? { page } : { } ) } ,
129+ extensions : { row : rowIndex , column,
130+ gridSpan : number ( cell . colSpan ?? cell . gridSpan ) ?? 1 , rowSpan : number ( cell . rowSpan ) ?? 1 ,
131+ ...( cell . hMerge === true ? { hMerge : true } : { } ) , ...( cell . vMerge === true ? { vMerge : true } : { } ) } } ) ;
132+ row . children . push ( cellId ) ;
133+ } ) ;
134+ } ) ;
135+ return true ;
136+ }
137+
138+ /**
139+ * 节点类型。
140+ *
141+ * 解析器自报的 `type` 是它的形状分类(pptx 一律是 "Shape"),不是文档语义:整篇因此落成
142+ * 清一色的 shape,Markdown 一个标题都没有(实测 99 页文档本地 96 个 heading、云端 0 个)。
143+ * 占位符类型才是 OOXML 里「这是标题」的确定性证据,与 local/pptx/parser.ts 用的是同一条
144+ * 判据 —— 两个引擎的产物必须在这件事上对得上,否则同一份文件换引擎结构就变了。
145+ *
146+ * 只在有文字时改判,空的标题占位符仍按原类型走:那些节点靠类型词命中才得以建出来,
147+ * 改成 heading 会让它们从 IR 里整个消失。
148+ */
149+ function cloudKind ( record : Record < string , unknown > , text : string ) : string {
150+ const placeholder = string ( object ( record . ph ) ?. type ) ;
151+ const declared = string ( record . type ) ?. toLowerCase ( ) ;
152+ if ( text && ( placeholder === 'title' || placeholder === 'ctrTitle' ) ) return 'heading' ;
153+ if ( text && declared === 'shape' ) return 'text' ;
154+ return declared ?? ( record . txBody ? 'shape' : record . table ? 'table' : text ? 'text' : 'object' ) ;
155+ }
156+
80157function cloudText ( record : Record < string , unknown > ) : string {
81158 if ( typeof record . text === 'string' ) return record . text ;
82159 if ( typeof record . t === 'string' ) return record . t ;
0 commit comments