omni-viewer-core 0.1.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.
Files changed (387) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/THIRD_PARTY_NOTICES.md +49 -0
  4. package/dist/assets/audio-engine/audio_engine.mjs +2 -0
  5. package/dist/assets/audio-engine/audio_engine.wasm +0 -0
  6. package/dist/assets/pdfjs/pdf.worker.min.mjs +21 -0
  7. package/dist/host/index.d.ts +133 -0
  8. package/dist/host/index.js +10 -0
  9. package/dist/i18n/catalog.en.d.ts +1 -0
  10. package/dist/i18n/catalog.en.js +411 -0
  11. package/dist/i18n/catalog.ko.d.ts +1 -0
  12. package/dist/i18n/catalog.ko.js +68 -0
  13. package/dist/i18n/index.d.ts +18 -0
  14. package/dist/i18n/index.js +41 -0
  15. package/dist/licenses/emscripten.txt +101 -0
  16. package/dist/licenses/kissfft.txt +28 -0
  17. package/dist/licenses/pdfjs-dist.txt +177 -0
  18. package/dist/parsers/a2l/index.d.ts +1 -0
  19. package/dist/parsers/a2l/index.js +1 -0
  20. package/dist/parsers/archive/index.d.ts +22 -0
  21. package/dist/parsers/archive/index.js +46 -0
  22. package/dist/parsers/archive/model.d.ts +29 -0
  23. package/dist/parsers/archive/model.js +1 -0
  24. package/dist/parsers/archive/worker.d.ts +21 -0
  25. package/dist/parsers/archive/worker.js +1 -0
  26. package/dist/parsers/arxml/index.d.ts +1 -0
  27. package/dist/parsers/arxml/index.js +1 -0
  28. package/dist/parsers/asc/index.d.ts +1 -0
  29. package/dist/parsers/asc/index.js +1 -0
  30. package/dist/parsers/audio/index.d.ts +9 -0
  31. package/dist/parsers/audio/index.js +46 -0
  32. package/dist/parsers/automotive/index.d.ts +33 -0
  33. package/dist/parsers/automotive/index.js +973 -0
  34. package/dist/parsers/blf/index.d.ts +1 -0
  35. package/dist/parsers/blf/index.js +1 -0
  36. package/dist/parsers/csv/delimiter.d.ts +31 -0
  37. package/dist/parsers/csv/delimiter.js +151 -0
  38. package/dist/parsers/csv/index.d.ts +30 -0
  39. package/dist/parsers/csv/index.js +108 -0
  40. package/dist/parsers/csv/parseText.d.ts +20 -0
  41. package/dist/parsers/csv/parseText.js +108 -0
  42. package/dist/parsers/csv/sort.d.ts +26 -0
  43. package/dist/parsers/csv/sort.js +224 -0
  44. package/dist/parsers/csv/statistics.d.ts +49 -0
  45. package/dist/parsers/csv/statistics.js +150 -0
  46. package/dist/parsers/dbc/index.d.ts +56 -0
  47. package/dist/parsers/dbc/index.js +94 -0
  48. package/dist/parsers/doc-binary/index.d.ts +35 -0
  49. package/dist/parsers/doc-binary/index.js +267 -0
  50. package/dist/parsers/doc-binary/vscode-parser.d.ts +142 -0
  51. package/dist/parsers/doc-binary/vscode-parser.js +3747 -0
  52. package/dist/parsers/excel/index.d.ts +17 -0
  53. package/dist/parsers/excel/index.js +406 -0
  54. package/dist/parsers/excel/model.d.ts +80 -0
  55. package/dist/parsers/excel/model.js +48 -0
  56. package/dist/parsers/excel/serialize.d.ts +3 -0
  57. package/dist/parsers/excel/serialize.js +78 -0
  58. package/dist/parsers/hdf5/index.d.ts +75 -0
  59. package/dist/parsers/hdf5/index.js +658 -0
  60. package/dist/parsers/hdf5/node.d.ts +21 -0
  61. package/dist/parsers/hdf5/node.js +95 -0
  62. package/dist/parsers/json/index.d.ts +15 -0
  63. package/dist/parsers/json/index.js +102 -0
  64. package/dist/parsers/json/model.d.ts +42 -0
  65. package/dist/parsers/json/model.js +140 -0
  66. package/dist/parsers/json/parse.d.ts +22 -0
  67. package/dist/parsers/json/parse.js +279 -0
  68. package/dist/parsers/json/tokenizer.d.ts +17 -0
  69. package/dist/parsers/json/tokenizer.js +175 -0
  70. package/dist/parsers/jsonl/index.d.ts +14 -0
  71. package/dist/parsers/jsonl/index.js +18 -0
  72. package/dist/parsers/jsonl/model.d.ts +23 -0
  73. package/dist/parsers/jsonl/model.js +1 -0
  74. package/dist/parsers/jsonl/parse.d.ts +7 -0
  75. package/dist/parsers/jsonl/parse.js +34 -0
  76. package/dist/parsers/markdown/index.d.ts +37 -0
  77. package/dist/parsers/markdown/index.js +59 -0
  78. package/dist/parsers/mat/index.d.ts +31 -0
  79. package/dist/parsers/mat/index.js +426 -0
  80. package/dist/parsers/mermaid/index.d.ts +5 -0
  81. package/dist/parsers/mermaid/index.js +3 -0
  82. package/dist/parsers/mf4/index.d.ts +1 -0
  83. package/dist/parsers/mf4/index.js +1 -0
  84. package/dist/parsers/parquet/index.d.ts +21 -0
  85. package/dist/parsers/parquet/index.js +68 -0
  86. package/dist/parsers/pcap/index.d.ts +1 -0
  87. package/dist/parsers/pcap/index.js +1 -0
  88. package/dist/parsers/pcapng/index.d.ts +1 -0
  89. package/dist/parsers/pcapng/index.js +1 -0
  90. package/dist/parsers/plantuml/index.d.ts +5 -0
  91. package/dist/parsers/plantuml/index.js +4 -0
  92. package/dist/parsers/ppt-binary/index.d.ts +5 -0
  93. package/dist/parsers/ppt-binary/index.js +62 -0
  94. package/dist/parsers/ppt-binary/pptBinaryBuildContext.d.ts +12 -0
  95. package/dist/parsers/ppt-binary/pptBinaryBuildContext.js +40 -0
  96. package/dist/parsers/ppt-binary/pptBinaryContainer.d.ts +4 -0
  97. package/dist/parsers/ppt-binary/pptBinaryContainer.js +174 -0
  98. package/dist/parsers/ppt-binary/pptBinaryTypes.d.ts +125 -0
  99. package/dist/parsers/ppt-binary/pptBinaryTypes.js +1 -0
  100. package/dist/parsers/ppt-binary/pptSlideLayouts.d.ts +9 -0
  101. package/dist/parsers/ppt-binary/pptSlideLayouts.js +510 -0
  102. package/dist/parsers/ppt-binary/vscode-parser.d.ts +116 -0
  103. package/dist/parsers/ppt-binary/vscode-parser.js +2853 -0
  104. package/dist/parsers/ppt-binary/vscode.d.ts +4 -0
  105. package/dist/parsers/ppt-binary/vscode.js +12 -0
  106. package/dist/parsers/pptx/index.d.ts +15 -0
  107. package/dist/parsers/pptx/index.js +88 -0
  108. package/dist/parsers/pptx/vscode-parser.d.ts +167 -0
  109. package/dist/parsers/pptx/vscode-parser.js +1503 -0
  110. package/dist/parsers/pptx/vscode.d.ts +4 -0
  111. package/dist/parsers/pptx/vscode.js +18 -0
  112. package/dist/parsers/pptx/zip-reader.d.ts +3 -0
  113. package/dist/parsers/pptx/zip-reader.js +20 -0
  114. package/dist/parsers/proto/index.d.ts +90 -0
  115. package/dist/parsers/proto/index.js +197 -0
  116. package/dist/parsers/psd/index.d.ts +16 -0
  117. package/dist/parsers/psd/index.js +10 -0
  118. package/dist/parsers/shapefile/dbf.d.ts +23 -0
  119. package/dist/parsers/shapefile/dbf.js +112 -0
  120. package/dist/parsers/shapefile/index.d.ts +44 -0
  121. package/dist/parsers/shapefile/index.js +119 -0
  122. package/dist/parsers/shapefile/reproject.d.ts +10 -0
  123. package/dist/parsers/shapefile/reproject.js +41 -0
  124. package/dist/parsers/slide-model.d.ts +106 -0
  125. package/dist/parsers/slide-model.js +1 -0
  126. package/dist/parsers/toml/index.d.ts +15 -0
  127. package/dist/parsers/toml/index.js +18 -0
  128. package/dist/parsers/toml/model.d.ts +15 -0
  129. package/dist/parsers/toml/model.js +1 -0
  130. package/dist/parsers/toml/parse.d.ts +16 -0
  131. package/dist/parsers/toml/parse.js +266 -0
  132. package/dist/parsers/types.d.ts +103 -0
  133. package/dist/parsers/types.js +106 -0
  134. package/dist/parsers/video/index.d.ts +6 -0
  135. package/dist/parsers/video/index.js +2 -0
  136. package/dist/parsers/yaml/index.d.ts +16 -0
  137. package/dist/parsers/yaml/index.js +58 -0
  138. package/dist/parsers/yaml/model.d.ts +22 -0
  139. package/dist/parsers/yaml/model.js +1 -0
  140. package/dist/parsers/yaml/self-loading.d.ts +3 -0
  141. package/dist/parsers/yaml/self-loading.js +19 -0
  142. package/dist/parsers/zip-scan.d.ts +8 -0
  143. package/dist/parsers/zip-scan.js +80 -0
  144. package/dist/registry/container.d.ts +17 -0
  145. package/dist/registry/container.js +49 -0
  146. package/dist/registry/index.d.ts +133 -0
  147. package/dist/registry/index.js +348 -0
  148. package/dist/registry/probe.d.ts +3 -0
  149. package/dist/registry/probe.js +177 -0
  150. package/dist/registry/sniff.d.ts +17 -0
  151. package/dist/registry/sniff.js +57 -0
  152. package/dist/styles/archive.css +10 -0
  153. package/dist/styles/audio.css +66 -0
  154. package/dist/styles/automotive.css +1 -0
  155. package/dist/styles/csv.css +262 -0
  156. package/dist/styles/excel.css +212 -0
  157. package/dist/styles/fallback.css +48 -0
  158. package/dist/styles/hdf5.css +3 -0
  159. package/dist/styles/hwp.css +12 -0
  160. package/dist/styles/image.css +115 -0
  161. package/dist/styles/json.css +342 -0
  162. package/dist/styles/jsonl.css +1 -0
  163. package/dist/styles/markdown.css +10 -0
  164. package/dist/styles/mat.css +2 -0
  165. package/dist/styles/mermaid.css +1 -0
  166. package/dist/styles/parquet.css +2 -0
  167. package/dist/styles/pdf.css +551 -0
  168. package/dist/styles/plantuml.css +1 -0
  169. package/dist/styles/ppt.css +1 -0
  170. package/dist/styles/proto.css +2 -0
  171. package/dist/styles/psd.css +1 -0
  172. package/dist/styles/shapefile.css +25 -0
  173. package/dist/styles/toml.css +1 -0
  174. package/dist/styles/video.css +74 -0
  175. package/dist/styles/word.css +685 -0
  176. package/dist/styles/yaml.css +1 -0
  177. package/dist/viewers/a2l/index.d.ts +15 -0
  178. package/dist/viewers/a2l/index.js +5 -0
  179. package/dist/viewers/archive/archive-preview-decoder.d.ts +4 -0
  180. package/dist/viewers/archive/archive-preview-decoder.js +223 -0
  181. package/dist/viewers/archive/controller.d.ts +24 -0
  182. package/dist/viewers/archive/controller.js +18 -0
  183. package/dist/viewers/archive/index.d.ts +25 -0
  184. package/dist/viewers/archive/index.js +230 -0
  185. package/dist/viewers/archive/preview.d.ts +2 -0
  186. package/dist/viewers/archive/preview.js +2 -0
  187. package/dist/viewers/archive/styles.d.ts +1 -0
  188. package/dist/viewers/archive/styles.js +11 -0
  189. package/dist/viewers/arxml/index.d.ts +15 -0
  190. package/dist/viewers/arxml/index.js +5 -0
  191. package/dist/viewers/asc/index.d.ts +15 -0
  192. package/dist/viewers/asc/index.js +5 -0
  193. package/dist/viewers/audio/controller.d.ts +33 -0
  194. package/dist/viewers/audio/controller.js +49 -0
  195. package/dist/viewers/audio/engine.d.ts +44 -0
  196. package/dist/viewers/audio/engine.js +108 -0
  197. package/dist/viewers/audio/index.d.ts +92 -0
  198. package/dist/viewers/audio/index.js +448 -0
  199. package/dist/viewers/audio/self-loading.d.ts +7 -0
  200. package/dist/viewers/audio/self-loading.js +42 -0
  201. package/dist/viewers/audio/styles.d.ts +1 -0
  202. package/dist/viewers/audio/styles.js +73 -0
  203. package/dist/viewers/automotive/index.d.ts +8 -0
  204. package/dist/viewers/automotive/index.js +152 -0
  205. package/dist/viewers/automotive/styles.d.ts +1 -0
  206. package/dist/viewers/automotive/styles.js +1 -0
  207. package/dist/viewers/avro/index.d.ts +15 -0
  208. package/dist/viewers/avro/index.js +5 -0
  209. package/dist/viewers/bag/index.d.ts +15 -0
  210. package/dist/viewers/bag/index.js +5 -0
  211. package/dist/viewers/blf/index.d.ts +15 -0
  212. package/dist/viewers/blf/index.js +5 -0
  213. package/dist/viewers/csv/controller.d.ts +126 -0
  214. package/dist/viewers/csv/controller.js +598 -0
  215. package/dist/viewers/csv/index.d.ts +48 -0
  216. package/dist/viewers/csv/index.js +846 -0
  217. package/dist/viewers/csv/styles.d.ts +1 -0
  218. package/dist/viewers/csv/styles.js +267 -0
  219. package/dist/viewers/db3/index.d.ts +15 -0
  220. package/dist/viewers/db3/index.js +5 -0
  221. package/dist/viewers/dbc/index.d.ts +15 -0
  222. package/dist/viewers/dbc/index.js +5 -0
  223. package/dist/viewers/diagram.d.ts +14 -0
  224. package/dist/viewers/diagram.js +95 -0
  225. package/dist/viewers/excel/controller.d.ts +108 -0
  226. package/dist/viewers/excel/controller.js +548 -0
  227. package/dist/viewers/excel/grid.d.ts +28 -0
  228. package/dist/viewers/excel/grid.js +99 -0
  229. package/dist/viewers/excel/index.d.ts +37 -0
  230. package/dist/viewers/excel/index.js +620 -0
  231. package/dist/viewers/excel/self-loading.d.ts +6 -0
  232. package/dist/viewers/excel/self-loading.js +13 -0
  233. package/dist/viewers/excel/styles.d.ts +1 -0
  234. package/dist/viewers/excel/styles.js +216 -0
  235. package/dist/viewers/fallback/index.d.ts +17 -0
  236. package/dist/viewers/fallback/index.js +130 -0
  237. package/dist/viewers/fallback/styles.d.ts +1 -0
  238. package/dist/viewers/fallback/styles.js +49 -0
  239. package/dist/viewers/hdf5/index.d.ts +19 -0
  240. package/dist/viewers/hdf5/index.js +170 -0
  241. package/dist/viewers/hdf5/styles.d.ts +1 -0
  242. package/dist/viewers/hdf5/styles.js +4 -0
  243. package/dist/viewers/hwp/controller.d.ts +19 -0
  244. package/dist/viewers/hwp/controller.js +22 -0
  245. package/dist/viewers/hwp/index.d.ts +39 -0
  246. package/dist/viewers/hwp/index.js +271 -0
  247. package/dist/viewers/hwp/self-loading.d.ts +2 -0
  248. package/dist/viewers/hwp/self-loading.js +6 -0
  249. package/dist/viewers/hwp/styles.d.ts +1 -0
  250. package/dist/viewers/hwp/styles.js +13 -0
  251. package/dist/viewers/image/controller.d.ts +210 -0
  252. package/dist/viewers/image/controller.js +336 -0
  253. package/dist/viewers/image/decode.d.ts +54 -0
  254. package/dist/viewers/image/decode.js +150 -0
  255. package/dist/viewers/image/index.d.ts +38 -0
  256. package/dist/viewers/image/index.js +749 -0
  257. package/dist/viewers/image/limits.d.ts +18 -0
  258. package/dist/viewers/image/limits.js +20 -0
  259. package/dist/viewers/image/styles.d.ts +1 -0
  260. package/dist/viewers/image/styles.js +119 -0
  261. package/dist/viewers/json/controller.d.ts +89 -0
  262. package/dist/viewers/json/controller.js +411 -0
  263. package/dist/viewers/json/index.d.ts +25 -0
  264. package/dist/viewers/json/index.js +503 -0
  265. package/dist/viewers/json/styles.d.ts +1 -0
  266. package/dist/viewers/json/styles.js +346 -0
  267. package/dist/viewers/json/transforms.d.ts +27 -0
  268. package/dist/viewers/json/transforms.js +253 -0
  269. package/dist/viewers/jsonl/controller.d.ts +48 -0
  270. package/dist/viewers/jsonl/controller.js +136 -0
  271. package/dist/viewers/jsonl/index.d.ts +22 -0
  272. package/dist/viewers/jsonl/index.js +184 -0
  273. package/dist/viewers/jsonl/styles.d.ts +1 -0
  274. package/dist/viewers/jsonl/styles.js +1 -0
  275. package/dist/viewers/markdown/controller.d.ts +50 -0
  276. package/dist/viewers/markdown/controller.js +62 -0
  277. package/dist/viewers/markdown/index.d.ts +64 -0
  278. package/dist/viewers/markdown/index.js +425 -0
  279. package/dist/viewers/markdown/math.d.ts +21 -0
  280. package/dist/viewers/markdown/math.js +100 -0
  281. package/dist/viewers/markdown/self-loading.d.ts +5 -0
  282. package/dist/viewers/markdown/self-loading.js +48 -0
  283. package/dist/viewers/markdown/styles.d.ts +1 -0
  284. package/dist/viewers/markdown/styles.js +11 -0
  285. package/dist/viewers/mat/index.d.ts +19 -0
  286. package/dist/viewers/mat/index.js +113 -0
  287. package/dist/viewers/mat/styles.d.ts +1 -0
  288. package/dist/viewers/mat/styles.js +3 -0
  289. package/dist/viewers/media.d.ts +9 -0
  290. package/dist/viewers/media.js +77 -0
  291. package/dist/viewers/mermaid/index.d.ts +17 -0
  292. package/dist/viewers/mermaid/index.js +6 -0
  293. package/dist/viewers/mermaid/self-loading.d.ts +2 -0
  294. package/dist/viewers/mermaid/self-loading.js +2 -0
  295. package/dist/viewers/mf4/index.d.ts +15 -0
  296. package/dist/viewers/mf4/index.js +5 -0
  297. package/dist/viewers/parquet/controller.d.ts +37 -0
  298. package/dist/viewers/parquet/controller.js +26 -0
  299. package/dist/viewers/parquet/index.d.ts +23 -0
  300. package/dist/viewers/parquet/index.js +134 -0
  301. package/dist/viewers/parquet/styles.d.ts +1 -0
  302. package/dist/viewers/parquet/styles.js +3 -0
  303. package/dist/viewers/pcap/index.d.ts +15 -0
  304. package/dist/viewers/pcap/index.js +5 -0
  305. package/dist/viewers/pcapng/index.d.ts +15 -0
  306. package/dist/viewers/pcapng/index.js +5 -0
  307. package/dist/viewers/pdf/controller.d.ts +120 -0
  308. package/dist/viewers/pdf/controller.js +191 -0
  309. package/dist/viewers/pdf/editing.d.ts +26 -0
  310. package/dist/viewers/pdf/editing.js +265 -0
  311. package/dist/viewers/pdf/index.d.ts +88 -0
  312. package/dist/viewers/pdf/index.js +1400 -0
  313. package/dist/viewers/pdf/self-loading.d.ts +6 -0
  314. package/dist/viewers/pdf/self-loading.js +14 -0
  315. package/dist/viewers/pdf/styles.d.ts +1 -0
  316. package/dist/viewers/pdf/styles.js +555 -0
  317. package/dist/viewers/plantuml/index.d.ts +17 -0
  318. package/dist/viewers/plantuml/index.js +6 -0
  319. package/dist/viewers/plantuml/self-loading.d.ts +2 -0
  320. package/dist/viewers/plantuml/self-loading.js +2 -0
  321. package/dist/viewers/ppt/chart.d.ts +8 -0
  322. package/dist/viewers/ppt/chart.js +83 -0
  323. package/dist/viewers/ppt/controller.d.ts +34 -0
  324. package/dist/viewers/ppt/controller.js +27 -0
  325. package/dist/viewers/ppt/index.d.ts +33 -0
  326. package/dist/viewers/ppt/index.js +101 -0
  327. package/dist/viewers/ppt/render.d.ts +5 -0
  328. package/dist/viewers/ppt/render.js +144 -0
  329. package/dist/viewers/ppt/self-loading.d.ts +9 -0
  330. package/dist/viewers/ppt/self-loading.js +4 -0
  331. package/dist/viewers/ppt/styles.d.ts +1 -0
  332. package/dist/viewers/ppt/styles.js +1 -0
  333. package/dist/viewers/proto/index.d.ts +17 -0
  334. package/dist/viewers/proto/index.js +170 -0
  335. package/dist/viewers/proto/styles.d.ts +1 -0
  336. package/dist/viewers/proto/styles.js +2 -0
  337. package/dist/viewers/psd/index.d.ts +27 -0
  338. package/dist/viewers/psd/index.js +39 -0
  339. package/dist/viewers/psd/self-loading.d.ts +10 -0
  340. package/dist/viewers/psd/self-loading.js +20 -0
  341. package/dist/viewers/psd/styles.d.ts +1 -0
  342. package/dist/viewers/psd/styles.js +1 -0
  343. package/dist/viewers/reqif/index.d.ts +15 -0
  344. package/dist/viewers/reqif/index.js +5 -0
  345. package/dist/viewers/shapefile/index.d.ts +35 -0
  346. package/dist/viewers/shapefile/index.js +336 -0
  347. package/dist/viewers/shapefile/self-loading.d.ts +7 -0
  348. package/dist/viewers/shapefile/self-loading.js +32 -0
  349. package/dist/viewers/shapefile/styles.d.ts +1 -0
  350. package/dist/viewers/shapefile/styles.js +28 -0
  351. package/dist/viewers/stp/index.d.ts +15 -0
  352. package/dist/viewers/stp/index.js +5 -0
  353. package/dist/viewers/structured-styles.d.ts +1 -0
  354. package/dist/viewers/structured-styles.js +1 -0
  355. package/dist/viewers/structured.d.ts +36 -0
  356. package/dist/viewers/structured.js +138 -0
  357. package/dist/viewers/toml/controller.d.ts +38 -0
  358. package/dist/viewers/toml/controller.js +61 -0
  359. package/dist/viewers/toml/index.d.ts +18 -0
  360. package/dist/viewers/toml/index.js +34 -0
  361. package/dist/viewers/types.d.ts +36 -0
  362. package/dist/viewers/types.js +9 -0
  363. package/dist/viewers/video/controller.d.ts +29 -0
  364. package/dist/viewers/video/controller.js +54 -0
  365. package/dist/viewers/video/index.d.ts +18 -0
  366. package/dist/viewers/video/index.js +305 -0
  367. package/dist/viewers/video/styles.d.ts +1 -0
  368. package/dist/viewers/video/styles.js +79 -0
  369. package/dist/viewers/word/controller.d.ts +19 -0
  370. package/dist/viewers/word/controller.js +22 -0
  371. package/dist/viewers/word/docx-preprocess.d.ts +63 -0
  372. package/dist/viewers/word/docx-preprocess.js +136 -0
  373. package/dist/viewers/word/index.d.ts +32 -0
  374. package/dist/viewers/word/index.js +187 -0
  375. package/dist/viewers/word/normalize-docx.d.ts +2 -0
  376. package/dist/viewers/word/normalize-docx.js +50 -0
  377. package/dist/viewers/word/paginate.d.ts +1 -0
  378. package/dist/viewers/word/paginate.js +1035 -0
  379. package/dist/viewers/word/self-loading.d.ts +2 -0
  380. package/dist/viewers/word/self-loading.js +7 -0
  381. package/dist/viewers/word/styles.d.ts +1 -0
  382. package/dist/viewers/word/styles.js +686 -0
  383. package/dist/viewers/yaml/controller.d.ts +42 -0
  384. package/dist/viewers/yaml/controller.js +41 -0
  385. package/dist/viewers/yaml/index.d.ts +21 -0
  386. package/dist/viewers/yaml/index.js +20 -0
  387. package/package.json +375 -0
@@ -0,0 +1,3747 @@
1
+ // @ts-nocheck — verbatim upstream parser; strict migration is tracked separately.
2
+ import { Buffer } from 'buffer';
3
+ let XLSX;
4
+ let JSZip;
5
+ const FREESECT = 0xffffffff;
6
+ const ENDOFCHAIN = 0xfffffffe;
7
+ const ANSI_DECODERS = [
8
+ { name: 'windows-1252', decoder: new TextDecoder('windows-1252') },
9
+ { name: 'euc-kr', decoder: new TextDecoder('euc-kr') },
10
+ { name: 'shift_jis', decoder: new TextDecoder('shift_jis') }
11
+ ];
12
+ export class DocBinaryParser {
13
+ static FIELD_CODE_NOISE_PATTERN = /\b(?:HYPERLINK|PAGEREF|TOC|REF)\b\s+"[^"]*"\s*/gi;
14
+ static HEADER_FOOTER_FIELD_PATTERN = /\b(?:PAGE|NUMPAGES|SECTIONPAGE|SECTIONPAGES|SECTION|SECTIONS)\b/gi;
15
+ static async parseBytes(input, deps = {}) {
16
+ XLSX = deps.xlsx;
17
+ JSZip = deps.jszip;
18
+ const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
19
+ const cfb = this.parseCfb(buffer);
20
+ const wordStream = cfb.getStream('WordDocument');
21
+ if (!wordStream) {
22
+ throw new Error('Invalid .doc file: missing WordDocument stream.');
23
+ }
24
+ const fib = this.parseFib(wordStream);
25
+ const extracted = this.extractDocumentText(cfb, wordStream, fib);
26
+ const objectPlacementMode = this.detectEmbeddedObjectPlacementMode(cfb, buffer);
27
+ const images = this.extractImages(cfb);
28
+ const embeddedTables = this.extractEmbeddedWorkbookTables(cfb, objectPlacementMode);
29
+ const embeddedPackageCharts = await this.extractEmbeddedPackageCharts(cfb, objectPlacementMode);
30
+ const blocks = this.buildDocumentBlocks(extracted.text, images, extracted.styledParagraphs);
31
+ if (blocks.length > 0 || embeddedTables.length > 0 || embeddedPackageCharts.length > 0) {
32
+ const combinedBlocks = this.composeDocumentBlocks(blocks, embeddedPackageCharts, embeddedTables, images);
33
+ return this.wrapLegacyHtml(this.buildLegacySections(combinedBlocks, extracted.headerFooterBySection));
34
+ }
35
+ return '<div class="ov-doc-legacy-empty">No readable text content found in this .doc file.</div>';
36
+ }
37
+ static extractDocumentText(cfb, wordStream, fib) {
38
+ const candidates = [];
39
+ const preferredTableStream = cfb.getStream(fib.tableStreamName);
40
+ if (preferredTableStream) {
41
+ const styleRuns = this.extractCharacterStyleRuns(wordStream, preferredTableStream, fib);
42
+ const paragraphStyleRuns = this.extractParagraphStyleRuns(wordStream, preferredTableStream, fib);
43
+ const sectionBoundaries = this.extractSectionBoundaries(preferredTableStream, fib, wordStream);
44
+ const clxCandidate = this.extractFromClx(wordStream, preferredTableStream, fib);
45
+ if (clxCandidate) {
46
+ clxCandidate.styledParagraphs = this.buildStyledParagraphs(clxCandidate, styleRuns, paragraphStyleRuns, sectionBoundaries);
47
+ clxCandidate.headerFooterBySection = this.extractHeaderFooterBySection(clxCandidate.decodedSegments, preferredTableStream, fib);
48
+ candidates.push(clxCandidate);
49
+ }
50
+ const candidate = this.extractFromPieceTable(wordStream, preferredTableStream);
51
+ if (candidate) {
52
+ candidate.styledParagraphs = this.buildStyledParagraphs(candidate, styleRuns, paragraphStyleRuns, sectionBoundaries);
53
+ candidate.headerFooterBySection = this.extractHeaderFooterBySection(candidate.decodedSegments, preferredTableStream, fib);
54
+ candidates.push(candidate);
55
+ }
56
+ }
57
+ const bestCandidate = candidates.sort((a, b) => b.score - a.score)[0];
58
+ if (bestCandidate && bestCandidate.text.trim().length >= 40) {
59
+ return bestCandidate;
60
+ }
61
+ return {
62
+ text: this.extractFallbackText(wordStream, cfb, fib),
63
+ score: 0
64
+ };
65
+ }
66
+ static extractSectionBoundaries(tableStream, fib, wordStream) {
67
+ if (fib.lcbPlcfSed <= 0 || fib.fcPlcfSed < 0 || fib.fcPlcfSed + fib.lcbPlcfSed > tableStream.length) {
68
+ return [];
69
+ }
70
+ const plc = tableStream.subarray(fib.fcPlcfSed, fib.fcPlcfSed + fib.lcbPlcfSed);
71
+ const possibleSedSize = 12;
72
+ const numerator = plc.length - 4;
73
+ const denominator = possibleSedSize + 4;
74
+ if (numerator <= 0 || numerator % denominator !== 0) {
75
+ return [];
76
+ }
77
+ const sectionCount = numerator / denominator;
78
+ if (sectionCount <= 0) {
79
+ return [];
80
+ }
81
+ const cps = [];
82
+ for (let index = 0; index < sectionCount + 1; index++) {
83
+ cps.push(plc.readUInt32LE(index * 4));
84
+ }
85
+ const boundaries = [];
86
+ for (let index = 0; index < sectionCount; index++) {
87
+ const cpStart = cps[index];
88
+ const cpEnd = cps[index + 1];
89
+ if (cpEnd <= cpStart) {
90
+ continue;
91
+ }
92
+ boundaries.push({
93
+ sectionIndex: index,
94
+ cpStart,
95
+ cpEnd,
96
+ layout: this.extractSectionLayoutFromSed(wordStream, plc, sectionCount, index)
97
+ });
98
+ }
99
+ return boundaries;
100
+ }
101
+ static extractHeaderFooterBySection(segments, tableStream, fib) {
102
+ const sectionMap = new Map();
103
+ if (!segments || segments.length === 0 || fib.lcbPlcfHdd <= 0 || fib.ccpHdd <= 0) {
104
+ return sectionMap;
105
+ }
106
+ if (fib.fcPlcfHdd < 0 || fib.fcPlcfHdd + fib.lcbPlcfHdd > tableStream.length) {
107
+ return sectionMap;
108
+ }
109
+ const plc = tableStream.subarray(fib.fcPlcfHdd, fib.fcPlcfHdd + fib.lcbPlcfHdd);
110
+ if (plc.length < 12 || plc.length % 4 !== 0) {
111
+ return sectionMap;
112
+ }
113
+ const cpCount = plc.length / 4;
114
+ const storyCount = cpCount - 2;
115
+ if (storyCount <= 0) {
116
+ return sectionMap;
117
+ }
118
+ const cps = [];
119
+ for (let index = 0; index < cpCount; index++) {
120
+ cps.push(plc.readUInt32LE(index * 4));
121
+ }
122
+ const hddBaseCp = fib.ccpText + fib.ccpFtn;
123
+ const resolveStoryText = (storyIndex) => {
124
+ if (storyIndex < 0 || storyIndex >= storyCount) {
125
+ return undefined;
126
+ }
127
+ const startCp = hddBaseCp + cps[storyIndex];
128
+ const endCp = hddBaseCp + cps[storyIndex + 1];
129
+ if (endCp <= startCp) {
130
+ return undefined;
131
+ }
132
+ return this.extractTextForCpRange(segments, startCp, endCp);
133
+ };
134
+ const resolveInheritedStoryText = (sectionIndex, storyOffset) => {
135
+ for (let currentSection = sectionIndex; currentSection >= 0; currentSection--) {
136
+ const storyText = resolveStoryText(6 + currentSection * 6 + storyOffset);
137
+ if (storyText) {
138
+ return storyText;
139
+ }
140
+ }
141
+ return undefined;
142
+ };
143
+ const sectionCount = Math.max(0, Math.floor((storyCount - 6) / 6));
144
+ for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
145
+ sectionMap.set(sectionIndex, {
146
+ sectionNumber: sectionIndex + 1,
147
+ sectionCount,
148
+ evenHeaderText: resolveInheritedStoryText(sectionIndex, 0),
149
+ oddHeaderText: resolveInheritedStoryText(sectionIndex, 1),
150
+ evenFooterText: resolveInheritedStoryText(sectionIndex, 2),
151
+ oddFooterText: resolveInheritedStoryText(sectionIndex, 3),
152
+ firstHeaderText: resolveInheritedStoryText(sectionIndex, 4),
153
+ firstFooterText: resolveInheritedStoryText(sectionIndex, 5)
154
+ });
155
+ }
156
+ return sectionMap;
157
+ }
158
+ static extractTextForCpRange(segments, startCp, endCp) {
159
+ if (endCp <= startCp) {
160
+ return undefined;
161
+ }
162
+ let text = '';
163
+ for (const segment of segments) {
164
+ const segmentStart = segment.cpStart;
165
+ const segmentEnd = segment.cpStart + segment.text.length;
166
+ if (segmentEnd <= startCp || segmentStart >= endCp) {
167
+ continue;
168
+ }
169
+ const localStart = Math.max(0, startCp - segmentStart);
170
+ const localEnd = Math.min(segment.text.length, endCp - segmentStart);
171
+ text += segment.text.slice(localStart, localEnd);
172
+ }
173
+ const normalized = text
174
+ .replace(/\u0007/g, '\n')
175
+ .replace(/\r/g, '\n')
176
+ .replace(/[ \t]+\n/g, '\n')
177
+ .replace(/\n{3,}/g, '\n\n')
178
+ .trim();
179
+ if (!normalized.length) {
180
+ return undefined;
181
+ }
182
+ const cleaned = this.normalizeHeaderFooterText(normalized);
183
+ return cleaned.length > 0 ? cleaned : undefined;
184
+ }
185
+ static normalizeHeaderFooterText(raw) {
186
+ return (raw || '')
187
+ .replace(this.FIELD_CODE_NOISE_PATTERN, '')
188
+ .replace(/[\u0000-\u001f]+/g, ' ')
189
+ .replace(/[ ]{2,}/g, ' ')
190
+ .replace(/\s*\n\s*/g, '\n')
191
+ .trim();
192
+ }
193
+ static extractSectionLayoutFromSed(wordStream, plc, sectionCount, sectionIndex) {
194
+ const sedSize = 12;
195
+ const sedOffset = (sectionCount + 1) * 4 + sectionIndex * sedSize;
196
+ if (sedOffset < 0 || sedOffset + sedSize > plc.length) {
197
+ return undefined;
198
+ }
199
+ const fcSepx = plc.readInt32LE(sedOffset + 2);
200
+ if (fcSepx <= 0 || fcSepx + 2 > wordStream.length) {
201
+ return undefined;
202
+ }
203
+ return this.parseSepxLayout(wordStream, fcSepx);
204
+ }
205
+ static parseSepxLayout(wordStream, fcSepx) {
206
+ if (fcSepx < 0 || fcSepx + 2 > wordStream.length) {
207
+ return undefined;
208
+ }
209
+ const cb = wordStream.readUInt16LE(fcSepx);
210
+ const grpprlOffset = fcSepx + 2;
211
+ if (cb <= 0 || grpprlOffset + cb > wordStream.length) {
212
+ return undefined;
213
+ }
214
+ return this.parseSectionGrpprl(wordStream.subarray(grpprlOffset, grpprlOffset + cb));
215
+ }
216
+ static parseSectionGrpprl(grpprl) {
217
+ const defaults = this.defaultLegacyLayoutMetrics();
218
+ let layout = { ...defaults };
219
+ let hasOverride = false;
220
+ let orientationLandscape = false;
221
+ let evenlySpacedColumns = false;
222
+ let offset = 0;
223
+ while (offset + 2 <= grpprl.length) {
224
+ const sprm = grpprl.readUInt16LE(offset);
225
+ const operandLength = this.getSprmOperandLength(sprm, grpprl, offset + 2);
226
+ if (operandLength < 0 || offset + 2 + operandLength > grpprl.length) {
227
+ break;
228
+ }
229
+ const operandOffset = offset + 2;
230
+ switch (sprm) {
231
+ case 0xF203:
232
+ if (operandLength >= 3) {
233
+ const columnIndex = grpprl[operandOffset];
234
+ const value = grpprl.readUInt16LE(operandOffset + 1);
235
+ layout.explicitColumnWidthsTwips[columnIndex] = value;
236
+ hasOverride = true;
237
+ }
238
+ break;
239
+ case 0xF204:
240
+ if (operandLength >= 3) {
241
+ const columnIndex = grpprl[operandOffset];
242
+ const value = grpprl.readUInt16LE(operandOffset + 1);
243
+ layout.explicitColumnSpacingsTwips[columnIndex] = value;
244
+ hasOverride = true;
245
+ }
246
+ break;
247
+ case 0x500B:
248
+ if (operandLength >= 2) {
249
+ layout.columns = Math.max(1, grpprl.readUInt16LE(operandOffset));
250
+ hasOverride = true;
251
+ }
252
+ break;
253
+ case 0x900C:
254
+ if (operandLength >= 2) {
255
+ layout.columnGapTwips = grpprl.readUInt16LE(operandOffset);
256
+ hasOverride = true;
257
+ }
258
+ break;
259
+ case 0xB01F:
260
+ if (operandLength >= 2) {
261
+ layout.pageWidthTwips = grpprl.readUInt16LE(operandOffset);
262
+ hasOverride = true;
263
+ }
264
+ break;
265
+ case 0x3005:
266
+ evenlySpacedColumns = grpprl[operandOffset] !== 0;
267
+ hasOverride = true;
268
+ break;
269
+ case 0xB017:
270
+ if (operandLength >= 2) {
271
+ layout.headerTopTwips = grpprl.readUInt16LE(operandOffset);
272
+ hasOverride = true;
273
+ }
274
+ break;
275
+ case 0xB018:
276
+ if (operandLength >= 2) {
277
+ layout.footerBottomTwips = grpprl.readUInt16LE(operandOffset);
278
+ hasOverride = true;
279
+ }
280
+ break;
281
+ case 0x3019:
282
+ layout.lineBetweenColumns = grpprl[operandOffset] !== 0;
283
+ hasOverride = true;
284
+ break;
285
+ case 0xB020:
286
+ if (operandLength >= 2) {
287
+ layout.pageHeightTwips = grpprl.readUInt16LE(operandOffset);
288
+ hasOverride = true;
289
+ }
290
+ break;
291
+ case 0xB021:
292
+ if (operandLength >= 2) {
293
+ layout.marginLeftTwips = grpprl.readUInt16LE(operandOffset);
294
+ hasOverride = true;
295
+ }
296
+ break;
297
+ case 0xB022:
298
+ if (operandLength >= 2) {
299
+ layout.marginRightTwips = grpprl.readUInt16LE(operandOffset);
300
+ hasOverride = true;
301
+ }
302
+ break;
303
+ case 0x9023:
304
+ if (operandLength >= 2) {
305
+ layout.marginTopTwips = Math.max(0, grpprl.readInt16LE(operandOffset));
306
+ hasOverride = true;
307
+ }
308
+ break;
309
+ case 0x9024:
310
+ if (operandLength >= 2) {
311
+ layout.marginBottomTwips = Math.max(0, grpprl.readInt16LE(operandOffset));
312
+ hasOverride = true;
313
+ }
314
+ break;
315
+ case 0xB025:
316
+ if (operandLength >= 2) {
317
+ layout.gutterTwips = grpprl.readUInt16LE(operandOffset);
318
+ hasOverride = true;
319
+ }
320
+ break;
321
+ case 0x301D:
322
+ orientationLandscape = grpprl[operandOffset] !== 0;
323
+ hasOverride = true;
324
+ break;
325
+ case 0x322A:
326
+ layout.rtlGutter = grpprl[operandOffset] !== 0;
327
+ hasOverride = true;
328
+ break;
329
+ default:
330
+ break;
331
+ }
332
+ offset += 2 + operandLength;
333
+ }
334
+ if (!hasOverride) {
335
+ return undefined;
336
+ }
337
+ if (orientationLandscape && layout.pageHeightTwips > layout.pageWidthTwips) {
338
+ [layout.pageWidthTwips, layout.pageHeightTwips] = [layout.pageHeightTwips, layout.pageWidthTwips];
339
+ }
340
+ if (layout.columns < 1) {
341
+ layout.columns = 1;
342
+ }
343
+ if (!evenlySpacedColumns && layout.columns > 1) {
344
+ layout.columns = Math.max(1, layout.columns);
345
+ }
346
+ if (layout.explicitColumnWidthsTwips.length > 0) {
347
+ layout.columns = Math.max(layout.columns, layout.explicitColumnWidthsTwips.filter((value) => value > 0).length);
348
+ }
349
+ return layout;
350
+ }
351
+ static parseFib(wordStream) {
352
+ if (wordStream.length < 32) {
353
+ throw new Error('Invalid WordDocument stream: missing FIB base.');
354
+ }
355
+ const nFib = wordStream.readUInt16LE(2);
356
+ const flags = wordStream.readUInt16LE(10);
357
+ const tableStreamName = ((flags >> 9) & 0x0001) === 1 ? '1Table' : '0Table';
358
+ let offset = 32;
359
+ const csw = wordStream.readUInt16LE(offset);
360
+ offset += 2 + csw * 2;
361
+ const cslw = wordStream.readUInt16LE(offset);
362
+ offset += 2;
363
+ const fibRgLwOffset = offset;
364
+ const fibRgLwLength = cslw * 4;
365
+ const ccpText = fibRgLwLength >= 16 && fibRgLwOffset + 16 <= wordStream.length
366
+ ? wordStream.readUInt32LE(fibRgLwOffset + 12)
367
+ : 0;
368
+ const ccpFtn = fibRgLwLength >= 20 && fibRgLwOffset + 20 <= wordStream.length
369
+ ? wordStream.readUInt32LE(fibRgLwOffset + 16)
370
+ : 0;
371
+ const ccpHdd = fibRgLwLength >= 24 && fibRgLwOffset + 24 <= wordStream.length
372
+ ? wordStream.readUInt32LE(fibRgLwOffset + 20)
373
+ : 0;
374
+ offset += fibRgLwLength;
375
+ const cbRgFcLcb = wordStream.readUInt16LE(offset);
376
+ offset += 2;
377
+ const fibRgFcLcbOffset = offset;
378
+ const readPair = (pairIndex) => {
379
+ const pairOffset = fibRgFcLcbOffset + pairIndex * 8;
380
+ if (pairIndex >= cbRgFcLcb || pairOffset + 8 > wordStream.length) {
381
+ return { fc: 0, lcb: 0 };
382
+ }
383
+ return {
384
+ fc: wordStream.readUInt32LE(pairOffset),
385
+ lcb: wordStream.readUInt32LE(pairOffset + 4)
386
+ };
387
+ };
388
+ const sed = readPair(6);
389
+ const hdd = readPair(11);
390
+ const clx = readPair(33);
391
+ const chpx = readPair(12);
392
+ const papx = readPair(13);
393
+ return {
394
+ nFib,
395
+ tableStreamName,
396
+ ccpText,
397
+ ccpFtn,
398
+ ccpHdd,
399
+ fcPlcfSed: sed.fc,
400
+ lcbPlcfSed: sed.lcb,
401
+ fcPlcfHdd: hdd.fc,
402
+ lcbPlcfHdd: hdd.lcb,
403
+ fcClx: clx.fc,
404
+ lcbClx: clx.lcb,
405
+ fcPlcfBteChpx: chpx.fc,
406
+ lcbPlcfBteChpx: chpx.lcb,
407
+ fcPlcfBtePapx: papx.fc,
408
+ lcbPlcfBtePapx: papx.lcb
409
+ };
410
+ }
411
+ static detectEmbeddedObjectPlacementMode(cfb, fileBuffer) {
412
+ const markerStreams = cfb.listStreams()
413
+ .map((stream) => stream.name)
414
+ .join('\n');
415
+ const markerPattern = /ObjectPool|MsoDataStore|OfficeArt|Escher|Drawing/i;
416
+ if (markerPattern.test(markerStreams)) {
417
+ return 'drawing-anchor';
418
+ }
419
+ const probeBuffers = [
420
+ fileBuffer,
421
+ cfb.getStream('WordDocument'),
422
+ cfb.getStream('1Table'),
423
+ cfb.getStream('0Table'),
424
+ cfb.getStream('Data')
425
+ ].filter((buffer) => Buffer.isBuffer(buffer));
426
+ const markerBytes = [/MSODRAWING/i, /OfficeArt/i, /Escher/i, /FSPA/i, /PlcfSpa/i];
427
+ for (const probe of probeBuffers) {
428
+ const ascii = probe.toString('latin1');
429
+ if (markerBytes.some((pattern) => pattern.test(ascii))) {
430
+ return 'drawing-anchor';
431
+ }
432
+ }
433
+ return 'text-flow';
434
+ }
435
+ static extractFromClx(wordStream, tableStream, fib) {
436
+ if (fib.lcbClx <= 0 || fib.fcClx < 0 || fib.fcClx + fib.lcbClx > tableStream.length) {
437
+ return null;
438
+ }
439
+ const clx = tableStream.subarray(fib.fcClx, fib.fcClx + fib.lcbClx);
440
+ let offset = 0;
441
+ while (offset < clx.length && clx[offset] === 0x01) {
442
+ if (offset + 3 > clx.length) {
443
+ return null;
444
+ }
445
+ const cbGrpprl = clx.readUInt16LE(offset + 1);
446
+ offset += 3 + cbGrpprl;
447
+ }
448
+ if (offset + 5 > clx.length || clx[offset] !== 0x02) {
449
+ return null;
450
+ }
451
+ const lcb = clx.readUInt32LE(offset + 1);
452
+ if (lcb < 16 || offset + 5 + lcb > clx.length) {
453
+ return null;
454
+ }
455
+ const plcPcd = clx.subarray(offset + 5, offset + 5 + lcb);
456
+ const pieceCount = (lcb - 4) / 12;
457
+ if (pieceCount <= 0 || !Number.isInteger(pieceCount)) {
458
+ return null;
459
+ }
460
+ return this.decodePieceTable(wordStream, plcPcd, pieceCount, 2)
461
+ ?? this.decodePieceTable(wordStream, plcPcd, pieceCount, 0);
462
+ }
463
+ static extractFromPieceTable(wordStream, tableStream) {
464
+ let best = null;
465
+ for (let offset = 0; offset + 5 < tableStream.length; offset++) {
466
+ if (tableStream[offset] !== 0x02) {
467
+ continue;
468
+ }
469
+ const lcb = tableStream.readUInt32LE(offset + 1);
470
+ if (lcb < 16 || offset + 5 + lcb > tableStream.length) {
471
+ continue;
472
+ }
473
+ if ((lcb - 4) % 12 !== 0) {
474
+ continue;
475
+ }
476
+ const pieceCount = (lcb - 4) / 12;
477
+ if (pieceCount <= 0 || pieceCount > 200000) {
478
+ continue;
479
+ }
480
+ const plcPcd = tableStream.subarray(offset + 5, offset + 5 + lcb);
481
+ for (const fcOffset of [2, 0]) {
482
+ const candidate = this.decodePieceTable(wordStream, plcPcd, pieceCount, fcOffset);
483
+ if (!candidate) {
484
+ continue;
485
+ }
486
+ if (!best || candidate.score > best.score) {
487
+ best = candidate;
488
+ }
489
+ }
490
+ }
491
+ return best;
492
+ }
493
+ static decodePieceTable(wordStream, plcPcd, pieceCount, fcOffset) {
494
+ const cpCount = pieceCount + 1;
495
+ const cpByteLength = cpCount * 4;
496
+ if (cpByteLength >= plcPcd.length) {
497
+ return null;
498
+ }
499
+ const cps = [];
500
+ for (let i = 0; i < cpCount; i++) {
501
+ cps.push(plcPcd.readUInt32LE(i * 4));
502
+ }
503
+ if (!this.isValidCpSequence(cps)) {
504
+ return null;
505
+ }
506
+ const decodedByAnsiDecoder = new Map();
507
+ const segmentsByAnsiDecoder = new Map();
508
+ for (const { name } of ANSI_DECODERS) {
509
+ decodedByAnsiDecoder.set(name, []);
510
+ segmentsByAnsiDecoder.set(name, []);
511
+ }
512
+ for (let i = 0; i < pieceCount; i++) {
513
+ const charCount = cps[i + 1] - cps[i];
514
+ if (charCount <= 0) {
515
+ continue;
516
+ }
517
+ const pcdOffset = cpByteLength + i * 8;
518
+ if (pcdOffset + fcOffset + 4 > plcPcd.length) {
519
+ return null;
520
+ }
521
+ const fcRaw = plcPcd.readUInt32LE(pcdOffset + fcOffset);
522
+ const compressed = (fcRaw & 0x40000000) !== 0;
523
+ const byteOffset = compressed ? ((fcRaw & 0x3fffffff) >>> 1) : (fcRaw & 0x3fffffff);
524
+ const byteLength = compressed ? charCount : charCount * 2;
525
+ if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > wordStream.length) {
526
+ return null;
527
+ }
528
+ const pieceBuffer = wordStream.subarray(byteOffset, wordStream.length >= byteOffset + byteLength ? byteOffset + byteLength : wordStream.length);
529
+ if (compressed) {
530
+ for (const { name, decoder } of ANSI_DECODERS) {
531
+ const pieceText = decoder.decode(pieceBuffer);
532
+ decodedByAnsiDecoder.get(name)?.push(pieceText);
533
+ segmentsByAnsiDecoder.get(name)?.push({
534
+ text: pieceText,
535
+ cpStart: cps[i],
536
+ fcStart: byteOffset,
537
+ bytesPerChar: 1
538
+ });
539
+ }
540
+ }
541
+ else {
542
+ const pieceText = pieceBuffer.toString('utf16le');
543
+ for (const { name } of ANSI_DECODERS) {
544
+ decodedByAnsiDecoder.get(name)?.push(pieceText);
545
+ segmentsByAnsiDecoder.get(name)?.push({
546
+ text: pieceText,
547
+ cpStart: cps[i],
548
+ fcStart: byteOffset,
549
+ bytesPerChar: 2
550
+ });
551
+ }
552
+ }
553
+ }
554
+ return this.selectBestDecodedCandidate(decodedByAnsiDecoder, segmentsByAnsiDecoder, cps[0] === 0, pieceCount);
555
+ }
556
+ static isValidCpSequence(cps) {
557
+ if (cps.length < 2) {
558
+ return false;
559
+ }
560
+ for (let i = 1; i < cps.length; i++) {
561
+ if (cps[i] < cps[i - 1]) {
562
+ return false;
563
+ }
564
+ }
565
+ const totalChars = cps[cps.length - 1] - cps[0];
566
+ return totalChars > 0 && totalChars <= 10_000_000;
567
+ }
568
+ static scoreExtractedText(text, startsAtZero, pieceCount) {
569
+ const paragraphs = text
570
+ .split(/\n+/)
571
+ .map((part) => part.trim())
572
+ .filter(Boolean);
573
+ const readableChars = (text.match(/[A-Za-z0-9가-힣]/g) || []).length;
574
+ const penalty = (text.match(/[\uFFFD]/g) || []).length * 30;
575
+ const hangulChars = (text.match(/[가-힣]/g) || []).length;
576
+ const suspiciousGlyphs = (text.match(/[ÃÂÐÑØÞãäåæçðñøþ]/g) || []).length * 12;
577
+ const controlLike = (text.match(/[^\x20-\x7E\u00A0-\u024F\u3131-\u318E\uAC00-\uD7A3\r\n\t]/g) || []).length * 6;
578
+ return text.length
579
+ + paragraphs.length * 120
580
+ + readableChars * 2
581
+ + hangulChars * 3
582
+ + (startsAtZero ? 200 : 0)
583
+ + Math.min(pieceCount, 2000)
584
+ - penalty
585
+ - suspiciousGlyphs
586
+ - controlLike;
587
+ }
588
+ static extractFallbackText(wordStream, cfb, fib) {
589
+ const sources = [
590
+ wordStream,
591
+ cfb.getStream(fib.tableStreamName)
592
+ ].filter((buffer) => Boolean(buffer));
593
+ const utf16Candidates = [];
594
+ const ansiCandidates = new Map();
595
+ for (const { name } of ANSI_DECODERS) {
596
+ ansiCandidates.set(name, []);
597
+ }
598
+ for (const source of sources) {
599
+ utf16Candidates.push(...this.extractReadableSegments(source.toString('utf16le')));
600
+ for (const { name, decoder } of ANSI_DECODERS) {
601
+ ansiCandidates.get(name)?.push(...this.extractReadableSegments(decoder.decode(source)));
602
+ }
603
+ }
604
+ const baseUtf16 = this.dedupeSegments(utf16Candidates);
605
+ let best = '';
606
+ let bestScore = Number.NEGATIVE_INFINITY;
607
+ for (const { name } of ANSI_DECODERS) {
608
+ const combined = this.dedupeSegments([...baseUtf16, ...(ansiCandidates.get(name) ?? [])]);
609
+ const normalized = this.normalizeDocumentText(combined.join('\n\n'));
610
+ if (normalized.length < 20) {
611
+ continue;
612
+ }
613
+ const score = this.scoreExtractedText(normalized, true, combined.length);
614
+ if (score > bestScore) {
615
+ best = normalized;
616
+ bestScore = score;
617
+ }
618
+ }
619
+ return best;
620
+ }
621
+ static extractCharacterStyleRuns(wordStream, tableStream, fib) {
622
+ if (fib.lcbPlcfBteChpx <= 4 || fib.fcPlcfBteChpx < 0 || fib.fcPlcfBteChpx + fib.lcbPlcfBteChpx > tableStream.length) {
623
+ return [];
624
+ }
625
+ const plcf = tableStream.subarray(fib.fcPlcfBteChpx, fib.fcPlcfBteChpx + fib.lcbPlcfBteChpx);
626
+ if ((plcf.length - 4) % 8 !== 0) {
627
+ return [];
628
+ }
629
+ const runCount = (plcf.length - 4) / 8;
630
+ const fcOffsets = [];
631
+ for (let i = 0; i < runCount + 1; i++) {
632
+ fcOffsets.push(plcf.readUInt32LE(i * 4));
633
+ }
634
+ const runs = [];
635
+ const pnOffset = (runCount + 1) * 4;
636
+ for (let i = 0; i < runCount; i++) {
637
+ const pnValue = plcf.readUInt32LE(pnOffset + i * 4);
638
+ const pn = pnValue & 0x003fffff;
639
+ const fkpOffset = pn * 512;
640
+ if (pn <= 0 || fkpOffset < 0 || fkpOffset + 512 > wordStream.length) {
641
+ continue;
642
+ }
643
+ const page = wordStream.subarray(fkpOffset, fkpOffset + 512);
644
+ runs.push(...this.parseChpxFkp(page, fcOffsets[i], fcOffsets[i + 1]));
645
+ }
646
+ return runs.sort((a, b) => a.fcStart - b.fcStart || a.fcEnd - b.fcEnd);
647
+ }
648
+ static extractParagraphStyleRuns(wordStream, tableStream, fib) {
649
+ if (fib.lcbPlcfBtePapx <= 4 || fib.fcPlcfBtePapx < 0 || fib.fcPlcfBtePapx + fib.lcbPlcfBtePapx > tableStream.length) {
650
+ return [];
651
+ }
652
+ const plcf = tableStream.subarray(fib.fcPlcfBtePapx, fib.fcPlcfBtePapx + fib.lcbPlcfBtePapx);
653
+ if ((plcf.length - 4) % 8 !== 0) {
654
+ return [];
655
+ }
656
+ const runCount = (plcf.length - 4) / 8;
657
+ const fcOffsets = [];
658
+ for (let i = 0; i < runCount + 1; i++) {
659
+ fcOffsets.push(plcf.readUInt32LE(i * 4));
660
+ }
661
+ const runs = [];
662
+ const pnOffset = (runCount + 1) * 4;
663
+ for (let i = 0; i < runCount; i++) {
664
+ const pnValue = plcf.readUInt32LE(pnOffset + i * 4);
665
+ const pn = pnValue & 0x003fffff;
666
+ const fkpOffset = pn * 512;
667
+ if (pn <= 0 || fkpOffset < 0 || fkpOffset + 512 > wordStream.length) {
668
+ continue;
669
+ }
670
+ const page = wordStream.subarray(fkpOffset, fkpOffset + 512);
671
+ runs.push(...this.parsePapxFkp(page, fcOffsets[i], fcOffsets[i + 1]));
672
+ }
673
+ return runs.sort((a, b) => a.fcStart - b.fcStart || a.fcEnd - b.fcEnd);
674
+ }
675
+ static parsePapxFkp(page, pageStartFc, pageEndFc) {
676
+ const crun = page[511];
677
+ if (crun <= 0 || 4 * (crun + 1) > 511) {
678
+ return [];
679
+ }
680
+ const rgfc = [];
681
+ for (let i = 0; i < crun + 1; i++) {
682
+ rgfc.push(page.readUInt32LE(i * 4));
683
+ }
684
+ const rgbxOffset = 4 * (crun + 1);
685
+ const runs = [];
686
+ for (let i = 0; i < crun; i++) {
687
+ const fcStart = rgfc[i];
688
+ const fcEnd = rgfc[i + 1];
689
+ if (fcStart >= fcEnd || fcEnd <= pageStartFc || fcStart >= pageEndFc) {
690
+ continue;
691
+ }
692
+ const bxOffset = rgbxOffset + i * 13;
693
+ if (bxOffset + 13 > 511) {
694
+ break;
695
+ }
696
+ const papxWordOffset = page.readUInt16LE(bxOffset);
697
+ const papxOffset = papxWordOffset * 2;
698
+ if (papxOffset <= 0 || papxOffset >= 511) {
699
+ continue;
700
+ }
701
+ const papx = this.parsePapxInFkp(page, papxOffset);
702
+ if (!papx || !this.hasParagraphLayoutStyle(papx)) {
703
+ continue;
704
+ }
705
+ runs.push({ fcStart, fcEnd, style: papx });
706
+ }
707
+ return runs;
708
+ }
709
+ static parsePapxInFkp(page, papxOffset) {
710
+ if (papxOffset >= 511) {
711
+ return null;
712
+ }
713
+ const cb = page[papxOffset];
714
+ let grpprlOffset = papxOffset + 1;
715
+ let grpprlLength = 0;
716
+ if (cb === 0) {
717
+ if (grpprlOffset >= 511) {
718
+ return null;
719
+ }
720
+ const cbExtended = page[grpprlOffset];
721
+ grpprlOffset += 1;
722
+ grpprlLength = cbExtended * 2;
723
+ }
724
+ else {
725
+ grpprlLength = Math.max(0, (cb * 2) - 1);
726
+ }
727
+ if (grpprlOffset + grpprlLength > 511 || grpprlLength < 2) {
728
+ return null;
729
+ }
730
+ const grpprlAndIstd = page.subarray(grpprlOffset, grpprlOffset + grpprlLength);
731
+ const grpprl = grpprlAndIstd.subarray(2);
732
+ return this.parseParagraphGrpprl(grpprl);
733
+ }
734
+ static parseParagraphGrpprl(grpprl) {
735
+ const style = {};
736
+ let offset = 0;
737
+ while (offset + 2 <= grpprl.length) {
738
+ const sprm = grpprl.readUInt16LE(offset);
739
+ const operandLength = this.getSprmOperandLength(sprm, grpprl, offset + 2);
740
+ if (operandLength < 0 || offset + 2 + operandLength > grpprl.length) {
741
+ break;
742
+ }
743
+ const operandOffset = offset + 2;
744
+ if (this.isExplicitTableBorderSprm(sprm)) {
745
+ style.tableHasExplicitBorders = true;
746
+ }
747
+ switch (sprm) {
748
+ case 0x2403:
749
+ style.textAlign = this.parseParagraphAlignment(grpprl[operandOffset]);
750
+ break;
751
+ case 0x2405:
752
+ style.keepLinesTogether = grpprl[operandOffset] !== 0;
753
+ break;
754
+ case 0x2406:
755
+ style.keepWithNext = grpprl[operandOffset] !== 0;
756
+ break;
757
+ case 0x2407:
758
+ style.pageBreakBefore = grpprl[operandOffset] !== 0;
759
+ break;
760
+ case 0x2416:
761
+ style.inTable = grpprl[operandOffset] !== 0;
762
+ break;
763
+ case 0x2417:
764
+ style.isTableTerminator = grpprl[operandOffset] !== 0;
765
+ break;
766
+ case 0xD608:
767
+ {
768
+ const tableDef = this.parseTDefTableOperand(grpprl.subarray(operandOffset, operandOffset + operandLength));
769
+ if (tableDef) {
770
+ style.tableColumnCount = tableDef.columnCount;
771
+ style.tableColumnWidthsTwips = tableDef.columnWidthsTwips;
772
+ style.tableCellMerges = tableDef.cellMerges;
773
+ }
774
+ }
775
+ break;
776
+ case 0xC64D:
777
+ if (operandLength >= 10) {
778
+ const shading = this.parseShdOperand(grpprl.subarray(operandOffset, operandOffset + 10));
779
+ if (shading.backgroundColor) {
780
+ style.backgroundColor = shading.backgroundColor;
781
+ }
782
+ }
783
+ break;
784
+ case 0x845E:
785
+ if (operandLength >= 2) {
786
+ style.marginLeftTwips = grpprl.readInt16LE(operandOffset);
787
+ }
788
+ break;
789
+ case 0x845D:
790
+ if (operandLength >= 2) {
791
+ style.marginRightTwips = grpprl.readInt16LE(operandOffset);
792
+ }
793
+ break;
794
+ case 0x8460:
795
+ if (operandLength >= 2) {
796
+ style.firstLineIndentTwips = grpprl.readInt16LE(operandOffset);
797
+ }
798
+ break;
799
+ default:
800
+ break;
801
+ }
802
+ offset += 2 + operandLength;
803
+ }
804
+ return style;
805
+ }
806
+ static isExplicitTableBorderSprm(sprm) {
807
+ return sprm === 0xD634 || sprm === 0xD612 || sprm === 0xD670;
808
+ }
809
+ static parseTDefTableOperand(buffer) {
810
+ if (buffer.length < 4) {
811
+ return undefined;
812
+ }
813
+ const dataOffset = buffer[1] === 0 && buffer.length >= 4 ? 2 : 1;
814
+ const itcMac = buffer[dataOffset];
815
+ if (itcMac < 1 || itcMac > 63) {
816
+ return undefined;
817
+ }
818
+ const expectedCentersLength = dataOffset + 1 + ((itcMac + 1) * 2);
819
+ if (buffer.length < expectedCentersLength) {
820
+ return undefined;
821
+ }
822
+ const boundaries = [];
823
+ for (let index = 0; index < itcMac + 1; index++) {
824
+ boundaries.push(buffer.readInt16LE(dataOffset + 1 + index * 2));
825
+ }
826
+ const columnWidthsTwips = [];
827
+ for (let index = 0; index < itcMac; index++) {
828
+ columnWidthsTwips.push(Math.max(0, boundaries[index + 1] - boundaries[index]));
829
+ }
830
+ const cellMerges = [];
831
+ const tc80Offset = expectedCentersLength;
832
+ const remaining = buffer.length - tc80Offset;
833
+ const tc80Size = itcMac > 0 && remaining >= itcMac * 2 && remaining % itcMac === 0
834
+ ? Math.floor(remaining / itcMac)
835
+ : 0;
836
+ if (tc80Size >= 2) {
837
+ for (let index = 0; index < itcMac; index++) {
838
+ const offset = tc80Offset + index * tc80Size;
839
+ const tcgrf = buffer.readUInt16LE(offset);
840
+ cellMerges.push({
841
+ horzMerge: tcgrf & 0x0003,
842
+ vertMerge: (tcgrf >> 5) & 0x0003
843
+ });
844
+ }
845
+ }
846
+ return {
847
+ columnCount: itcMac,
848
+ columnWidthsTwips,
849
+ cellMerges
850
+ };
851
+ }
852
+ static parseChpxFkp(page, pageStartFc, pageEndFc) {
853
+ const crun = page[511];
854
+ if (crun <= 0 || 4 * (crun + 1) > 511) {
855
+ return [];
856
+ }
857
+ const rgfc = [];
858
+ for (let i = 0; i < crun + 1; i++) {
859
+ rgfc.push(page.readUInt32LE(i * 4));
860
+ }
861
+ const rgbOffset = 4 * (crun + 1);
862
+ const runs = [];
863
+ for (let i = 0; i < crun; i++) {
864
+ const fcStart = rgfc[i];
865
+ const fcEnd = rgfc[i + 1];
866
+ if (fcStart >= fcEnd || fcEnd <= pageStartFc || fcStart >= pageEndFc) {
867
+ continue;
868
+ }
869
+ const chpxOffset = page[rgbOffset + i] * 2;
870
+ if (chpxOffset <= 0 || chpxOffset >= 511) {
871
+ continue;
872
+ }
873
+ const grpprlLength = page[chpxOffset];
874
+ const grpprlStart = chpxOffset + 1;
875
+ const grpprlEnd = Math.min(511, grpprlStart + grpprlLength);
876
+ if (grpprlStart >= grpprlEnd) {
877
+ continue;
878
+ }
879
+ const style = this.parseCharacterGrpprl(page.subarray(grpprlStart, grpprlEnd));
880
+ if (!this.hasCharacterStyle(style)) {
881
+ continue;
882
+ }
883
+ runs.push({
884
+ fcStart,
885
+ fcEnd,
886
+ style
887
+ });
888
+ }
889
+ return runs;
890
+ }
891
+ static parseCharacterGrpprl(grpprl) {
892
+ const style = {};
893
+ let offset = 0;
894
+ while (offset + 2 <= grpprl.length) {
895
+ const sprm = grpprl.readUInt16LE(offset);
896
+ const operandLength = this.getSprmOperandLength(sprm, grpprl, offset + 2);
897
+ if (operandLength < 0 || offset + 2 + operandLength > grpprl.length) {
898
+ break;
899
+ }
900
+ const operandOffset = offset + 2;
901
+ switch (sprm) {
902
+ case 0x0835:
903
+ style.bold = grpprl[operandOffset] !== 0;
904
+ break;
905
+ case 0x0836:
906
+ style.italic = grpprl[operandOffset] !== 0;
907
+ break;
908
+ case 0x2A3E:
909
+ style.underline = grpprl[operandOffset] !== 0;
910
+ break;
911
+ case 0x4A43:
912
+ if (operandLength >= 2) {
913
+ style.fontSizeHalfPoints = grpprl.readUInt16LE(operandOffset);
914
+ }
915
+ break;
916
+ case 0x6870:
917
+ if (operandLength >= 4) {
918
+ style.color = this.parseColorRef(grpprl.subarray(operandOffset, operandOffset + 4));
919
+ }
920
+ break;
921
+ case 0xCA71:
922
+ if (operandLength >= 10) {
923
+ const shading = this.parseShdOperand(grpprl.subarray(operandOffset, operandOffset + 10));
924
+ if (shading.foregroundColor) {
925
+ style.color = style.color ?? shading.foregroundColor;
926
+ }
927
+ if (shading.backgroundColor) {
928
+ style.backgroundColor = shading.backgroundColor;
929
+ }
930
+ }
931
+ break;
932
+ case 0x2A0C:
933
+ if (operandLength >= 1) {
934
+ style.highlightColor = this.parseHighlightIco(grpprl[operandOffset]);
935
+ }
936
+ break;
937
+ default:
938
+ break;
939
+ }
940
+ offset += 2 + operandLength;
941
+ }
942
+ return style;
943
+ }
944
+ static getSprmOperandLength(sprm, buffer, operandOffset) {
945
+ const spra = (sprm >> 13) & 0x7;
946
+ switch (spra) {
947
+ case 0:
948
+ case 1:
949
+ return 1;
950
+ case 2:
951
+ case 4:
952
+ case 5:
953
+ return 2;
954
+ case 3:
955
+ return 4;
956
+ case 6:
957
+ return operandOffset < buffer.length ? 1 + buffer[operandOffset] : -1;
958
+ case 7:
959
+ return 3;
960
+ default:
961
+ return -1;
962
+ }
963
+ }
964
+ static hasCharacterStyle(style) {
965
+ return Boolean(style && (style.bold
966
+ || style.italic
967
+ || style.underline
968
+ || style.fontSizeHalfPoints
969
+ || style.color
970
+ || style.backgroundColor
971
+ || style.highlightColor));
972
+ }
973
+ static parseColorRef(buffer) {
974
+ if (buffer.length < 4 || buffer[3] === 0xFF) {
975
+ return undefined;
976
+ }
977
+ const red = buffer[0];
978
+ const green = buffer[1];
979
+ const blue = buffer[2];
980
+ return `#${red.toString(16).padStart(2, '0')}${green.toString(16).padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`;
981
+ }
982
+ static parseShdOperand(buffer) {
983
+ if (buffer.length < 10) {
984
+ return {};
985
+ }
986
+ const foregroundColor = this.parseColorRef(buffer.subarray(0, 4));
987
+ const backgroundColor = this.sanitizeRenderableBackground(this.parseColorRef(buffer.subarray(4, 8)));
988
+ const pattern = buffer.readUInt16LE(8);
989
+ if (pattern === 0) {
990
+ return { foregroundColor };
991
+ }
992
+ return {
993
+ foregroundColor,
994
+ backgroundColor: backgroundColor ?? this.sanitizeRenderableBackground(foregroundColor)
995
+ };
996
+ }
997
+ static parseHighlightIco(value) {
998
+ const highlightMap = {
999
+ 1: '#000000',
1000
+ 2: '#0000ff',
1001
+ 3: '#00ffff',
1002
+ 4: '#00ff00',
1003
+ 5: '#ff00ff',
1004
+ 6: '#ff0000',
1005
+ 7: '#ffff00',
1006
+ 8: '#ffffff',
1007
+ 9: '#00008b',
1008
+ 10: '#008b8b',
1009
+ 11: '#006400',
1010
+ 12: '#8b008b',
1011
+ 13: '#8b0000',
1012
+ 14: '#808000',
1013
+ 15: '#808080',
1014
+ 16: '#d3d3d3'
1015
+ };
1016
+ return highlightMap[value];
1017
+ }
1018
+ static sanitizeRenderableBackground(color) {
1019
+ if (!color) {
1020
+ return undefined;
1021
+ }
1022
+ const rgb = this.parseHexColor(color);
1023
+ if (!rgb) {
1024
+ return undefined;
1025
+ }
1026
+ const luminance = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
1027
+ if (luminance < 48) {
1028
+ return undefined;
1029
+ }
1030
+ return color;
1031
+ }
1032
+ static parseHexColor(color) {
1033
+ const match = color.match(/^#([0-9a-f]{6})$/i);
1034
+ if (!match) {
1035
+ return undefined;
1036
+ }
1037
+ const hex = match[1];
1038
+ return {
1039
+ r: parseInt(hex.slice(0, 2), 16),
1040
+ g: parseInt(hex.slice(2, 4), 16),
1041
+ b: parseInt(hex.slice(4, 6), 16)
1042
+ };
1043
+ }
1044
+ static buildStyledParagraphs(candidate, styleRuns, paragraphStyleRuns, sectionBoundaries) {
1045
+ if ((!styleRuns.length && !paragraphStyleRuns.length) || !candidate.decodedSegments || candidate.decodedSegments.length === 0) {
1046
+ return undefined;
1047
+ }
1048
+ return this.enrichStructuredTableParagraphs(this.buildStyledParagraphsFromSegments(candidate.decodedSegments, styleRuns, paragraphStyleRuns, sectionBoundaries));
1049
+ }
1050
+ static enrichStructuredTableParagraphs(paragraphs) {
1051
+ if (paragraphs.length === 0) {
1052
+ return paragraphs;
1053
+ }
1054
+ const enriched = paragraphs.map((paragraph) => ({
1055
+ ...paragraph,
1056
+ style: paragraph.style ? { ...paragraph.style } : undefined,
1057
+ runs: paragraph.runs?.map((run) => ({
1058
+ ...run,
1059
+ style: run.style ? { ...run.style } : undefined
1060
+ }))
1061
+ }));
1062
+ let rowStart = -1;
1063
+ for (let index = 0; index < enriched.length; index++) {
1064
+ const paragraph = enriched[index];
1065
+ if (!paragraph.inTable) {
1066
+ rowStart = -1;
1067
+ continue;
1068
+ }
1069
+ if (rowStart < 0) {
1070
+ rowStart = index;
1071
+ }
1072
+ if (!paragraph.isTableTerminator) {
1073
+ continue;
1074
+ }
1075
+ for (let rowIndex = rowStart; rowIndex <= index; rowIndex++) {
1076
+ const target = enriched[rowIndex];
1077
+ target.tableColumnCount = target.tableColumnCount ?? paragraph.tableColumnCount;
1078
+ target.tableColumnWidthsTwips = target.tableColumnWidthsTwips ?? paragraph.tableColumnWidthsTwips;
1079
+ target.tableCellMerges = target.tableCellMerges ?? paragraph.tableCellMerges;
1080
+ target.tableHasExplicitBorders = target.tableHasExplicitBorders ?? paragraph.tableHasExplicitBorders;
1081
+ if (paragraph.style?.backgroundColor && !target.style?.backgroundColor) {
1082
+ target.style = this.mergeStyles(target.style, { backgroundColor: paragraph.style.backgroundColor });
1083
+ }
1084
+ }
1085
+ rowStart = -1;
1086
+ }
1087
+ return enriched;
1088
+ }
1089
+ static buildStyledParagraphsFromSegments(segments, styleRuns, paragraphStyleRuns, sectionBoundaries) {
1090
+ const paragraphs = [];
1091
+ let currentText = '';
1092
+ let currentRuns = [];
1093
+ let currentRunText = '';
1094
+ let currentRunStyle;
1095
+ let currentParagraphStyle = {};
1096
+ let visibleCount = 0;
1097
+ let boldCount = 0;
1098
+ let italicCount = 0;
1099
+ let underlineCount = 0;
1100
+ let pendingPageBreakBefore = false;
1101
+ let paragraphCpStart;
1102
+ const fontSizes = new Map();
1103
+ const textColors = new Map();
1104
+ const backgroundColors = new Map();
1105
+ const highlightColors = new Map();
1106
+ const resolveSectionIndex = (cp) => {
1107
+ const boundary = resolveSectionBoundary(cp);
1108
+ return boundary?.sectionIndex;
1109
+ };
1110
+ const resolveSectionBoundary = (cp) => {
1111
+ if (cp === undefined || sectionBoundaries.length === 0) {
1112
+ return undefined;
1113
+ }
1114
+ for (const boundary of sectionBoundaries) {
1115
+ if (cp >= boundary.cpStart && cp < boundary.cpEnd) {
1116
+ return boundary;
1117
+ }
1118
+ }
1119
+ return cp >= sectionBoundaries[sectionBoundaries.length - 1].cpEnd
1120
+ ? sectionBoundaries[sectionBoundaries.length - 1]
1121
+ : undefined;
1122
+ };
1123
+ const countAdjacentParagraphBreaks = (text, startIndex, direction) => {
1124
+ let count = 0;
1125
+ let cursor = startIndex + direction;
1126
+ while (cursor >= 0 && cursor < text.length) {
1127
+ const code = text.charCodeAt(cursor);
1128
+ if (code === 0x000d || code === 0x000b) {
1129
+ count += 1;
1130
+ cursor += direction;
1131
+ continue;
1132
+ }
1133
+ if (code === 0x0020 || code === 0x0009) {
1134
+ cursor += direction;
1135
+ continue;
1136
+ }
1137
+ break;
1138
+ }
1139
+ return count;
1140
+ };
1141
+ const flushRun = () => {
1142
+ if (currentRunText.length === 0) {
1143
+ return;
1144
+ }
1145
+ currentRuns.push({
1146
+ text: currentRunText,
1147
+ style: currentRunStyle
1148
+ });
1149
+ currentRunText = '';
1150
+ currentRunStyle = undefined;
1151
+ };
1152
+ const flushParagraph = () => {
1153
+ flushRun();
1154
+ const normalized = this.normalizeParagraphText(currentText, true);
1155
+ const preserveEmpty = normalized.length === 0
1156
+ && !currentParagraphStyle.inTable
1157
+ && !currentParagraphStyle.isTableTerminator
1158
+ && this.hasCharacterStyle(currentParagraphStyle);
1159
+ if (normalized.length > 0 || currentParagraphStyle.inTable || currentParagraphStyle.isTableTerminator || preserveEmpty) {
1160
+ const style = {};
1161
+ if (visibleCount > 0) {
1162
+ if (boldCount / visibleCount >= 0.55)
1163
+ style.bold = true;
1164
+ if (italicCount / visibleCount >= 0.55)
1165
+ style.italic = true;
1166
+ if (underlineCount / visibleCount >= 0.55)
1167
+ style.underline = true;
1168
+ let bestFontSize = 0;
1169
+ let bestFontCount = 0;
1170
+ for (const [fontSize, count] of Array.from(fontSizes.entries())) {
1171
+ if (count > bestFontCount) {
1172
+ bestFontSize = fontSize;
1173
+ bestFontCount = count;
1174
+ }
1175
+ }
1176
+ if (bestFontSize >= 2 && bestFontCount / visibleCount >= 0.45) {
1177
+ style.fontSizeHalfPoints = bestFontSize;
1178
+ }
1179
+ const dominantColor = this.pickDominantStyleValue(textColors, visibleCount, 0.45);
1180
+ if (dominantColor) {
1181
+ style.color = dominantColor;
1182
+ }
1183
+ const dominantBackground = this.pickDominantStyleValue(backgroundColors, visibleCount, 0.45);
1184
+ if (dominantBackground) {
1185
+ style.backgroundColor = dominantBackground;
1186
+ }
1187
+ const dominantHighlight = this.pickDominantStyleValue(highlightColors, visibleCount, 0.35);
1188
+ if (dominantHighlight) {
1189
+ style.highlightColor = dominantHighlight;
1190
+ }
1191
+ }
1192
+ const sectionBoundary = resolveSectionBoundary(paragraphCpStart);
1193
+ paragraphs.push({
1194
+ text: normalized,
1195
+ style: this.mergeStyles(currentParagraphStyle, this.hasCharacterStyle(style) ? style : undefined),
1196
+ runs: this.normalizeInlineRuns(currentRuns),
1197
+ listLevel: this.inferListLevel(currentParagraphStyle),
1198
+ inTable: Boolean(currentParagraphStyle.inTable),
1199
+ isTableTerminator: Boolean(currentParagraphStyle.isTableTerminator),
1200
+ tableColumnCount: currentParagraphStyle.tableColumnCount,
1201
+ tableColumnWidthsTwips: currentParagraphStyle.tableColumnWidthsTwips,
1202
+ tableCellMerges: currentParagraphStyle.tableCellMerges,
1203
+ tableHasExplicitBorders: currentParagraphStyle.tableHasExplicitBorders,
1204
+ preserveEmpty,
1205
+ pageBreakBefore: pendingPageBreakBefore || Boolean(currentParagraphStyle.pageBreakBefore),
1206
+ sectionIndex: sectionBoundary?.sectionIndex,
1207
+ sectionLayout: sectionBoundary?.layout
1208
+ });
1209
+ pendingPageBreakBefore = false;
1210
+ }
1211
+ currentText = '';
1212
+ currentRuns = [];
1213
+ currentRunText = '';
1214
+ currentRunStyle = undefined;
1215
+ currentParagraphStyle = {};
1216
+ visibleCount = 0;
1217
+ boldCount = 0;
1218
+ italicCount = 0;
1219
+ underlineCount = 0;
1220
+ paragraphCpStart = undefined;
1221
+ fontSizes.clear();
1222
+ textColors.clear();
1223
+ backgroundColors.clear();
1224
+ highlightColors.clear();
1225
+ };
1226
+ for (const segment of segments) {
1227
+ for (let index = 0; index < segment.text.length; index++) {
1228
+ const char = segment.text[index];
1229
+ const code = char.charCodeAt(0);
1230
+ const cp = segment.cpStart + index;
1231
+ const fc = segment.fcStart + index * segment.bytesPerChar;
1232
+ const paragraphStyle = this.findParagraphStyleForFc(paragraphStyleRuns, fc);
1233
+ if (code === 0x0013) {
1234
+ const field = this.readFieldInstruction(segment.text, index);
1235
+ if (field) {
1236
+ const embeddedObjectClass = this.parseEmbeddedChartObjectClass(field.fieldCode);
1237
+ if (embeddedObjectClass) {
1238
+ const anchorPageBreakBefore = pendingPageBreakBefore
1239
+ || Boolean(currentParagraphStyle.pageBreakBefore);
1240
+ if (paragraphStyle) {
1241
+ currentParagraphStyle = this.mergeStyles(currentParagraphStyle, paragraphStyle) ?? currentParagraphStyle;
1242
+ }
1243
+ flushParagraph();
1244
+ const sectionBoundary = resolveSectionBoundary(cp);
1245
+ paragraphs.push({
1246
+ text: '',
1247
+ embeddedChartAnchor: true,
1248
+ embeddedObjectClass,
1249
+ pageBreakBefore: anchorPageBreakBefore,
1250
+ sectionIndex: sectionBoundary?.sectionIndex,
1251
+ sectionLayout: sectionBoundary?.layout
1252
+ });
1253
+ pendingPageBreakBefore = false;
1254
+ }
1255
+ index = field.fieldEndIndex;
1256
+ continue;
1257
+ }
1258
+ }
1259
+ if (code === 0x0001) {
1260
+ const anchorPageBreakBefore = pendingPageBreakBefore || Boolean(currentParagraphStyle.pageBreakBefore);
1261
+ if (paragraphStyle) {
1262
+ currentParagraphStyle = this.mergeStyles(currentParagraphStyle, paragraphStyle) ?? currentParagraphStyle;
1263
+ }
1264
+ flushParagraph();
1265
+ const sectionBoundary = resolveSectionBoundary(cp);
1266
+ paragraphs.push({
1267
+ text: '',
1268
+ embeddedImageAnchor: true,
1269
+ floatingSide: this.inferFloatingSideFromStyle(currentParagraphStyle),
1270
+ floatingWidthMode: this.inferFloatingWidthModeFromStyle(currentParagraphStyle),
1271
+ floatingPlacement: this.inferFloatingPlacementFromStyle(currentParagraphStyle),
1272
+ floatingClearancePx: this.inferFloatingClearancePx(currentParagraphStyle),
1273
+ pageBreakBefore: anchorPageBreakBefore,
1274
+ sectionIndex: sectionBoundary?.sectionIndex,
1275
+ sectionLayout: sectionBoundary?.layout
1276
+ });
1277
+ pendingPageBreakBefore = false;
1278
+ continue;
1279
+ }
1280
+ if (code === 0x0008) {
1281
+ const surroundingBreakCount = countAdjacentParagraphBreaks(segment.text, index, -1)
1282
+ + countAdjacentParagraphBreaks(segment.text, index, 1);
1283
+ const anchorPageBreakBefore = pendingPageBreakBefore
1284
+ || Boolean(currentParagraphStyle.pageBreakBefore);
1285
+ const embeddedAssetPreference = surroundingBreakCount >= 8 ? 'chart' : 'image';
1286
+ if (paragraphStyle) {
1287
+ currentParagraphStyle = this.mergeStyles(currentParagraphStyle, paragraphStyle) ?? currentParagraphStyle;
1288
+ }
1289
+ flushParagraph();
1290
+ const sectionBoundary = resolveSectionBoundary(cp);
1291
+ paragraphs.push({
1292
+ text: '',
1293
+ embeddedAssetAnchor: true,
1294
+ embeddedAssetPreference,
1295
+ pageBreakBefore: anchorPageBreakBefore,
1296
+ sectionIndex: sectionBoundary?.sectionIndex,
1297
+ sectionLayout: sectionBoundary?.layout
1298
+ });
1299
+ pendingPageBreakBefore = false;
1300
+ continue;
1301
+ }
1302
+ if (code === 0x000c) {
1303
+ flushParagraph();
1304
+ pendingPageBreakBefore = true;
1305
+ continue;
1306
+ }
1307
+ if (char === '\r' || code === 0x0007 || code === 0x000b) {
1308
+ if (paragraphStyle) {
1309
+ currentParagraphStyle = this.mergeStyles(currentParagraphStyle, paragraphStyle) ?? currentParagraphStyle;
1310
+ }
1311
+ flushParagraph();
1312
+ continue;
1313
+ }
1314
+ if (code < 32 && char !== '\t') {
1315
+ continue;
1316
+ }
1317
+ const style = this.findCharacterStyleForFc(styleRuns, fc);
1318
+ if (paragraphStyle) {
1319
+ currentParagraphStyle = this.mergeStyles(currentParagraphStyle, paragraphStyle) ?? currentParagraphStyle;
1320
+ }
1321
+ if (paragraphCpStart === undefined) {
1322
+ paragraphCpStart = cp;
1323
+ }
1324
+ currentText += char;
1325
+ if (!this.areCharacterStylesEqual(currentRunStyle, style)) {
1326
+ flushRun();
1327
+ currentRunStyle = style ? { ...style } : undefined;
1328
+ }
1329
+ currentRunText += char;
1330
+ if (!/\s/.test(char)) {
1331
+ visibleCount += 1;
1332
+ if (style?.bold)
1333
+ boldCount += 1;
1334
+ if (style?.italic)
1335
+ italicCount += 1;
1336
+ if (style?.underline)
1337
+ underlineCount += 1;
1338
+ if (style?.fontSizeHalfPoints) {
1339
+ fontSizes.set(style.fontSizeHalfPoints, (fontSizes.get(style.fontSizeHalfPoints) ?? 0) + 1);
1340
+ }
1341
+ if (style?.color) {
1342
+ textColors.set(style.color, (textColors.get(style.color) ?? 0) + 1);
1343
+ }
1344
+ if (style?.backgroundColor) {
1345
+ backgroundColors.set(style.backgroundColor, (backgroundColors.get(style.backgroundColor) ?? 0) + 1);
1346
+ }
1347
+ if (style?.highlightColor) {
1348
+ highlightColors.set(style.highlightColor, (highlightColors.get(style.highlightColor) ?? 0) + 1);
1349
+ }
1350
+ }
1351
+ }
1352
+ }
1353
+ flushParagraph();
1354
+ return paragraphs;
1355
+ }
1356
+ static readFieldInstruction(text, startIndex) {
1357
+ if (text.charCodeAt(startIndex) !== 0x0013) {
1358
+ return undefined;
1359
+ }
1360
+ let separatorIndex = -1;
1361
+ let endIndex = -1;
1362
+ for (let index = startIndex + 1; index < text.length; index++) {
1363
+ const code = text.charCodeAt(index);
1364
+ if (code === 0x0014 && separatorIndex < 0) {
1365
+ separatorIndex = index;
1366
+ }
1367
+ if (code === 0x0015) {
1368
+ endIndex = index;
1369
+ break;
1370
+ }
1371
+ }
1372
+ if (endIndex < 0) {
1373
+ return undefined;
1374
+ }
1375
+ const rawCode = text.slice(startIndex + 1, separatorIndex >= 0 ? separatorIndex : endIndex);
1376
+ return {
1377
+ fieldCode: rawCode.replace(/[\u0000-\u001f]+/g, ' ').trim(),
1378
+ fieldEndIndex: endIndex
1379
+ };
1380
+ }
1381
+ static parseEmbeddedChartObjectClass(fieldCode) {
1382
+ const match = fieldCode.match(/\bEMBED\s+([^\s]+)/i);
1383
+ if (!match) {
1384
+ return undefined;
1385
+ }
1386
+ const objectClass = match[1].trim();
1387
+ return /chart/i.test(objectClass) ? objectClass : undefined;
1388
+ }
1389
+ static normalizeInlineRuns(runs) {
1390
+ const normalizedRuns = [];
1391
+ for (const run of runs) {
1392
+ const text = run.text.replace(/\u0000/g, '');
1393
+ if (text.length === 0) {
1394
+ continue;
1395
+ }
1396
+ const previous = normalizedRuns[normalizedRuns.length - 1];
1397
+ if (previous && this.areCharacterStylesEqual(previous.style, run.style)) {
1398
+ previous.text += text;
1399
+ continue;
1400
+ }
1401
+ normalizedRuns.push({
1402
+ text,
1403
+ style: run.style
1404
+ });
1405
+ }
1406
+ return normalizedRuns.length > 0 ? normalizedRuns : undefined;
1407
+ }
1408
+ static findCharacterStyleForFc(styleRuns, fc) {
1409
+ for (let index = styleRuns.length - 1; index >= 0; index--) {
1410
+ const run = styleRuns[index];
1411
+ if (fc >= run.fcStart && fc < run.fcEnd) {
1412
+ return run.style;
1413
+ }
1414
+ if (fc >= run.fcEnd) {
1415
+ break;
1416
+ }
1417
+ }
1418
+ return undefined;
1419
+ }
1420
+ static findParagraphStyleForFc(styleRuns, fc) {
1421
+ for (let index = styleRuns.length - 1; index >= 0; index--) {
1422
+ const run = styleRuns[index];
1423
+ if (fc >= run.fcStart && fc < run.fcEnd) {
1424
+ return run.style;
1425
+ }
1426
+ if (fc >= run.fcEnd) {
1427
+ break;
1428
+ }
1429
+ }
1430
+ return undefined;
1431
+ }
1432
+ static areCharacterStylesEqual(left, right) {
1433
+ return Boolean(left?.bold) === Boolean(right?.bold)
1434
+ && Boolean(left?.italic) === Boolean(right?.italic)
1435
+ && Boolean(left?.underline) === Boolean(right?.underline)
1436
+ && (left?.fontSizeHalfPoints ?? 0) === (right?.fontSizeHalfPoints ?? 0)
1437
+ && (left?.color ?? '') === (right?.color ?? '')
1438
+ && (left?.backgroundColor ?? '') === (right?.backgroundColor ?? '')
1439
+ && (left?.highlightColor ?? '') === (right?.highlightColor ?? '')
1440
+ && (left?.textAlign ?? 'left') === (right?.textAlign ?? 'left')
1441
+ && (left?.marginLeftTwips ?? 0) === (right?.marginLeftTwips ?? 0)
1442
+ && (left?.marginRightTwips ?? 0) === (right?.marginRightTwips ?? 0)
1443
+ && (left?.firstLineIndentTwips ?? 0) === (right?.firstLineIndentTwips ?? 0)
1444
+ && Boolean(left?.pageBreakBefore) === Boolean(right?.pageBreakBefore)
1445
+ && Boolean(left?.keepWithNext) === Boolean(right?.keepWithNext)
1446
+ && Boolean(left?.keepLinesTogether) === Boolean(right?.keepLinesTogether)
1447
+ && Boolean(left?.inTable) === Boolean(right?.inTable)
1448
+ && Boolean(left?.isTableTerminator) === Boolean(right?.isTableTerminator)
1449
+ && (left?.tableColumnCount ?? 0) === (right?.tableColumnCount ?? 0);
1450
+ }
1451
+ static hasParagraphLayoutStyle(style) {
1452
+ return Boolean(style && (style.textAlign
1453
+ || style.marginLeftTwips
1454
+ || style.marginRightTwips
1455
+ || style.firstLineIndentTwips
1456
+ || style.pageBreakBefore
1457
+ || style.keepWithNext
1458
+ || style.keepLinesTogether
1459
+ || style.inTable
1460
+ || style.isTableTerminator
1461
+ || style.tableColumnCount));
1462
+ }
1463
+ static parseParagraphAlignment(value) {
1464
+ switch (value) {
1465
+ case 1:
1466
+ return 'center';
1467
+ case 2:
1468
+ return 'right';
1469
+ case 3:
1470
+ case 4:
1471
+ return 'justify';
1472
+ case 0:
1473
+ default:
1474
+ return 'left';
1475
+ }
1476
+ }
1477
+ static inferListLevel(style) {
1478
+ if (!style) {
1479
+ return 0;
1480
+ }
1481
+ const indent = Math.max(0, style.marginLeftTwips ?? 0);
1482
+ const hanging = Math.max(0, -(style.firstLineIndentTwips ?? 0));
1483
+ const effectiveIndent = Math.max(indent, hanging);
1484
+ return Math.max(0, Math.min(6, Math.round(effectiveIndent / 360) - 1));
1485
+ }
1486
+ static mergeStyles(base, override) {
1487
+ if (!base && !override) {
1488
+ return undefined;
1489
+ }
1490
+ return {
1491
+ ...base,
1492
+ ...override
1493
+ };
1494
+ }
1495
+ static pickDominantStyleValue(values, visibleCount, threshold) {
1496
+ let bestValue;
1497
+ let bestCount = 0;
1498
+ for (const [value, count] of Array.from(values.entries())) {
1499
+ if (count > bestCount) {
1500
+ bestValue = value;
1501
+ bestCount = count;
1502
+ }
1503
+ }
1504
+ return bestValue && visibleCount > 0 && bestCount / visibleCount >= threshold
1505
+ ? bestValue
1506
+ : undefined;
1507
+ }
1508
+ static selectBestDecodedCandidate(decodedByAnsiDecoder, segmentsByAnsiDecoder, startsAtZero, pieceCount) {
1509
+ let best = null;
1510
+ for (const { name } of ANSI_DECODERS) {
1511
+ const normalized = this.normalizeDocumentText((decodedByAnsiDecoder.get(name) ?? []).join(''));
1512
+ if (normalized.length < 20) {
1513
+ continue;
1514
+ }
1515
+ const candidate = {
1516
+ text: normalized,
1517
+ score: this.scoreExtractedText(normalized, startsAtZero, pieceCount),
1518
+ decodedSegments: segmentsByAnsiDecoder.get(name) ?? []
1519
+ };
1520
+ if (!best || candidate.score > best.score) {
1521
+ best = candidate;
1522
+ }
1523
+ }
1524
+ return best;
1525
+ }
1526
+ static extractReadableSegments(raw) {
1527
+ return raw
1528
+ .replace(/\u0000/g, '')
1529
+ .split(/[\r\n\t\x00-\x1f]+/)
1530
+ .map((text) => this.normalizeParagraphText(text))
1531
+ .filter((text) => text.length >= 3 && /[A-Za-z0-9가-힣]/.test(text));
1532
+ }
1533
+ static dedupeSegments(candidates) {
1534
+ const deduped = [];
1535
+ const seen = new Set();
1536
+ for (const raw of candidates) {
1537
+ const normalized = this.normalizeParagraphText(raw);
1538
+ if (!normalized || this.isNoise(normalized) || seen.has(normalized)) {
1539
+ continue;
1540
+ }
1541
+ seen.add(normalized);
1542
+ deduped.push(normalized);
1543
+ }
1544
+ return deduped;
1545
+ }
1546
+ static renderHtml(rawText, images, styledParagraphs) {
1547
+ const blocks = this.buildDocumentBlocks(rawText, images, styledParagraphs);
1548
+ if (blocks.length === 0) {
1549
+ return '';
1550
+ }
1551
+ return this.wrapLegacyHtml(this.buildLegacySections(blocks));
1552
+ }
1553
+ static buildDocumentBlocks(rawText, images, styledParagraphs) {
1554
+ const lines = this.buildRenderableLines(rawText, styledParagraphs);
1555
+ if (lines.length === 0 && images.length === 0) {
1556
+ return [];
1557
+ }
1558
+ return this.buildBlocks(lines);
1559
+ }
1560
+ static buildRenderableLines(rawText, styledParagraphs) {
1561
+ const rawLines = styledParagraphs && styledParagraphs.length > 0
1562
+ ? styledParagraphs
1563
+ .map((entry) => {
1564
+ const normalizedText = this.normalizeParagraphText(entry.text, true);
1565
+ const fallbackEmbeddedObjectClass = !entry.embeddedChartAnchor
1566
+ ? this.parseEmbeddedChartObjectClass(normalizedText)
1567
+ : undefined;
1568
+ return {
1569
+ text: normalizedText,
1570
+ style: entry.style,
1571
+ runs: entry.runs,
1572
+ listLevel: entry.listLevel,
1573
+ inTable: entry.inTable,
1574
+ isTableTerminator: entry.isTableTerminator,
1575
+ tableColumnCount: entry.tableColumnCount,
1576
+ tableColumnWidthsTwips: entry.tableColumnWidthsTwips,
1577
+ tableCellMerges: entry.tableCellMerges,
1578
+ tableHasExplicitBorders: entry.tableHasExplicitBorders,
1579
+ embeddedChartAnchor: entry.embeddedChartAnchor || Boolean(fallbackEmbeddedObjectClass),
1580
+ embeddedImageAnchor: entry.embeddedImageAnchor,
1581
+ embeddedAssetAnchor: entry.embeddedAssetAnchor,
1582
+ embeddedAssetPreference: entry.embeddedAssetPreference,
1583
+ embeddedObjectClass: entry.embeddedObjectClass || fallbackEmbeddedObjectClass,
1584
+ floatingSide: entry.floatingSide,
1585
+ floatingWidthMode: entry.floatingWidthMode,
1586
+ floatingPlacement: entry.floatingPlacement,
1587
+ floatingClearancePx: entry.floatingClearancePx,
1588
+ preserveEmpty: entry.preserveEmpty,
1589
+ pageBreakBefore: entry.pageBreakBefore,
1590
+ sectionIndex: entry.sectionIndex,
1591
+ sectionLayout: entry.sectionLayout
1592
+ };
1593
+ })
1594
+ .filter((entry) => entry.text.length > 0 || entry.preserveEmpty || entry.inTable || entry.isTableTerminator || entry.embeddedChartAnchor || entry.embeddedImageAnchor || entry.embeddedAssetAnchor)
1595
+ : rawText
1596
+ .split(/\n+/)
1597
+ .map((text) => {
1598
+ const normalizedText = this.normalizeParagraphText(text, true);
1599
+ const fallbackEmbeddedObjectClass = this.parseEmbeddedChartObjectClass(normalizedText);
1600
+ return {
1601
+ text: normalizedText,
1602
+ embeddedChartAnchor: Boolean(fallbackEmbeddedObjectClass),
1603
+ embeddedObjectClass: fallbackEmbeddedObjectClass
1604
+ };
1605
+ })
1606
+ .filter((entry) => entry.text.length > 0);
1607
+ return rawLines.filter((line, index) => (line.inTable
1608
+ || line.preserveEmpty
1609
+ || line.isTableTerminator
1610
+ || line.embeddedChartAnchor
1611
+ || line.embeddedImageAnchor
1612
+ || line.embeddedAssetAnchor
1613
+ || !this.shouldDropLine(line.text, rawLines[index - 1]?.text || '', rawLines[index + 1]?.text || '')));
1614
+ }
1615
+ static composeDocumentBlocks(baseBlocks, packageCharts, workbookTables, images = []) {
1616
+ const combinedBlocks = [...baseBlocks];
1617
+ const packageBlocks = this.buildEmbeddedSheetBlocks(packageCharts);
1618
+ const inferSectionMetadataAround = (blocks, index) => {
1619
+ for (let current = index - 1; current >= 0; current--) {
1620
+ const candidate = blocks[current];
1621
+ if (candidate.sectionIndex !== undefined || candidate.sectionLayout !== undefined) {
1622
+ return {
1623
+ sectionIndex: candidate.sectionIndex,
1624
+ sectionLayout: candidate.sectionLayout
1625
+ };
1626
+ }
1627
+ }
1628
+ for (let current = index; current < blocks.length; current++) {
1629
+ const candidate = blocks[current];
1630
+ if (candidate.sectionIndex !== undefined || candidate.sectionLayout !== undefined) {
1631
+ return {
1632
+ sectionIndex: candidate.sectionIndex,
1633
+ sectionLayout: candidate.sectionLayout
1634
+ };
1635
+ }
1636
+ }
1637
+ return {};
1638
+ };
1639
+ const inheritBlockMetadata = (block, metadata) => ({
1640
+ ...block,
1641
+ pageBreakBefore: block.pageBreakBefore ?? metadata?.pageBreakBefore,
1642
+ sectionIndex: block.sectionIndex ?? metadata?.sectionIndex,
1643
+ sectionLayout: block.sectionLayout ?? metadata?.sectionLayout
1644
+ });
1645
+ const imageQueue = [...images];
1646
+ const chartQueue = [...packageBlocks];
1647
+ const normalizeAssetCueText = (value) => this.normalizeParagraphText(this.stripFieldCodeNoise(value || ''))
1648
+ .replace(/\s+/g, ' ')
1649
+ .trim();
1650
+ const getCaptionCueKind = (block) => {
1651
+ if (block.kind !== 'paragraph' && block.kind !== 'heading') {
1652
+ return undefined;
1653
+ }
1654
+ const normalized = normalizeAssetCueText(block.text);
1655
+ if (!normalized || normalized.length > 120) {
1656
+ return undefined;
1657
+ }
1658
+ if (/^(chart|table|diagram|graph|도표|차트|표)\s*([0-9]+|[ivxlcdm]+)?(?:[\s:.)-]|$)/i.test(normalized)) {
1659
+ return 'chart';
1660
+ }
1661
+ if (/^(figure|fig\.?|image|photo|picture|그림|사진)\s*([0-9]+|[ivxlcdm]+)?(?:[\s:.)-]|$)/i.test(normalized)) {
1662
+ return 'image';
1663
+ }
1664
+ return undefined;
1665
+ };
1666
+ const findCaptionInsertionIndex = (blocks, kind) => {
1667
+ for (let index = 0; index < blocks.length; index++) {
1668
+ const cueKind = getCaptionCueKind(blocks[index]);
1669
+ if (cueKind === kind) {
1670
+ return index + 1;
1671
+ }
1672
+ }
1673
+ return undefined;
1674
+ };
1675
+ for (let index = 0; index < combinedBlocks.length; index++) {
1676
+ if (combinedBlocks[index].kind !== 'embedded-asset-anchor') {
1677
+ continue;
1678
+ }
1679
+ const anchor = combinedBlocks[index];
1680
+ if (anchor.kind !== 'embedded-asset-anchor') {
1681
+ continue;
1682
+ }
1683
+ const metadata = {
1684
+ pageBreakBefore: anchor.pageBreakBefore,
1685
+ sectionIndex: anchor.sectionIndex,
1686
+ sectionLayout: anchor.sectionLayout
1687
+ };
1688
+ const preferredKind = anchor.assetPreference;
1689
+ if (preferredKind === 'chart' && chartQueue.length > 0) {
1690
+ combinedBlocks.splice(index, 1, inheritBlockMetadata(chartQueue.shift(), metadata));
1691
+ continue;
1692
+ }
1693
+ if (preferredKind === 'image' && imageQueue.length > 0) {
1694
+ const image = imageQueue.shift();
1695
+ combinedBlocks.splice(index, 1, inheritBlockMetadata({
1696
+ kind: 'image',
1697
+ ...image
1698
+ }, metadata));
1699
+ continue;
1700
+ }
1701
+ if (imageQueue.length > 0) {
1702
+ const image = imageQueue.shift();
1703
+ combinedBlocks.splice(index, 1, inheritBlockMetadata({
1704
+ kind: 'image',
1705
+ ...image
1706
+ }, metadata));
1707
+ continue;
1708
+ }
1709
+ if (chartQueue.length > 0) {
1710
+ combinedBlocks.splice(index, 1, inheritBlockMetadata(chartQueue.shift(), metadata));
1711
+ continue;
1712
+ }
1713
+ }
1714
+ if (packageBlocks.length > 0) {
1715
+ const remaining = [...chartQueue];
1716
+ for (let index = 0; index < combinedBlocks.length && remaining.length > 0; index++) {
1717
+ if (combinedBlocks[index].kind !== 'embedded-chart-anchor') {
1718
+ continue;
1719
+ }
1720
+ const anchor = combinedBlocks[index];
1721
+ combinedBlocks.splice(index, 1, inheritBlockMetadata(remaining.shift(), {
1722
+ pageBreakBefore: anchor.pageBreakBefore,
1723
+ sectionIndex: anchor.sectionIndex,
1724
+ sectionLayout: anchor.sectionLayout
1725
+ }));
1726
+ }
1727
+ const filtered = combinedBlocks.filter((block) => block.kind !== 'embedded-chart-anchor');
1728
+ combinedBlocks.splice(0, combinedBlocks.length, ...filtered);
1729
+ if (remaining.length > 0) {
1730
+ for (const block of remaining) {
1731
+ const insertionIndex = findCaptionInsertionIndex(combinedBlocks, 'chart')
1732
+ ?? this.findPreferredAssetInsertionIndex(combinedBlocks);
1733
+ const sectionMetadata = inferSectionMetadataAround(combinedBlocks, insertionIndex);
1734
+ combinedBlocks.splice(insertionIndex, 0, inheritBlockMetadata(block, sectionMetadata));
1735
+ }
1736
+ }
1737
+ }
1738
+ else {
1739
+ const filtered = combinedBlocks.filter((block) => block.kind !== 'embedded-chart-anchor');
1740
+ combinedBlocks.splice(0, combinedBlocks.length, ...filtered);
1741
+ }
1742
+ const workbookBlocks = this.buildEmbeddedSheetBlocks(workbookTables);
1743
+ if (workbookBlocks.length > 0) {
1744
+ const insertionIndex = this.findPreferredAssetInsertionIndex(combinedBlocks);
1745
+ const sectionMetadata = inferSectionMetadataAround(combinedBlocks, insertionIndex);
1746
+ combinedBlocks.splice(insertionIndex, 0, ...workbookBlocks.map((block) => inheritBlockMetadata(block, sectionMetadata)));
1747
+ }
1748
+ const imageAnchorIndexes = combinedBlocks
1749
+ .map((block, index) => (block.kind === 'image-gallery' && block.images.length === 0 ? index : -1))
1750
+ .filter((index) => index >= 0);
1751
+ const remainingImages = [...imageQueue];
1752
+ for (const index of imageAnchorIndexes) {
1753
+ const gallery = combinedBlocks[index];
1754
+ if (gallery.kind !== 'image-gallery') {
1755
+ continue;
1756
+ }
1757
+ if (remainingImages.length === 0) {
1758
+ continue;
1759
+ }
1760
+ const image = remainingImages.shift();
1761
+ combinedBlocks.splice(index, 1, {
1762
+ kind: 'image',
1763
+ ...image,
1764
+ floating: gallery.floating,
1765
+ floatingSide: gallery.floatingSide,
1766
+ floatingWidthMode: gallery.floatingWidthMode,
1767
+ floatingPlacement: gallery.floatingPlacement,
1768
+ floatingClearancePx: gallery.floatingClearancePx,
1769
+ pageBreakBefore: gallery.pageBreakBefore,
1770
+ sectionIndex: gallery.sectionIndex,
1771
+ sectionLayout: gallery.sectionLayout
1772
+ });
1773
+ }
1774
+ if (remainingImages.length > 0) {
1775
+ for (const image of remainingImages) {
1776
+ const insertionIndex = findCaptionInsertionIndex(combinedBlocks, 'image')
1777
+ ?? findCaptionInsertionIndex(combinedBlocks, 'chart')
1778
+ ?? this.findPreferredAssetInsertionIndex(combinedBlocks);
1779
+ const sectionMetadata = inferSectionMetadataAround(combinedBlocks, insertionIndex);
1780
+ combinedBlocks.splice(insertionIndex, 0, inheritBlockMetadata({
1781
+ kind: 'image',
1782
+ ...image
1783
+ }, sectionMetadata));
1784
+ }
1785
+ }
1786
+ const filteredBlocks = combinedBlocks.filter((block) => (!(block.kind === 'image-gallery' && block.images.length === 0)
1787
+ && block.kind !== 'embedded-asset-anchor'));
1788
+ combinedBlocks.splice(0, combinedBlocks.length, ...filteredBlocks);
1789
+ return combinedBlocks;
1790
+ }
1791
+ static findPreferredAssetInsertionIndex(blocks) {
1792
+ for (let index = blocks.length - 1; index >= 0; index--) {
1793
+ if (blocks[index].kind === 'table') {
1794
+ return index + 1;
1795
+ }
1796
+ }
1797
+ return blocks.length;
1798
+ }
1799
+ static buildEmbeddedSheetBlocks(sheets) {
1800
+ return sheets.map((sheet) => ({
1801
+ kind: 'embedded-sheet',
1802
+ title: sheet.title,
1803
+ chart: sheet.chart,
1804
+ rows: sheet.showTable ? sheet.rows : undefined,
1805
+ objectPlacementMode: sheet.objectPlacementMode
1806
+ }));
1807
+ }
1808
+ static wrapLegacyHtml(sections) {
1809
+ return this.renderLegacyDocumentModel(this.buildRenderedDocumentModel(this.buildSemanticDocumentModel(sections)));
1810
+ }
1811
+ static buildSemanticDocumentModel(sections) {
1812
+ return {
1813
+ sections: sections.map((section) => this.buildSemanticSectionModel(section))
1814
+ };
1815
+ }
1816
+ static buildSemanticSectionModel(section) {
1817
+ const semanticBlocks = [];
1818
+ for (const block of section.blocks) {
1819
+ const model = this.buildSemanticBlockModel(block, semanticBlocks.length === 0);
1820
+ if (model) {
1821
+ semanticBlocks.push(model);
1822
+ }
1823
+ }
1824
+ return {
1825
+ sectionIndex: section.sectionIndex,
1826
+ layout: section.layout,
1827
+ headerFooter: this.buildSemanticHeaderFooterModel(section.headerFooter),
1828
+ blocks: semanticBlocks
1829
+ };
1830
+ }
1831
+ static buildRenderedDocumentModel(documentModel) {
1832
+ return {
1833
+ sections: documentModel.sections.map((section) => this.buildRenderedSectionModel(section))
1834
+ };
1835
+ }
1836
+ static buildRenderedSectionModel(section) {
1837
+ const renderedBlocks = [];
1838
+ for (const block of section.blocks) {
1839
+ const model = this.buildRenderedBlockModel(block);
1840
+ if (model) {
1841
+ renderedBlocks.push(model);
1842
+ }
1843
+ }
1844
+ return {
1845
+ sectionIndex: section.sectionIndex,
1846
+ layout: section.layout,
1847
+ headerFooter: this.buildRenderedHeaderFooterModel(section.headerFooter),
1848
+ renderedBlocks
1849
+ };
1850
+ }
1851
+ static buildSemanticHeaderFooterModel(headerFooter) {
1852
+ if (!headerFooter) {
1853
+ return undefined;
1854
+ }
1855
+ const buildTokens = (value) => {
1856
+ const tokens = this.tokenizeHeaderFooterText(value);
1857
+ return tokens.length > 0 ? tokens : undefined;
1858
+ };
1859
+ return {
1860
+ sectionNumber: headerFooter.sectionNumber,
1861
+ sectionCount: headerFooter.sectionCount,
1862
+ evenHeaderTokens: buildTokens(headerFooter.evenHeaderText),
1863
+ oddHeaderTokens: buildTokens(headerFooter.oddHeaderText),
1864
+ evenFooterTokens: buildTokens(headerFooter.evenFooterText),
1865
+ oddFooterTokens: buildTokens(headerFooter.oddFooterText),
1866
+ firstHeaderTokens: buildTokens(headerFooter.firstHeaderText),
1867
+ firstFooterTokens: buildTokens(headerFooter.firstFooterText)
1868
+ };
1869
+ }
1870
+ static buildRenderedHeaderFooterModel(headerFooter) {
1871
+ if (!headerFooter) {
1872
+ return undefined;
1873
+ }
1874
+ const stringify = (tokens) => {
1875
+ if (!tokens || tokens.length === 0) {
1876
+ return undefined;
1877
+ }
1878
+ return tokens.map((token) => token.kind === 'text' ? token.value : token.field).join('');
1879
+ };
1880
+ return {
1881
+ sectionNumber: headerFooter.sectionNumber,
1882
+ sectionCount: headerFooter.sectionCount,
1883
+ evenHeaderText: stringify(headerFooter.evenHeaderTokens),
1884
+ oddHeaderText: stringify(headerFooter.oddHeaderTokens),
1885
+ evenFooterText: stringify(headerFooter.evenFooterTokens),
1886
+ oddFooterText: stringify(headerFooter.oddFooterTokens),
1887
+ firstHeaderText: stringify(headerFooter.firstHeaderTokens),
1888
+ firstFooterText: stringify(headerFooter.firstFooterTokens)
1889
+ };
1890
+ }
1891
+ static renderLegacyDocumentModel(documentModel) {
1892
+ const content = documentModel.sections
1893
+ .map((section) => {
1894
+ const style = [
1895
+ `--ov-page-width-mm:${this.twipsToMm(section.layout.pageWidthTwips).toFixed(2)}mm`,
1896
+ `--ov-page-height-mm:${this.twipsToMm(section.layout.pageHeightTwips).toFixed(2)}mm`,
1897
+ `--ov-page-padding-top-mm:${this.twipsToMm(section.layout.marginTopTwips).toFixed(2)}mm`,
1898
+ `--ov-page-padding-right-mm:${this.twipsToMm(section.layout.marginRightTwips).toFixed(2)}mm`,
1899
+ `--ov-page-padding-bottom-mm:${this.twipsToMm(section.layout.marginBottomTwips).toFixed(2)}mm`,
1900
+ `--ov-page-padding-left-mm:${this.twipsToMm(section.layout.marginLeftTwips).toFixed(2)}mm`,
1901
+ `--ov-page-gutter-mm:${this.twipsToMm(section.layout.gutterTwips).toFixed(2)}mm`,
1902
+ `--ov-page-header-mm:${this.twipsToMm(section.layout.headerTopTwips).toFixed(2)}mm`,
1903
+ `--ov-page-footer-mm:${this.twipsToMm(section.layout.footerBottomTwips).toFixed(2)}mm`,
1904
+ `--ov-columns:${Math.max(1, section.layout.columns)}`,
1905
+ `--ov-column-gap-mm:${this.twipsToMm(section.layout.columnGapTwips).toFixed(2)}mm`,
1906
+ `--ov-column-rule-width:${section.layout.lineBetweenColumns ? '1px' : '0px'}`,
1907
+ `--ov-gutter-side:${section.layout.rtlGutter ? 'right' : 'left'}`
1908
+ ].join(';');
1909
+ const metaJson = this.escapeHtml(JSON.stringify(section.headerFooter ?? {}));
1910
+ const columnWidths = section.layout.explicitColumnWidthsTwips.map((value) => this.twipsToMm(value).toFixed(2)).join(',');
1911
+ const columnSpacings = section.layout.explicitColumnSpacingsTwips.map((value) => this.twipsToMm(value).toFixed(2)).join(',');
1912
+ const blocksHtml = section.renderedBlocks
1913
+ .map((block) => this.wrapLegacyBlock(block))
1914
+ .join('\n');
1915
+ return `<section class="ov-doc-legacy-section" data-ov-gutter-side="${section.layout.rtlGutter ? 'right' : 'left'}" data-ov-columns="${Math.max(1, section.layout.columns)}" data-ov-custom-columns="${section.layout.explicitColumnWidthsTwips.length > 0 ? 'true' : 'false'}" data-ov-column-widths="${columnWidths}" data-ov-column-spacings="${columnSpacings}" style="${style}"><script type="application/json" class="ov-doc-legacy-section-meta">${metaJson}</script>${blocksHtml}</section>`;
1916
+ })
1917
+ .join('');
1918
+ return `<div class="ov-doc-legacy">${content}</div>`;
1919
+ }
1920
+ static inferLegacyLayoutMetrics(blocks) {
1921
+ const explicitLayout = blocks.find((block) => block.sectionLayout)?.sectionLayout;
1922
+ if (explicitLayout) {
1923
+ return explicitLayout;
1924
+ }
1925
+ const a4Portrait = { width: 11906, height: 16838 };
1926
+ const a4Landscape = { width: 16838, height: 11906 };
1927
+ const standardMarginTwips = 1440;
1928
+ const narrowMarginTwips = 1080;
1929
+ const widestTableTwips = blocks
1930
+ .filter((block) => block.kind === 'table')
1931
+ .map((block) => (block.columnWidthsTwips ?? []).reduce((sum, width) => sum + Math.max(0, width), 0))
1932
+ .reduce((widest, width) => Math.max(widest, width), 0);
1933
+ const needsLandscape = widestTableTwips > (a4Portrait.width - standardMarginTwips * 2);
1934
+ const page = needsLandscape ? a4Landscape : a4Portrait;
1935
+ const sideMarginTwips = needsLandscape && widestTableTwips > (page.width - standardMarginTwips * 2)
1936
+ ? narrowMarginTwips
1937
+ : standardMarginTwips;
1938
+ return {
1939
+ pageWidthTwips: page.width,
1940
+ pageHeightTwips: page.height,
1941
+ marginTopTwips: standardMarginTwips,
1942
+ marginRightTwips: sideMarginTwips,
1943
+ marginBottomTwips: standardMarginTwips,
1944
+ marginLeftTwips: sideMarginTwips,
1945
+ gutterTwips: 0,
1946
+ headerTopTwips: 720,
1947
+ footerBottomTwips: 720,
1948
+ columns: 1,
1949
+ columnGapTwips: 720,
1950
+ lineBetweenColumns: false,
1951
+ rtlGutter: false,
1952
+ explicitColumnWidthsTwips: [],
1953
+ explicitColumnSpacingsTwips: []
1954
+ };
1955
+ }
1956
+ static defaultLegacyLayoutMetrics() {
1957
+ return {
1958
+ pageWidthTwips: 11906,
1959
+ pageHeightTwips: 16838,
1960
+ marginTopTwips: 1440,
1961
+ marginRightTwips: 1440,
1962
+ marginBottomTwips: 1440,
1963
+ marginLeftTwips: 1440,
1964
+ gutterTwips: 0,
1965
+ headerTopTwips: 720,
1966
+ footerBottomTwips: 720,
1967
+ columns: 1,
1968
+ columnGapTwips: 720,
1969
+ lineBetweenColumns: false,
1970
+ rtlGutter: false,
1971
+ explicitColumnWidthsTwips: [],
1972
+ explicitColumnSpacingsTwips: []
1973
+ };
1974
+ }
1975
+ static buildLegacySections(blocks, headerFooterBySection) {
1976
+ if (blocks.length === 0) {
1977
+ return [];
1978
+ }
1979
+ const sections = [];
1980
+ let currentBlocks = [];
1981
+ const flushSection = () => {
1982
+ if (currentBlocks.length === 0) {
1983
+ return;
1984
+ }
1985
+ sections.push({
1986
+ sectionIndex: currentBlocks[0]?.sectionIndex,
1987
+ layout: this.inferLegacyLayoutMetrics(currentBlocks),
1988
+ blocks: currentBlocks,
1989
+ headerFooter: currentBlocks[0]?.sectionIndex !== undefined
1990
+ ? headerFooterBySection?.get(currentBlocks[0].sectionIndex)
1991
+ : undefined
1992
+ });
1993
+ currentBlocks = [];
1994
+ };
1995
+ for (const block of blocks) {
1996
+ const sectionIndexChanged = currentBlocks.length > 0
1997
+ && block.sectionIndex !== undefined
1998
+ && currentBlocks[currentBlocks.length - 1].sectionIndex !== undefined
1999
+ && block.sectionIndex !== currentBlocks[currentBlocks.length - 1].sectionIndex;
2000
+ if (sectionIndexChanged && currentBlocks.length > 0) {
2001
+ flushSection();
2002
+ }
2003
+ currentBlocks.push(block);
2004
+ }
2005
+ flushSection();
2006
+ return sections;
2007
+ }
2008
+ static twipsToMm(value) {
2009
+ return (Math.max(0, value) / 1440) * 25.4;
2010
+ }
2011
+ static buildBlocks(lines) {
2012
+ const blocks = [];
2013
+ let index = 0;
2014
+ while (index < lines.length) {
2015
+ const line = lines[index];
2016
+ const text = line.text;
2017
+ if (line.embeddedChartAnchor) {
2018
+ blocks.push({
2019
+ kind: 'embedded-chart-anchor',
2020
+ objectClass: line.embeddedObjectClass,
2021
+ pageBreakBefore: line.pageBreakBefore,
2022
+ sectionIndex: line.sectionIndex,
2023
+ sectionLayout: line.sectionLayout
2024
+ });
2025
+ index += 1;
2026
+ continue;
2027
+ }
2028
+ if (line.embeddedAssetAnchor) {
2029
+ blocks.push({
2030
+ kind: 'embedded-asset-anchor',
2031
+ assetPreference: line.embeddedAssetPreference,
2032
+ pageBreakBefore: line.pageBreakBefore,
2033
+ sectionIndex: line.sectionIndex,
2034
+ sectionLayout: line.sectionLayout
2035
+ });
2036
+ index += 1;
2037
+ continue;
2038
+ }
2039
+ if (line.embeddedImageAnchor) {
2040
+ blocks.push({
2041
+ kind: 'image-gallery',
2042
+ images: [],
2043
+ floating: true,
2044
+ floatingSide: line.floatingSide,
2045
+ floatingWidthMode: line.floatingWidthMode,
2046
+ floatingPlacement: line.floatingPlacement,
2047
+ floatingClearancePx: line.floatingClearancePx,
2048
+ pageBreakBefore: line.pageBreakBefore,
2049
+ sectionIndex: line.sectionIndex,
2050
+ sectionLayout: line.sectionLayout
2051
+ });
2052
+ index += 1;
2053
+ continue;
2054
+ }
2055
+ const implicitBulletList = this.collectImplicitBulletList(lines, index);
2056
+ if (implicitBulletList) {
2057
+ blocks.push({
2058
+ kind: 'list',
2059
+ ordered: false,
2060
+ items: implicitBulletList.items,
2061
+ pageBreakBefore: line.pageBreakBefore,
2062
+ sectionIndex: line.sectionIndex,
2063
+ sectionLayout: line.sectionLayout
2064
+ });
2065
+ index = implicitBulletList.nextIndex;
2066
+ continue;
2067
+ }
2068
+ if (this.isLikelyDocumentTitle(line, index, lines)) {
2069
+ blocks.push({
2070
+ kind: 'heading',
2071
+ text: this.normalizeParagraphText(text),
2072
+ style: line.style,
2073
+ runs: line.runs,
2074
+ pageBreakBefore: line.pageBreakBefore,
2075
+ sectionIndex: line.sectionIndex,
2076
+ sectionLayout: line.sectionLayout
2077
+ });
2078
+ index += 1;
2079
+ continue;
2080
+ }
2081
+ if (this.isLikelyLeadParagraph(line, lines[index + 1])) {
2082
+ blocks.push({
2083
+ kind: 'paragraph',
2084
+ text: this.normalizeParagraphText(text),
2085
+ style: {
2086
+ ...line.style,
2087
+ bold: true,
2088
+ fontSizeHalfPoints: Math.max(line.style?.fontSizeHalfPoints ?? 0, 36),
2089
+ firstLineIndentTwips: 0
2090
+ },
2091
+ runs: this.promoteInlineRuns(line.runs, {
2092
+ bold: true,
2093
+ fontSizeHalfPoints: Math.max(line.style?.fontSizeHalfPoints ?? 0, 36),
2094
+ firstLineIndentTwips: 0
2095
+ }),
2096
+ pageBreakBefore: line.pageBreakBefore,
2097
+ sectionIndex: line.sectionIndex,
2098
+ sectionLayout: line.sectionLayout
2099
+ });
2100
+ index += 1;
2101
+ continue;
2102
+ }
2103
+ if (this.isLikelySectionLeadParagraph(line, lines[index - 1], lines[index + 1])) {
2104
+ blocks.push({
2105
+ kind: 'heading',
2106
+ text: this.normalizeParagraphText(text),
2107
+ style: {
2108
+ ...line.style,
2109
+ bold: true,
2110
+ fontSizeHalfPoints: Math.max(line.style?.fontSizeHalfPoints ?? 0, 32)
2111
+ },
2112
+ runs: this.promoteInlineRuns(line.runs, {
2113
+ bold: true,
2114
+ fontSizeHalfPoints: Math.max(line.style?.fontSizeHalfPoints ?? 0, 32)
2115
+ }),
2116
+ pageBreakBefore: line.pageBreakBefore,
2117
+ sectionIndex: line.sectionIndex,
2118
+ sectionLayout: line.sectionLayout
2119
+ });
2120
+ index += 1;
2121
+ continue;
2122
+ }
2123
+ const structuredTable = this.collectStructuredTable(lines, index);
2124
+ if (structuredTable) {
2125
+ blocks.push({
2126
+ kind: 'table',
2127
+ rows: this.buildTableCells(structuredTable.rows, structuredTable.cellMerges),
2128
+ columnWidthsTwips: structuredTable.columnWidthsTwips,
2129
+ cellMerges: structuredTable.cellMerges,
2130
+ hasExplicitBorders: structuredTable.hasExplicitBorders,
2131
+ pageBreakBefore: line.pageBreakBefore,
2132
+ sectionIndex: line.sectionIndex,
2133
+ sectionLayout: line.sectionLayout
2134
+ });
2135
+ index = structuredTable.nextIndex;
2136
+ continue;
2137
+ }
2138
+ const definitionSection = this.collectDefinitionSection(lines.map((entry) => entry.text), index);
2139
+ if (definitionSection) {
2140
+ blocks.push({ kind: 'heading', text: definitionSection.heading, sectionIndex: line.sectionIndex, sectionLayout: line.sectionLayout });
2141
+ blocks.push({ kind: 'table', rows: this.buildTableCells(definitionSection.rows), pageBreakBefore: line.pageBreakBefore, sectionIndex: line.sectionIndex, sectionLayout: line.sectionLayout });
2142
+ index = definitionSection.nextIndex;
2143
+ continue;
2144
+ }
2145
+ if (this.looksLikeSectionHeading(line, lines[index - 1], lines[index + 1])) {
2146
+ blocks.push({
2147
+ kind: 'heading',
2148
+ text: this.normalizeParagraphText(text),
2149
+ style: line.style,
2150
+ runs: line.runs,
2151
+ pageBreakBefore: line.pageBreakBefore,
2152
+ sectionIndex: line.sectionIndex,
2153
+ sectionLayout: line.sectionLayout
2154
+ });
2155
+ index += 1;
2156
+ continue;
2157
+ }
2158
+ // Avoid reconstructing generic tab-delimited/plain-text tables heuristically.
2159
+ // For legacy .doc files this frequently misclassifies flowing paragraphs as
2160
+ // side-by-side columns. Prefer binary table metadata only.
2161
+ const listKind = this.getListKind(text);
2162
+ if (listKind) {
2163
+ const items = [];
2164
+ while (index < lines.length) {
2165
+ const currentLine = lines[index];
2166
+ const match = this.getListKind(currentLine.text);
2167
+ if (!match || match.kind !== listKind.kind) {
2168
+ break;
2169
+ }
2170
+ items.push({
2171
+ text: this.normalizeParagraphText(currentLine.text.replace(match.pattern, '').trim()),
2172
+ level: currentLine.listLevel ?? 0,
2173
+ style: currentLine.style
2174
+ });
2175
+ index += 1;
2176
+ }
2177
+ blocks.push({ kind: 'list', ordered: listKind.kind === 'ordered', items, pageBreakBefore: line.pageBreakBefore, sectionIndex: line.sectionIndex, sectionLayout: line.sectionLayout });
2178
+ continue;
2179
+ }
2180
+ blocks.push({
2181
+ kind: 'paragraph',
2182
+ text: this.normalizeParagraphText(text),
2183
+ style: line.style,
2184
+ runs: line.runs,
2185
+ pageBreakBefore: line.pageBreakBefore,
2186
+ sectionIndex: line.sectionIndex,
2187
+ sectionLayout: line.sectionLayout
2188
+ });
2189
+ index += 1;
2190
+ }
2191
+ return blocks;
2192
+ }
2193
+ static collectImplicitBulletList(lines, startIndex) {
2194
+ const items = [];
2195
+ let index = startIndex;
2196
+ while (index < lines.length) {
2197
+ const line = lines[index];
2198
+ const normalizedText = this.normalizeParagraphText(line.text);
2199
+ if (!this.isImplicitBulletCandidate(line, normalizedText)) {
2200
+ break;
2201
+ }
2202
+ items.push({
2203
+ text: normalizedText,
2204
+ level: Math.max(0, line.listLevel ?? 0),
2205
+ style: line.style
2206
+ });
2207
+ index += 1;
2208
+ }
2209
+ if (items.length < 3) {
2210
+ return null;
2211
+ }
2212
+ return {
2213
+ items,
2214
+ nextIndex: index
2215
+ };
2216
+ }
2217
+ static isImplicitBulletCandidate(line, normalizedText) {
2218
+ if (!line || !normalizedText) {
2219
+ return false;
2220
+ }
2221
+ if (line.inTable
2222
+ || line.isTableTerminator
2223
+ || line.embeddedChartAnchor
2224
+ || line.embeddedImageAnchor
2225
+ || line.embeddedAssetAnchor) {
2226
+ return false;
2227
+ }
2228
+ if ((line.style?.keepWithNext ?? false) || (line.style?.pageBreakBefore ?? false)) {
2229
+ return false;
2230
+ }
2231
+ if (this.getListKind(normalizedText)) {
2232
+ return false;
2233
+ }
2234
+ const wordCount = normalizedText.split(/\s+/).length;
2235
+ return normalizedText.length <= 96 && wordCount >= 2 && wordCount <= 14;
2236
+ }
2237
+ static isLikelyLeadParagraph(line, next) {
2238
+ if (!line) {
2239
+ return false;
2240
+ }
2241
+ const normalizedText = this.normalizeParagraphText(line.text);
2242
+ if (!normalizedText || normalizedText.length > 120 || !this.hasEnoughLetters(normalizedText)) {
2243
+ return false;
2244
+ }
2245
+ const hasLeadCue = Boolean(line.style?.keepWithNext)
2246
+ || Math.abs(line.style?.firstLineIndentTwips ?? 0) >= 360;
2247
+ if (!hasLeadCue) {
2248
+ return false;
2249
+ }
2250
+ const nextText = this.normalizeParagraphText(next?.text || '');
2251
+ const nextLength = nextText.length;
2252
+ const nextIsSpacer = nextLength === 0;
2253
+ const nextLooksTabular = Boolean(next?.inTable);
2254
+ return nextLength > 120 || nextIsSpacer || nextLooksTabular;
2255
+ }
2256
+ static isLikelySectionLeadParagraph(line, previous, next) {
2257
+ if (!line) {
2258
+ return false;
2259
+ }
2260
+ const normalizedText = this.normalizeParagraphText(line.text);
2261
+ if (!normalizedText || normalizedText.length > 96 || !this.hasEnoughLetters(normalizedText)) {
2262
+ return false;
2263
+ }
2264
+ if (line.inTable
2265
+ || line.isTableTerminator
2266
+ || line.embeddedChartAnchor
2267
+ || line.embeddedImageAnchor
2268
+ || line.embeddedAssetAnchor) {
2269
+ return false;
2270
+ }
2271
+ const previousLength = this.normalizeParagraphText(previous?.text || '').length;
2272
+ const nextLength = this.normalizeParagraphText(next?.text || '').length;
2273
+ const sparseStyle = !line.style
2274
+ || Object.keys(line.style).every((key) => ['bold', 'fontSizeHalfPoints'].includes(key));
2275
+ return nextLength > 120 && (previousLength > 120 || sparseStyle);
2276
+ }
2277
+ static promoteInlineRuns(runs, style) {
2278
+ if (!runs || runs.length === 0) {
2279
+ return undefined;
2280
+ }
2281
+ return runs.map((run) => ({
2282
+ text: run.text,
2283
+ style: {
2284
+ ...run.style,
2285
+ ...style
2286
+ }
2287
+ }));
2288
+ }
2289
+ static collectDefinitionSection(lines, startIndex) {
2290
+ const heading = this.normalizeParagraphText(lines[startIndex]);
2291
+ if (!this.isCompactHeadingCandidate(heading)) {
2292
+ return null;
2293
+ }
2294
+ const rows = [];
2295
+ let index = startIndex + 1;
2296
+ while (index < lines.length) {
2297
+ const row = this.splitDefinitionColumns(lines[index]);
2298
+ if (!row) {
2299
+ break;
2300
+ }
2301
+ rows.push(row);
2302
+ index += 1;
2303
+ }
2304
+ if (rows.length < 2) {
2305
+ return null;
2306
+ }
2307
+ return {
2308
+ heading,
2309
+ rows: [['Item', 'Description'], ...rows],
2310
+ nextIndex: index
2311
+ };
2312
+ }
2313
+ static collectStructuredTable(lines, startIndex) {
2314
+ if (!lines[startIndex]?.inTable) {
2315
+ return null;
2316
+ }
2317
+ const rows = [];
2318
+ let expectedColumnCount = 0;
2319
+ let columnWidthsTwips;
2320
+ const cellMerges = [];
2321
+ let index = startIndex;
2322
+ let sawTerminator = false;
2323
+ let sawExplicitTableMetadata = false;
2324
+ let hasExplicitBorders = false;
2325
+ while (index < lines.length && lines[index].inTable) {
2326
+ const rowLines = [];
2327
+ let rowMetadata;
2328
+ while (index < lines.length && lines[index].inTable) {
2329
+ const line = lines[index];
2330
+ rowLines.push(line);
2331
+ expectedColumnCount = Math.max(expectedColumnCount, line.tableColumnCount ?? 0);
2332
+ if (!columnWidthsTwips && line.tableColumnWidthsTwips?.length) {
2333
+ columnWidthsTwips = line.tableColumnWidthsTwips;
2334
+ }
2335
+ if (line.tableColumnCount || line.tableCellMerges?.length || line.isTableTerminator) {
2336
+ rowMetadata = line;
2337
+ if ((line.tableColumnCount ?? 0) >= 2 || (line.tableCellMerges?.length ?? 0) > 0) {
2338
+ sawExplicitTableMetadata = true;
2339
+ }
2340
+ }
2341
+ hasExplicitBorders = hasExplicitBorders || Boolean(line.tableHasExplicitBorders);
2342
+ index += 1;
2343
+ if (line.isTableTerminator) {
2344
+ sawTerminator = true;
2345
+ break;
2346
+ }
2347
+ }
2348
+ const row = this.buildStructuredTableRow(rowLines, rowMetadata?.tableColumnCount ?? expectedColumnCount);
2349
+ if (row.length > 0) {
2350
+ rows.push(row);
2351
+ cellMerges.push(rowMetadata?.tableCellMerges ?? []);
2352
+ }
2353
+ }
2354
+ if (rows.length === 0) {
2355
+ return null;
2356
+ }
2357
+ const maxColumns = Math.max(expectedColumnCount, ...rows.map((row) => row.length));
2358
+ if (maxColumns < 2) {
2359
+ return null;
2360
+ }
2361
+ if (!sawTerminator || !sawExplicitTableMetadata) {
2362
+ return null;
2363
+ }
2364
+ return {
2365
+ rows: rows.map((row) => [...row, ...Array(Math.max(0, maxColumns - row.length)).fill('')]),
2366
+ columnWidthsTwips,
2367
+ cellMerges,
2368
+ hasExplicitBorders,
2369
+ nextIndex: index
2370
+ };
2371
+ }
2372
+ static buildStructuredTableRow(lines, expectedColumnCount = 0) {
2373
+ const columns = [];
2374
+ for (const line of lines) {
2375
+ const source = (line.runs?.map((run) => run.text).join('') ?? line.text)
2376
+ .replace(/\u0007/g, '\t');
2377
+ if (source.includes('\t')) {
2378
+ columns.push(...this.splitStructuredTableRow(line, 0));
2379
+ continue;
2380
+ }
2381
+ const normalized = this.normalizeParagraphText(source);
2382
+ if (normalized.length > 0) {
2383
+ columns.push(normalized);
2384
+ }
2385
+ }
2386
+ while (columns.length > 0 && columns[columns.length - 1].length === 0 && (expectedColumnCount === 0 || columns.length > expectedColumnCount)) {
2387
+ columns.pop();
2388
+ }
2389
+ while (expectedColumnCount > 0 && columns.length < expectedColumnCount) {
2390
+ columns.push('');
2391
+ }
2392
+ return columns;
2393
+ }
2394
+ static splitStructuredTableRow(line, expectedColumnCount = 0) {
2395
+ const source = (line.runs?.map((run) => run.text).join('') ?? line.text)
2396
+ .replace(/\u0007/g, '\t');
2397
+ const columns = source
2398
+ .split('\t')
2399
+ .map((cell) => this.normalizeParagraphText(cell));
2400
+ while (columns.length > 0 && columns[columns.length - 1].length === 0 && (expectedColumnCount === 0 || columns.length > expectedColumnCount)) {
2401
+ columns.pop();
2402
+ }
2403
+ while (expectedColumnCount > 0 && columns.length < expectedColumnCount) {
2404
+ columns.push('');
2405
+ }
2406
+ return columns;
2407
+ }
2408
+ static buildTableCells(rows, cellMerges) {
2409
+ if (rows.length === 0) {
2410
+ return [];
2411
+ }
2412
+ const columnCount = Math.max(...rows.map((row) => row.length));
2413
+ const grid = rows.map((row) => [...row, ...Array(Math.max(0, columnCount - row.length)).fill('')]);
2414
+ const covered = Array.from({ length: grid.length }, () => Array(columnCount).fill(false));
2415
+ const result = [];
2416
+ for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
2417
+ const renderedRow = [];
2418
+ for (let colIndex = 0; colIndex < columnCount; colIndex++) {
2419
+ if (covered[rowIndex][colIndex]) {
2420
+ continue;
2421
+ }
2422
+ const text = grid[rowIndex][colIndex];
2423
+ let colspan = 1;
2424
+ let rowspan = 1;
2425
+ const merge = cellMerges?.[rowIndex]?.[colIndex];
2426
+ if (merge?.vertMerge === 1 || merge?.horzMerge === 1) {
2427
+ continue;
2428
+ }
2429
+ while (colIndex + colspan < columnCount
2430
+ && (cellMerges?.[rowIndex]?.[colIndex + colspan]?.horzMerge === 1
2431
+ || (!cellMerges?.[rowIndex]?.length && grid[rowIndex][colIndex + colspan].length === 0))) {
2432
+ colspan += 1;
2433
+ }
2434
+ if (text.length > 0 || merge?.vertMerge === 2) {
2435
+ let nextRow = rowIndex + 1;
2436
+ while (nextRow < grid.length) {
2437
+ let canExtend = true;
2438
+ for (let spanIndex = 0; spanIndex < colspan; spanIndex++) {
2439
+ const nextMerge = cellMerges?.[nextRow]?.[colIndex + spanIndex];
2440
+ const mergedDown = nextMerge?.vertMerge === 1;
2441
+ const emptyFallback = !cellMerges?.[nextRow]?.length && grid[nextRow][colIndex + spanIndex].length === 0;
2442
+ if (!mergedDown && !emptyFallback) {
2443
+ canExtend = false;
2444
+ break;
2445
+ }
2446
+ }
2447
+ if (!canExtend) {
2448
+ break;
2449
+ }
2450
+ rowspan += 1;
2451
+ nextRow += 1;
2452
+ }
2453
+ }
2454
+ for (let rowSpanIndex = 0; rowSpanIndex < rowspan; rowSpanIndex++) {
2455
+ for (let colSpanIndex = 0; colSpanIndex < colspan; colSpanIndex++) {
2456
+ covered[rowIndex + rowSpanIndex][colIndex + colSpanIndex] = rowSpanIndex !== 0 || colSpanIndex !== 0;
2457
+ }
2458
+ }
2459
+ renderedRow.push({
2460
+ text,
2461
+ colspan: colspan > 1 ? colspan : undefined,
2462
+ rowspan: rowspan > 1 ? rowspan : undefined
2463
+ });
2464
+ colIndex += colspan - 1;
2465
+ }
2466
+ result.push(renderedRow);
2467
+ }
2468
+ return result;
2469
+ }
2470
+ static renderBlocks(blocks) {
2471
+ const rendered = [];
2472
+ for (const block of blocks) {
2473
+ const semanticModel = this.buildSemanticBlockModel(block, rendered.length === 0);
2474
+ const model = semanticModel ? this.buildRenderedBlockModel(semanticModel) : undefined;
2475
+ if (!model) {
2476
+ continue;
2477
+ }
2478
+ rendered.push(this.wrapLegacyBlock(model));
2479
+ }
2480
+ return rendered.join('\n');
2481
+ }
2482
+ static buildSemanticBlockModel(block, isFirstRenderableBlock) {
2483
+ if (block.kind === 'heading') {
2484
+ const merged = this.flattenSingleRunBlock(block.text, block.style, block.runs);
2485
+ return {
2486
+ kind: 'content',
2487
+ tag: isFirstRenderableBlock ? 'h1' : 'h2',
2488
+ text: merged.text,
2489
+ inlineTokens: this.buildSemanticInlineTokens(merged.runs, merged.text),
2490
+ pageBreakBefore: block.pageBreakBefore,
2491
+ style: merged.style
2492
+ };
2493
+ }
2494
+ if (block.kind === 'paragraph') {
2495
+ const merged = this.flattenSingleRunBlock(block.text, block.style, block.runs);
2496
+ return {
2497
+ kind: 'content',
2498
+ tag: 'p',
2499
+ text: merged.text,
2500
+ inlineTokens: this.buildSemanticInlineTokens(merged.runs, merged.text),
2501
+ semanticRole: this.inferSemanticContentRole(merged.text, merged.style),
2502
+ pageBreakBefore: block.pageBreakBefore,
2503
+ style: merged.style
2504
+ };
2505
+ }
2506
+ if (block.kind === 'list') {
2507
+ return {
2508
+ kind: 'list',
2509
+ ordered: block.ordered,
2510
+ items: block.items.map((item) => ({
2511
+ text: item.text,
2512
+ level: item.level,
2513
+ style: {
2514
+ ...item.style,
2515
+ marginLeftTwips: (item.level || 0) * 360
2516
+ }
2517
+ })),
2518
+ pageBreakBefore: block.pageBreakBefore
2519
+ };
2520
+ }
2521
+ if (block.kind === 'table') {
2522
+ return {
2523
+ kind: 'table',
2524
+ table: this.buildSemanticTableModel(block),
2525
+ pageBreakBefore: block.pageBreakBefore
2526
+ };
2527
+ }
2528
+ if (block.kind === 'embedded-sheet') {
2529
+ return {
2530
+ kind: 'sheet',
2531
+ title: block.title,
2532
+ chart: block.chart,
2533
+ rows: block.rows,
2534
+ headerRowCount: this.detectEmbeddedSheetHeaderRowCount(block.rows),
2535
+ objectPlacementMode: block.objectPlacementMode,
2536
+ pageBreakBefore: block.pageBreakBefore
2537
+ };
2538
+ }
2539
+ if (block.kind === 'image') {
2540
+ return {
2541
+ kind: 'image',
2542
+ src: block.src,
2543
+ alt: block.alt,
2544
+ floating: block.floating,
2545
+ floatingSide: block.floatingSide,
2546
+ floatingWidthMode: block.floatingWidthMode,
2547
+ floatingPlacement: block.floatingPlacement,
2548
+ floatingClearancePx: block.floatingClearancePx,
2549
+ pageBreakBefore: block.pageBreakBefore
2550
+ };
2551
+ }
2552
+ if (block.kind === 'image-gallery' && block.images.length > 0) {
2553
+ return {
2554
+ kind: 'images',
2555
+ images: block.images,
2556
+ floating: block.floating,
2557
+ floatingSide: block.floatingSide,
2558
+ floatingWidthMode: block.floatingWidthMode,
2559
+ floatingPlacement: block.floatingPlacement,
2560
+ floatingClearancePx: block.floatingClearancePx,
2561
+ pageBreakBefore: block.pageBreakBefore
2562
+ };
2563
+ }
2564
+ return undefined;
2565
+ }
2566
+ static buildRenderedBlockModel(block) {
2567
+ if (block.kind === 'content') {
2568
+ const content = this.renderSemanticInlineTokens(block.inlineTokens, block.text);
2569
+ const hasInlineField = !!block.inlineTokens?.some((token) => token.kind === 'field');
2570
+ const hasInlineBreak = !!block.inlineTokens?.some((token) => token.kind === 'tab' || token.kind === 'line-break');
2571
+ const textLength = this.stripFieldCodeNoise(block.text).trim().length;
2572
+ return {
2573
+ kind: 'content',
2574
+ html: `<${block.tag}${this.renderInlineStyleAttribute(block.style)}>${content}</${block.tag}>`,
2575
+ pageBreakBefore: block.pageBreakBefore,
2576
+ style: block.style,
2577
+ semanticKind: block.kind,
2578
+ semanticTag: block.tag,
2579
+ semanticRole: block.semanticRole,
2580
+ textLength,
2581
+ hasInlineField,
2582
+ hasInlineBreak,
2583
+ estimatedHeightPx: this.estimateContentBlockHeightPx(block.tag, textLength, hasInlineBreak, block.semanticRole),
2584
+ minimumFragmentHeightPx: this.estimateContentBlockHeightPx(block.tag, Math.min(textLength, 72), hasInlineBreak, block.semanticRole)
2585
+ };
2586
+ }
2587
+ if (block.kind === 'list') {
2588
+ const listTag = block.ordered ? 'ol' : 'ul';
2589
+ const itemCount = block.items.length;
2590
+ return {
2591
+ kind: 'content',
2592
+ html: `<${listTag}>${block.items.map((item) => {
2593
+ const style = this.renderInlineStyleAttribute(item.style);
2594
+ return `<li${style}>${this.escapeHtml(item.text)}</li>`;
2595
+ }).join('')}</${listTag}>`,
2596
+ pageBreakBefore: block.pageBreakBefore,
2597
+ semanticKind: block.kind,
2598
+ itemCount,
2599
+ estimatedHeightPx: this.estimateListBlockHeightPx(itemCount),
2600
+ minimumFragmentHeightPx: this.estimateListBlockHeightPx(Math.min(itemCount, 2))
2601
+ };
2602
+ }
2603
+ if (block.kind === 'table') {
2604
+ const rowCount = block.table.rows.length;
2605
+ return {
2606
+ kind: 'table',
2607
+ html: this.renderRenderedTableModel(this.buildRenderedTableModel(block.table)),
2608
+ pageBreakBefore: block.pageBreakBefore,
2609
+ semanticKind: block.kind,
2610
+ rowCount,
2611
+ estimatedHeightPx: this.estimateTableBlockHeightPx(rowCount, block.table.headerRowCount),
2612
+ minimumFragmentHeightPx: this.estimateTableBlockHeightPx(Math.min(rowCount, Math.max(block.table.headerRowCount + 2, 3)), Math.min(block.table.headerRowCount, rowCount))
2613
+ };
2614
+ }
2615
+ if (block.kind === 'sheet') {
2616
+ const parts = ['<section class="ov-doc-embedded-sheet"><div class="ov-doc-embedded-sheet-card">'];
2617
+ if (block.title) {
2618
+ parts.push(`<div class="ov-doc-embedded-sheet-head"><h2>${this.escapeHtml(block.title)}</h2></div>`);
2619
+ }
2620
+ if (block.chart) {
2621
+ parts.push(this.renderEmbeddedChart(block.chart));
2622
+ }
2623
+ if (block.rows) {
2624
+ const headerRowCount = Math.min(block.headerRowCount || 0, block.rows.length);
2625
+ const headerRows = block.rows.slice(0, headerRowCount);
2626
+ const bodyRows = block.rows.slice(headerRowCount);
2627
+ const renderRows = (rows, cellTag) => rows
2628
+ .map((row) => `<tr>${row.map((cell) => `<${cellTag}>${this.escapeHtml(cell)}</${cellTag}>`).join('')}</tr>`)
2629
+ .join('');
2630
+ const theadHtml = headerRows.length > 0 ? `<thead>${renderRows(headerRows, 'th')}</thead>` : '';
2631
+ const tbodyHtml = bodyRows.length > 0 ? `<tbody>${renderRows(bodyRows, 'td')}</tbody>` : '';
2632
+ parts.push(`<div class="ov-doc-embedded-table-wrap"><div class="ov-doc-embedded-table-label">Data Table</div><div class="ov-doc-legacy-table" data-ov-table-header-rows="${headerRows.length}"><table>${theadHtml}${tbodyHtml}</table></div></div>`);
2633
+ }
2634
+ parts.push(`</div></section>`);
2635
+ return {
2636
+ kind: 'sheet',
2637
+ html: parts.join(''),
2638
+ pageBreakBefore: block.pageBreakBefore,
2639
+ semanticKind: block.kind,
2640
+ rowCount: block.rows?.length || 0,
2641
+ objectPlacementMode: block.objectPlacementMode,
2642
+ estimatedHeightPx: this.estimateSheetBlockHeightPx(block.rows?.length || 0, !!block.chart),
2643
+ minimumFragmentHeightPx: this.estimateSheetBlockHeightPx(Math.min(block.rows?.length || 0, 3), !!block.chart)
2644
+ };
2645
+ }
2646
+ if (block.kind === 'image') {
2647
+ const floatingClass = block.floating
2648
+ ? ` ov-doc-legacy-image-floating ov-doc-legacy-image-floating-${block.floatingSide || 'right'} ov-doc-legacy-image-floating-${block.floatingWidthMode || 'regular'}`
2649
+ : '';
2650
+ const figureClass = `ov-doc-legacy-image ov-doc-legacy-image-inline${floatingClass}`;
2651
+ const captionHtml = block.alt ? `<figcaption>${this.escapeHtml(block.alt)}</figcaption>` : '';
2652
+ return {
2653
+ kind: 'image',
2654
+ html: `<figure class="${figureClass}"><img src="${block.src}" alt="${this.escapeHtml(block.alt)}">${captionHtml}</figure>`,
2655
+ pageBreakBefore: block.pageBreakBefore,
2656
+ semanticKind: block.kind,
2657
+ semanticRole: block.floating ? 'floating-media' : undefined,
2658
+ mediaCount: 1,
2659
+ estimatedHeightPx: this.estimateImageBlockHeightPx(1),
2660
+ minimumFragmentHeightPx: this.estimateImageBlockHeightPx(1),
2661
+ floatingSide: block.floatingSide,
2662
+ floatingWidthMode: block.floatingWidthMode,
2663
+ floatingPlacement: block.floatingPlacement,
2664
+ floatingClearancePx: block.floatingClearancePx
2665
+ };
2666
+ }
2667
+ if (block.kind === 'images') {
2668
+ const items = block.images
2669
+ .map((image) => `<figure class="ov-doc-legacy-image${block.floating ? ` ov-doc-legacy-image-floating ov-doc-legacy-image-floating-${block.floatingSide || 'right'} ov-doc-legacy-image-floating-${block.floatingWidthMode || 'regular'}` : ''}"><img src="${image.src}" alt="${this.escapeHtml(image.alt)}">${image.alt ? `<figcaption>${this.escapeHtml(image.alt)}</figcaption>` : ''}</figure>`)
2670
+ .join('');
2671
+ const mediaCount = block.images.length;
2672
+ return {
2673
+ kind: 'images',
2674
+ html: block.images.length === 1
2675
+ ? `<section class="ov-doc-legacy-images"><div class="ov-doc-legacy-image-grid">${items}</div></section>`
2676
+ : `<section class="ov-doc-legacy-images"><h2>Images</h2><div class="ov-doc-legacy-image-grid">${items}</div></section>`,
2677
+ pageBreakBefore: block.pageBreakBefore,
2678
+ semanticKind: block.kind,
2679
+ semanticRole: block.floating ? 'floating-media' : undefined,
2680
+ mediaCount,
2681
+ estimatedHeightPx: this.estimateImageBlockHeightPx(mediaCount),
2682
+ minimumFragmentHeightPx: this.estimateImageBlockHeightPx(Math.min(mediaCount, 2)),
2683
+ floatingSide: block.floatingSide,
2684
+ floatingWidthMode: block.floatingWidthMode,
2685
+ floatingPlacement: block.floatingPlacement,
2686
+ floatingClearancePx: block.floatingClearancePx
2687
+ };
2688
+ }
2689
+ return undefined;
2690
+ }
2691
+ static renderRenderedTableModel(tableModel) {
2692
+ const renderRow = (row) => (`<tr>${row.cells.map((cell) => {
2693
+ const colspanAttr = cell.colspan && cell.colspan > 1 ? ` colspan="${cell.colspan}"` : '';
2694
+ const rowspanAttr = cell.rowspan && cell.rowspan > 1 ? ` rowspan="${cell.rowspan}"` : '';
2695
+ return `<${row.cellTag}${colspanAttr}${rowspanAttr}>${this.escapeHtml(cell.text)}</${row.cellTag}>`;
2696
+ }).join('')}</tr>`);
2697
+ const theadHtml = tableModel.headerRows.length > 0
2698
+ ? `<thead>${tableModel.headerRows.map((row) => renderRow(row)).join('')}</thead>`
2699
+ : '';
2700
+ const tbodyHtml = `<tbody>${tableModel.bodyRows.map((row) => renderRow(row)).join('')}</tbody>`;
2701
+ const explicitBordersAttr = tableModel.hasExplicitBorders ? ' data-ov-explicit-borders="true"' : '';
2702
+ return `<div class="ov-doc-legacy-table" data-ov-table-header-rows="${tableModel.headerRowCount}"${explicitBordersAttr}><table>${tableModel.colGroupHtml}${theadHtml}${tbodyHtml}</table></div>`;
2703
+ }
2704
+ static buildRenderedTableModel(tableModel) {
2705
+ return {
2706
+ columnCount: tableModel.columnCount,
2707
+ colGroupHtml: this.renderTableColGroup(tableModel.columnWidthsTwips, tableModel.columnCount),
2708
+ headerRowCount: tableModel.headerRowCount,
2709
+ hasExplicitBorders: tableModel.hasExplicitBorders,
2710
+ headerRows: tableModel.rows
2711
+ .filter((row) => row.rowKind === 'header')
2712
+ .map((row) => ({
2713
+ cellTag: 'th',
2714
+ cells: row.cells.map((cell) => ({
2715
+ text: cell.text,
2716
+ colspan: cell.colspan,
2717
+ rowspan: cell.rowspan
2718
+ }))
2719
+ })),
2720
+ bodyRows: tableModel.rows
2721
+ .filter((row) => row.rowKind === 'body')
2722
+ .map((row) => ({
2723
+ cellTag: 'td',
2724
+ cells: row.cells.map((cell) => ({
2725
+ text: cell.text,
2726
+ colspan: cell.colspan,
2727
+ rowspan: cell.rowspan
2728
+ }))
2729
+ }))
2730
+ };
2731
+ }
2732
+ static wrapLegacyBlock(block) {
2733
+ const attrs = [
2734
+ `class="ov-doc-legacy-block ov-doc-legacy-block-${block.kind}"`,
2735
+ block.pageBreakBefore ? 'data-ov-page-break-before="true"' : '',
2736
+ block.style?.keepWithNext ? 'data-ov-keep-with-next="true"' : '',
2737
+ block.style?.keepLinesTogether ? 'data-ov-keep-lines-together="true"' : '',
2738
+ block.semanticKind ? `data-ov-semantic-kind="${block.semanticKind}"` : '',
2739
+ block.semanticTag ? `data-ov-semantic-tag="${block.semanticTag}"` : '',
2740
+ block.semanticRole ? `data-ov-semantic-role="${block.semanticRole}"` : '',
2741
+ typeof block.textLength === 'number' ? `data-ov-text-length="${block.textLength}"` : '',
2742
+ block.hasInlineField ? 'data-ov-inline-field="true"' : '',
2743
+ block.hasInlineBreak ? 'data-ov-inline-break="true"' : '',
2744
+ typeof block.itemCount === 'number' ? `data-ov-item-count="${block.itemCount}"` : '',
2745
+ typeof block.rowCount === 'number' ? `data-ov-row-count="${block.rowCount}"` : '',
2746
+ typeof block.mediaCount === 'number' ? `data-ov-media-count="${block.mediaCount}"` : '',
2747
+ typeof block.estimatedHeightPx === 'number' ? `data-ov-estimated-height="${block.estimatedHeightPx}"` : '',
2748
+ typeof block.minimumFragmentHeightPx === 'number' ? `data-ov-min-fragment-height="${block.minimumFragmentHeightPx}"` : '',
2749
+ block.floatingSide ? `data-ov-floating-side="${block.floatingSide}"` : '',
2750
+ block.floatingWidthMode ? `data-ov-floating-width="${block.floatingWidthMode}"` : '',
2751
+ block.floatingPlacement ? `data-ov-floating-placement="${block.floatingPlacement}"` : '',
2752
+ typeof block.floatingClearancePx === 'number' ? `data-ov-floating-clearance="${block.floatingClearancePx}"` : '',
2753
+ block.objectPlacementMode ? `data-ov-object-placement="${block.objectPlacementMode}"` : ''
2754
+ ].filter(Boolean).join(' ');
2755
+ return `<div ${attrs}>${block.html}</div>`;
2756
+ }
2757
+ static detectEmbeddedSheetHeaderRowCount(rows) {
2758
+ if (!rows || rows.length === 0) {
2759
+ return 0;
2760
+ }
2761
+ if (rows.length === 1) {
2762
+ return 1;
2763
+ }
2764
+ const normalize = (value) => String(value || '').trim();
2765
+ const firstRow = rows[0];
2766
+ const secondRow = rows[1];
2767
+ const firstFilled = firstRow.filter((cell) => normalize(cell).length > 0).length;
2768
+ const secondFilled = secondRow.filter((cell) => normalize(cell).length > 0).length;
2769
+ const firstNumeric = firstRow.filter((cell) => /^[-+]?[\d,.%]+$/.test(normalize(cell))).length;
2770
+ const secondNumeric = secondRow.filter((cell) => /^[-+]?[\d,.%]+$/.test(normalize(cell))).length;
2771
+ if (rows.length > 2 && firstFilled > 0 && secondFilled > 0 && firstNumeric === 0 && secondNumeric === 0) {
2772
+ return 2;
2773
+ }
2774
+ return 1;
2775
+ }
2776
+ static inferFloatingSideFromStyle(style) {
2777
+ if (style?.textAlign === 'center') {
2778
+ return 'center';
2779
+ }
2780
+ if (style?.textAlign === 'right') {
2781
+ return 'right';
2782
+ }
2783
+ if ((style?.marginRightTwips ?? 0) > (style?.marginLeftTwips ?? 0)) {
2784
+ return 'left';
2785
+ }
2786
+ return 'right';
2787
+ }
2788
+ static inferFloatingWidthModeFromStyle(style) {
2789
+ if (style?.textAlign === 'center') {
2790
+ return 'wide';
2791
+ }
2792
+ const left = Math.max(0, style?.marginLeftTwips ?? 0);
2793
+ const right = Math.max(0, style?.marginRightTwips ?? 0);
2794
+ const total = left + right;
2795
+ if (total >= 1440) {
2796
+ return 'narrow';
2797
+ }
2798
+ if (total <= 240) {
2799
+ return 'wide';
2800
+ }
2801
+ return 'regular';
2802
+ }
2803
+ static inferFloatingPlacementFromStyle(style) {
2804
+ const side = this.inferFloatingSideFromStyle(style);
2805
+ const widthMode = this.inferFloatingWidthModeFromStyle(style);
2806
+ if (side === 'center' || widthMode === 'wide') {
2807
+ return 'center-block';
2808
+ }
2809
+ return 'edge-wrap';
2810
+ }
2811
+ static inferFloatingClearancePx(style) {
2812
+ const side = this.inferFloatingSideFromStyle(style);
2813
+ const widthMode = this.inferFloatingWidthModeFromStyle(style);
2814
+ if (side === 'center') {
2815
+ return 18;
2816
+ }
2817
+ if (widthMode === 'wide') {
2818
+ return 16;
2819
+ }
2820
+ if (widthMode === 'narrow') {
2821
+ return 8;
2822
+ }
2823
+ return 12;
2824
+ }
2825
+ static estimateContentBlockHeightPx(tag, textLength, hasInlineBreak, semanticRole) {
2826
+ const base = tag === 'h1' ? 64 : tag === 'h2' ? 52 : semanticRole === 'caption' ? 42 : 34;
2827
+ const approxLines = Math.max(1, Math.ceil(textLength / (tag === 'p' ? 72 : 40)) + (hasInlineBreak ? 1 : 0));
2828
+ return base + Math.max(0, approxLines - 1) * 18;
2829
+ }
2830
+ static estimateListBlockHeightPx(itemCount) {
2831
+ return 28 + Math.max(1, itemCount) * 24;
2832
+ }
2833
+ static estimateTableBlockHeightPx(rowCount, headerRowCount) {
2834
+ return 32 + Math.max(1, headerRowCount) * 28 + Math.max(0, rowCount - headerRowCount) * 24;
2835
+ }
2836
+ static estimateSheetBlockHeightPx(rowCount, hasChart) {
2837
+ return 72 + (hasChart ? 180 : 0) + Math.max(0, rowCount) * 22;
2838
+ }
2839
+ static estimateImageBlockHeightPx(mediaCount) {
2840
+ return mediaCount <= 1 ? 260 : 120 + (Math.ceil(mediaCount / 2) * 180);
2841
+ }
2842
+ static flattenSingleRunBlock(text, style, runs) {
2843
+ if (!runs || runs.length !== 1) {
2844
+ return { text, style, runs };
2845
+ }
2846
+ const [run] = runs;
2847
+ if (this.normalizeParagraphText(run.text, true) !== this.normalizeParagraphText(text, true)) {
2848
+ return { text, style, runs };
2849
+ }
2850
+ return {
2851
+ text: run.text,
2852
+ style: this.mergeStyles(style, run.style),
2853
+ runs: undefined
2854
+ };
2855
+ }
2856
+ static renderInlineStyleAttribute(style) {
2857
+ if (!style) {
2858
+ return '';
2859
+ }
2860
+ const declarations = [];
2861
+ if (style.bold) {
2862
+ declarations.push('font-weight:700');
2863
+ }
2864
+ if (style.italic) {
2865
+ declarations.push('font-style:italic');
2866
+ }
2867
+ if (style.underline) {
2868
+ declarations.push('text-decoration:underline');
2869
+ }
2870
+ if (style.fontSizeHalfPoints && style.fontSizeHalfPoints >= 2) {
2871
+ declarations.push(`font-size:${(style.fontSizeHalfPoints / 2).toFixed(1)}pt`);
2872
+ declarations.push(`line-height:${Math.max(1.15, Math.min(1.9, (style.fontSizeHalfPoints / 24) + 0.8)).toFixed(2)}`);
2873
+ }
2874
+ if (style.color) {
2875
+ declarations.push(`color:${style.color}`);
2876
+ }
2877
+ const safeBackground = this.sanitizeRenderableBackground(style.backgroundColor);
2878
+ if (safeBackground) {
2879
+ declarations.push(`background-color:${safeBackground}`);
2880
+ }
2881
+ const safeHighlight = this.sanitizeRenderableBackground(style.highlightColor);
2882
+ if (safeHighlight) {
2883
+ declarations.push(`background-color:${safeHighlight}`);
2884
+ }
2885
+ if (style.textAlign) {
2886
+ declarations.push(`text-align:${style.textAlign}`);
2887
+ }
2888
+ if (style.marginLeftTwips) {
2889
+ declarations.push(`margin-left:${(style.marginLeftTwips / 20).toFixed(1)}pt`);
2890
+ }
2891
+ if (style.marginRightTwips) {
2892
+ declarations.push(`margin-right:${(style.marginRightTwips / 20).toFixed(1)}pt`);
2893
+ }
2894
+ if (style.firstLineIndentTwips) {
2895
+ declarations.push(`text-indent:${(style.firstLineIndentTwips / 20).toFixed(1)}pt`);
2896
+ }
2897
+ return declarations.length > 0 ? ` style="${declarations.join(';')}"` : '';
2898
+ }
2899
+ static renderInlineStyledText(runs, fallbackText) {
2900
+ if (!runs || runs.length === 0) {
2901
+ return this.escapeHtml(this.stripFieldCodeNoise(fallbackText));
2902
+ }
2903
+ return runs
2904
+ .map((run) => {
2905
+ const text = this.escapeHtml(this.stripFieldCodeNoise(run.text));
2906
+ const styleAttr = this.renderInlineStyleAttribute(run.style);
2907
+ return styleAttr ? `<span${styleAttr}>${text}</span>` : text;
2908
+ })
2909
+ .join('');
2910
+ }
2911
+ static buildSemanticInlineTokens(runs, fallbackText) {
2912
+ if (!runs || runs.length === 0) {
2913
+ const tokens = this.tokenizeSemanticInlineText(this.stripFieldCodeNoise(fallbackText));
2914
+ return tokens.length > 0 ? tokens : undefined;
2915
+ }
2916
+ const tokens = runs.flatMap((run) => this.tokenizeSemanticInlineText(this.stripFieldCodeNoise(run.text), run.style));
2917
+ return tokens.length > 0 ? tokens : undefined;
2918
+ }
2919
+ static renderSemanticInlineTokens(tokens, fallbackText) {
2920
+ if (!tokens || tokens.length === 0) {
2921
+ return this.escapeHtml(this.stripFieldCodeNoise(fallbackText));
2922
+ }
2923
+ return tokens
2924
+ .map((token) => {
2925
+ if (token.kind === 'tab') {
2926
+ return '\t';
2927
+ }
2928
+ if (token.kind === 'line-break') {
2929
+ return '<br>';
2930
+ }
2931
+ const text = this.escapeHtml(token.kind === 'field' ? token.field : token.text);
2932
+ const styleAttr = this.renderInlineStyleAttribute(token.style);
2933
+ return styleAttr ? `<span${styleAttr}>${text}</span>` : text;
2934
+ })
2935
+ .join('');
2936
+ }
2937
+ static tokenizeSemanticInlineText(raw, style) {
2938
+ if (!raw) {
2939
+ return [];
2940
+ }
2941
+ const tokens = [];
2942
+ const pattern = /(SECTIONPAGES|SECTIONPAGE|NUMPAGES|SECTIONS|SECTION|PAGE)|(\t)|(\n)/g;
2943
+ let lastIndex = 0;
2944
+ let match;
2945
+ while ((match = pattern.exec(raw)) !== null) {
2946
+ if (match.index > lastIndex) {
2947
+ tokens.push({ kind: 'text', text: raw.slice(lastIndex, match.index), style });
2948
+ }
2949
+ if (match[1]) {
2950
+ tokens.push({
2951
+ kind: 'field',
2952
+ field: match[1],
2953
+ style
2954
+ });
2955
+ }
2956
+ else if (match[2]) {
2957
+ tokens.push({ kind: 'tab' });
2958
+ }
2959
+ else if (match[3]) {
2960
+ tokens.push({ kind: 'line-break' });
2961
+ }
2962
+ lastIndex = match.index + match[0].length;
2963
+ }
2964
+ if (lastIndex < raw.length) {
2965
+ tokens.push({ kind: 'text', text: raw.slice(lastIndex), style });
2966
+ }
2967
+ return tokens.filter((token) => token.kind !== 'text' || token.text.length > 0);
2968
+ }
2969
+ static inferSemanticContentRole(text, style) {
2970
+ const normalized = this.stripFieldCodeNoise(text).replace(/\s+/g, ' ').trim();
2971
+ if (!normalized || normalized.length > 120) {
2972
+ return undefined;
2973
+ }
2974
+ const captionPattern = /^(figure|fig\.?|table|chart|image|photo|picture|diagram|exhibit|그림|표|사진|도표)\s*([0-9]+|[ivxlcdm]+)?(?:[\s:.)-]|$)/i;
2975
+ if (captionPattern.test(normalized)) {
2976
+ return 'caption';
2977
+ }
2978
+ if ((style?.italic || style?.textAlign === 'center') && normalized.length <= 80) {
2979
+ return 'caption';
2980
+ }
2981
+ return undefined;
2982
+ }
2983
+ static tokenizeHeaderFooterText(raw) {
2984
+ const text = String(raw || '');
2985
+ if (!text) {
2986
+ return [];
2987
+ }
2988
+ const tokens = [];
2989
+ let lastIndex = 0;
2990
+ const pattern = /\b(SECTIONPAGES|SECTIONPAGE|NUMPAGES|SECTIONS|SECTION|PAGE)\b/g;
2991
+ let match;
2992
+ while ((match = pattern.exec(text)) !== null) {
2993
+ if (match.index > lastIndex) {
2994
+ tokens.push({ kind: 'text', value: text.slice(lastIndex, match.index) });
2995
+ }
2996
+ tokens.push({
2997
+ kind: 'field',
2998
+ field: match[1].toUpperCase()
2999
+ });
3000
+ lastIndex = match.index + match[0].length;
3001
+ }
3002
+ if (lastIndex < text.length) {
3003
+ tokens.push({ kind: 'text', value: text.slice(lastIndex) });
3004
+ }
3005
+ return tokens.filter((token) => token.kind !== 'text' || token.value.length > 0);
3006
+ }
3007
+ static renderTableColGroup(columnWidthsTwips, columnCount) {
3008
+ if (!columnWidthsTwips || columnWidthsTwips.length === 0 || columnCount <= 0) {
3009
+ return '';
3010
+ }
3011
+ const widths = columnWidthsTwips.slice(0, columnCount);
3012
+ const total = widths.reduce((sum, width) => sum + Math.max(0, width), 0);
3013
+ if (total <= 0) {
3014
+ return '';
3015
+ }
3016
+ const cols = widths
3017
+ .map((width) => `<col style="width:${((Math.max(0, width) / total) * 100).toFixed(2)}%">`)
3018
+ .join('');
3019
+ return `<colgroup>${cols}</colgroup>`;
3020
+ }
3021
+ static buildSemanticTableModel(block) {
3022
+ const columnCount = Math.max(...block.rows.map((row) => row.reduce((sum, cell) => sum + (cell.colspan ?? 1), 0)));
3023
+ const headerRowCount = this.detectTableHeaderRowCount(block.rows);
3024
+ const mapRows = (rows, rowKind) => rows.map((row) => ({
3025
+ rowKind,
3026
+ cells: row.map((cell) => ({
3027
+ text: cell.text,
3028
+ colspan: cell.colspan,
3029
+ rowspan: cell.rowspan
3030
+ }))
3031
+ }));
3032
+ return {
3033
+ columnCount,
3034
+ columnWidthsTwips: block.columnWidthsTwips,
3035
+ headerRowCount,
3036
+ hasExplicitBorders: block.hasExplicitBorders,
3037
+ rows: [
3038
+ ...mapRows(block.rows.slice(0, headerRowCount), 'header'),
3039
+ ...mapRows(block.rows.slice(headerRowCount), 'body')
3040
+ ]
3041
+ };
3042
+ }
3043
+ static detectTableHeaderRowCount(rows) {
3044
+ if (rows.length < 2) {
3045
+ return 0;
3046
+ }
3047
+ const isNumericLike = (text) => {
3048
+ const normalized = this.normalizeParagraphText(text);
3049
+ return /^[-+]?[$]?\d[\d\s,./:%-]*$/.test(normalized);
3050
+ };
3051
+ const hasStructuralSpan = (row) => (row.some((cell) => (cell.colspan ?? 1) > 1 || (cell.rowspan ?? 1) > 1));
3052
+ const isDenseHeaderRow = (row) => {
3053
+ if (!row.length) {
3054
+ return false;
3055
+ }
3056
+ const nonEmptyCells = row.filter((cell) => this.normalizeParagraphText(cell.text).length > 0);
3057
+ if (nonEmptyCells.length !== row.length) {
3058
+ return false;
3059
+ }
3060
+ return nonEmptyCells.every((cell) => {
3061
+ const text = this.normalizeParagraphText(cell.text);
3062
+ return text.length > 0 && text.length <= 80;
3063
+ });
3064
+ };
3065
+ const looksLikeDataRow = (row) => {
3066
+ const normalizedCells = row
3067
+ .map((cell) => this.normalizeParagraphText(cell.text))
3068
+ .filter((text) => text.length > 0);
3069
+ if (normalizedCells.length < 2) {
3070
+ return false;
3071
+ }
3072
+ const numericLikeCount = normalizedCells.filter((text) => isNumericLike(text)).length;
3073
+ return numericLikeCount / normalizedCells.length >= 0.5;
3074
+ };
3075
+ if (!isDenseHeaderRow(rows[0])) {
3076
+ return 0;
3077
+ }
3078
+ let headerRowCount = 1;
3079
+ const maxHeaderRows = Math.min(3, rows.length - 1);
3080
+ for (let index = 1; index < maxHeaderRows; index++) {
3081
+ const row = rows[index];
3082
+ if (!isDenseHeaderRow(row)) {
3083
+ break;
3084
+ }
3085
+ if (looksLikeDataRow(row) && !hasStructuralSpan(rows[index - 1])) {
3086
+ break;
3087
+ }
3088
+ if (hasStructuralSpan(rows[index - 1]) || hasStructuralSpan(row) || !looksLikeDataRow(row)) {
3089
+ headerRowCount += 1;
3090
+ continue;
3091
+ }
3092
+ break;
3093
+ }
3094
+ return headerRowCount;
3095
+ }
3096
+ static getListKind(text) {
3097
+ const bulletPattern = /^([\u2022\u00b7\-*o])\s+/;
3098
+ if (bulletPattern.test(text)) {
3099
+ return { kind: 'bullet', pattern: bulletPattern };
3100
+ }
3101
+ const orderedPattern = /^((\d+|[A-Za-z])[.)])\s+/;
3102
+ if (orderedPattern.test(text)) {
3103
+ return { kind: 'ordered', pattern: orderedPattern };
3104
+ }
3105
+ return null;
3106
+ }
3107
+ static collectTableRows(lines, startIndex) {
3108
+ const rows = [];
3109
+ let index = startIndex;
3110
+ while (index < lines.length) {
3111
+ const columns = this.splitTableColumns(lines[index]);
3112
+ if (!columns || columns.length < 2) {
3113
+ break;
3114
+ }
3115
+ rows.push(columns);
3116
+ index += 1;
3117
+ }
3118
+ if (rows.length < 2) {
3119
+ return null;
3120
+ }
3121
+ const maxColumns = Math.max(...rows.map((row) => row.length));
3122
+ if (maxColumns < 2) {
3123
+ return null;
3124
+ }
3125
+ return {
3126
+ rows: rows.map((row) => [...row, ...Array(Math.max(0, maxColumns - row.length)).fill('')]),
3127
+ nextIndex: index
3128
+ };
3129
+ }
3130
+ static collectDefinitionTable(lines, startIndex) {
3131
+ const rows = [];
3132
+ let index = startIndex;
3133
+ while (index < lines.length) {
3134
+ const row = this.splitDefinitionColumns(lines[index]);
3135
+ if (!row) {
3136
+ break;
3137
+ }
3138
+ rows.push(row);
3139
+ index += 1;
3140
+ }
3141
+ if (rows.length < 3) {
3142
+ return null;
3143
+ }
3144
+ const uniqueKeys = new Set(rows.map((row) => row[0]));
3145
+ if (uniqueKeys.size < 3) {
3146
+ return null;
3147
+ }
3148
+ return {
3149
+ rows: [['Item', 'Description'], ...rows],
3150
+ nextIndex: index
3151
+ };
3152
+ }
3153
+ static splitTableColumns(line) {
3154
+ const tabColumns = line
3155
+ .split('\t')
3156
+ .map((cell) => this.normalizeParagraphText(cell))
3157
+ .filter(Boolean);
3158
+ if (tabColumns.length >= 2) {
3159
+ return tabColumns;
3160
+ }
3161
+ const pipeColumns = line
3162
+ .split(/\s*\|\s*/)
3163
+ .map((cell) => this.normalizeParagraphText(cell))
3164
+ .filter(Boolean);
3165
+ if (pipeColumns.length >= 2) {
3166
+ return pipeColumns;
3167
+ }
3168
+ return null;
3169
+ }
3170
+ static splitDefinitionColumns(line) {
3171
+ const normalized = this.normalizeParagraphText(line);
3172
+ const match = normalized.match(/^([^:]{1,40})\s*:\s+(.{2,})$/);
3173
+ if (!match) {
3174
+ return null;
3175
+ }
3176
+ const key = this.normalizeParagraphText(match[1]);
3177
+ const value = this.normalizeParagraphText(match[2]);
3178
+ if (!key || !value || !this.hasEnoughLetters(key) || value.length < 2) {
3179
+ return null;
3180
+ }
3181
+ return [key, value];
3182
+ }
3183
+ static looksLikeSectionHeading(line, previous, next) {
3184
+ const normalizedText = this.normalizeParagraphText(line.text);
3185
+ if (!normalizedText || normalizedText.includes(':') || /[.!?]$/.test(normalizedText)) {
3186
+ return false;
3187
+ }
3188
+ if (normalizedText.length > 24 || !this.isCompactHeadingCandidate(normalizedText)) {
3189
+ return false;
3190
+ }
3191
+ const previousLength = this.normalizeParagraphText(previous?.text || '').length;
3192
+ const nextLength = this.normalizeParagraphText(next?.text || '').length;
3193
+ const prominentStyle = Boolean(line.style?.bold)
3194
+ || Boolean(line.style?.textAlign === 'center')
3195
+ || (line.style?.fontSizeHalfPoints ?? 0) >= 26;
3196
+ return prominentStyle && nextLength > 15 && (previousLength === 0 || previousLength > 20);
3197
+ }
3198
+ static isLikelyDocumentTitle(line, index, lines) {
3199
+ const text = this.normalizeParagraphText(line.text);
3200
+ if (index > 2 || !text || text.length > 120 || !this.hasEnoughLetters(text)) {
3201
+ return false;
3202
+ }
3203
+ const nextLength = this.normalizeParagraphText(lines[index + 1]?.text || '').length;
3204
+ const strongStyle = (line.style?.fontSizeHalfPoints ?? 0) >= 28
3205
+ || Boolean(line.style?.bold && line.style?.textAlign === 'center')
3206
+ || Boolean(line.style?.bold && text.length <= 48);
3207
+ return strongStyle && nextLength > 20;
3208
+ }
3209
+ static isCompactHeadingCandidate(text) {
3210
+ const words = text.split(/\s+/).filter(Boolean);
3211
+ return text.length >= 2
3212
+ && text.length <= 24
3213
+ && words.length <= 4
3214
+ && this.hasEnoughLetters(text);
3215
+ }
3216
+ static shouldDropLine(text, previous, next) {
3217
+ const normalized = this.normalizeParagraphText(text);
3218
+ if (!normalized) {
3219
+ return true;
3220
+ }
3221
+ if (/^EMBED\s+/i.test(normalized)) {
3222
+ return true;
3223
+ }
3224
+ if (/^\d+$/.test(normalized)) {
3225
+ return true;
3226
+ }
3227
+ const prevNormalized = this.normalizeParagraphText(previous);
3228
+ const nextNormalized = this.normalizeParagraphText(next);
3229
+ const adjacentNumber = /^\d+$/.test(prevNormalized) || /^\d+$/.test(nextNormalized);
3230
+ if (adjacentNumber && normalized.length <= 48) {
3231
+ return true;
3232
+ }
3233
+ const isShortLabel = normalized.length <= 14 && /^[A-Za-z가-힣]+(?:\s+[A-Za-z가-힣]+)?$/.test(normalized);
3234
+ if (!isShortLabel) {
3235
+ return adjacentNumber && normalized.split(/\s+/).length <= 6;
3236
+ }
3237
+ const surroundedByLongParagraphs = prevNormalized.length > 20 && nextNormalized.length > 20;
3238
+ return /^\d+$/.test(prevNormalized)
3239
+ || /^\d+$/.test(nextNormalized)
3240
+ || /^EMBED\s+/i.test(prevNormalized)
3241
+ || /^EMBED\s+/i.test(nextNormalized)
3242
+ || surroundedByLongParagraphs;
3243
+ }
3244
+ static parseCfb(file) {
3245
+ if (file.length < 512) {
3246
+ throw new Error('Invalid CFB file: too small.');
3247
+ }
3248
+ if (file.slice(0, 8).toString('hex') !== 'd0cf11e0a1b11ae1') {
3249
+ throw new Error('Invalid CFB signature.');
3250
+ }
3251
+ const sectorShift = file.readUInt16LE(30);
3252
+ const miniSectorShift = file.readUInt16LE(32);
3253
+ const sectorSize = 1 << sectorShift;
3254
+ const miniSectorSize = 1 << miniSectorShift;
3255
+ const numFatSectors = file.readUInt32LE(44);
3256
+ const firstDirSector = file.readInt32LE(48);
3257
+ const miniCutoff = file.readUInt32LE(56);
3258
+ const firstMiniFatSector = file.readInt32LE(60);
3259
+ const numMiniFatSectors = file.readUInt32LE(64);
3260
+ const firstDifatSector = file.readInt32LE(68);
3261
+ const numDifatSectors = file.readUInt32LE(72);
3262
+ const readSector = (sid) => {
3263
+ const offset = (sid + 1) * sectorSize;
3264
+ if (offset < 0 || offset + sectorSize > file.length) {
3265
+ return Buffer.alloc(0);
3266
+ }
3267
+ return file.subarray(offset, offset + sectorSize);
3268
+ };
3269
+ const difat = [];
3270
+ for (let i = 0; i < 109; i++) {
3271
+ const sid = file.readInt32LE(76 + i * 4);
3272
+ if (sid !== -1) {
3273
+ difat.push(sid);
3274
+ }
3275
+ }
3276
+ let nextDifat = firstDifatSector;
3277
+ for (let i = 0; i < numDifatSectors && nextDifat !== ENDOFCHAIN && nextDifat !== -1; i++) {
3278
+ const sector = readSector(nextDifat);
3279
+ if (sector.length === 0) {
3280
+ break;
3281
+ }
3282
+ const entryCount = sectorSize / 4 - 1;
3283
+ for (let j = 0; j < entryCount; j++) {
3284
+ const sid = sector.readInt32LE(j * 4);
3285
+ if (sid !== -1) {
3286
+ difat.push(sid);
3287
+ }
3288
+ }
3289
+ nextDifat = sector.readInt32LE(sectorSize - 4);
3290
+ }
3291
+ const fatSectors = difat.slice(0, numFatSectors);
3292
+ const fat = [];
3293
+ for (const sid of fatSectors) {
3294
+ const sector = readSector(sid);
3295
+ if (sector.length === 0) {
3296
+ continue;
3297
+ }
3298
+ for (let i = 0; i < sectorSize; i += 4) {
3299
+ fat.push(sector.readInt32LE(i));
3300
+ }
3301
+ }
3302
+ const readChain = (startSid) => {
3303
+ if (startSid < 0 || startSid === ENDOFCHAIN) {
3304
+ return Buffer.alloc(0);
3305
+ }
3306
+ const chunks = [];
3307
+ const visited = new Set();
3308
+ let sid = startSid;
3309
+ while (sid >= 0 && sid !== ENDOFCHAIN && sid !== FREESECT) {
3310
+ if (visited.has(sid) || sid >= fat.length) {
3311
+ break;
3312
+ }
3313
+ visited.add(sid);
3314
+ const sector = readSector(sid);
3315
+ if (sector.length === 0) {
3316
+ break;
3317
+ }
3318
+ chunks.push(sector);
3319
+ sid = fat[sid];
3320
+ }
3321
+ return Buffer.concat(chunks);
3322
+ };
3323
+ const dirStream = readChain(firstDirSector);
3324
+ const entries = [];
3325
+ for (let offset = 0; offset + 128 <= dirStream.length; offset += 128) {
3326
+ const nameLength = dirStream.readUInt16LE(offset + 64);
3327
+ const name = dirStream
3328
+ .subarray(offset, offset + Math.max(0, nameLength - 2))
3329
+ .toString('utf16le')
3330
+ .replace(/\u0000/g, '');
3331
+ const type = dirStream.readUInt8(offset + 66);
3332
+ const startSector = dirStream.readInt32LE(offset + 116);
3333
+ const sizeLow = dirStream.readUInt32LE(offset + 120);
3334
+ const sizeHigh = dirStream.readUInt32LE(offset + 124);
3335
+ const size = sizeHigh > 0 ? Number(sizeLow) : sizeLow;
3336
+ entries.push({ name, type, startSector, size });
3337
+ }
3338
+ const root = entries.find((entry) => entry.type === 5);
3339
+ const miniStream = root
3340
+ ? readChain(root.startSector).subarray(0, root.size)
3341
+ : Buffer.alloc(0);
3342
+ const miniFatData = readChain(firstMiniFatSector);
3343
+ const miniFat = [];
3344
+ for (let i = 0; i + 4 <= miniFatData.length && i / 4 < numMiniFatSectors * (sectorSize / 4); i += 4) {
3345
+ miniFat.push(miniFatData.readInt32LE(i));
3346
+ }
3347
+ const readMiniChain = (startMiniSid, size) => {
3348
+ if (startMiniSid < 0) {
3349
+ return Buffer.alloc(0);
3350
+ }
3351
+ const chunks = [];
3352
+ const visited = new Set();
3353
+ let sid = startMiniSid;
3354
+ while (sid >= 0 && sid !== ENDOFCHAIN && sid !== FREESECT) {
3355
+ if (visited.has(sid) || sid >= miniFat.length) {
3356
+ break;
3357
+ }
3358
+ visited.add(sid);
3359
+ const start = sid * miniSectorSize;
3360
+ const end = start + miniSectorSize;
3361
+ if (start < 0 || end > miniStream.length) {
3362
+ break;
3363
+ }
3364
+ chunks.push(miniStream.subarray(start, end));
3365
+ sid = miniFat[sid];
3366
+ }
3367
+ return Buffer.concat(chunks).subarray(0, size);
3368
+ };
3369
+ const streamMap = new Map();
3370
+ for (const entry of entries) {
3371
+ if (entry.name && entry.type === 2) {
3372
+ streamMap.set(entry.name, entry);
3373
+ }
3374
+ }
3375
+ return {
3376
+ getStream: (name) => {
3377
+ const entry = streamMap.get(name);
3378
+ if (!entry) {
3379
+ return null;
3380
+ }
3381
+ if (entry.size < miniCutoff) {
3382
+ return readMiniChain(entry.startSector, entry.size);
3383
+ }
3384
+ return readChain(entry.startSector).subarray(0, entry.size);
3385
+ },
3386
+ listStreams: () => entries
3387
+ .filter((entry) => entry.name && entry.type === 2)
3388
+ .map((entry) => ({ name: entry.name, size: entry.size }))
3389
+ };
3390
+ }
3391
+ static normalizeDocumentText(raw) {
3392
+ return raw
3393
+ .replace(/\u0000/g, '')
3394
+ .replace(this.FIELD_CODE_NOISE_PATTERN, '')
3395
+ .replace(/[\u0001-\u0006\u0008\u000c\u000e-\u0012\u0014-\u001f]/g, ' ')
3396
+ .replace(/[\u0007\u000b]/g, '\n')
3397
+ .replace(/\r/g, '\n')
3398
+ .replace(/[ \t]+\n/g, '\n')
3399
+ .replace(/\n{3,}/g, '\n\n')
3400
+ .replace(/[ ]{2,}/g, ' ')
3401
+ .trim();
3402
+ }
3403
+ static normalizeParagraphText(raw, preserveTabs = false) {
3404
+ const normalized = (raw || '')
3405
+ .replace(this.FIELD_CODE_NOISE_PATTERN, '')
3406
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ' ')
3407
+ .replace(/\r/g, '');
3408
+ if (preserveTabs) {
3409
+ return normalized
3410
+ .replace(/[ \t]+\n/g, '\n')
3411
+ .trim();
3412
+ }
3413
+ return normalized
3414
+ .replace(/\s+/g, ' ')
3415
+ .trim();
3416
+ }
3417
+ static stripFieldCodeNoise(raw) {
3418
+ return (raw || '').replace(this.FIELD_CODE_NOISE_PATTERN, '');
3419
+ }
3420
+ static isNoise(text) {
3421
+ if (text.length < 3) {
3422
+ return true;
3423
+ }
3424
+ const lower = text.toLowerCase();
3425
+ if (/^(times new roman|arial|calibri|courier new|normal|heading)\b/.test(lower)) {
3426
+ return true;
3427
+ }
3428
+ if (/^[_\W]+$/.test(text)) {
3429
+ return true;
3430
+ }
3431
+ if (!this.isMostlyReadable(text)) {
3432
+ return true;
3433
+ }
3434
+ if (!this.hasEnoughLetters(text)) {
3435
+ return true;
3436
+ }
3437
+ return false;
3438
+ }
3439
+ static isMostlyReadable(text) {
3440
+ let readable = 0;
3441
+ for (let i = 0; i < text.length; i++) {
3442
+ const code = text.charCodeAt(i);
3443
+ const isPrintableAscii = code >= 32 && code <= 126;
3444
+ const isKorean = code >= 0xac00 && code <= 0xd7a3;
3445
+ const isCommonUnicode = code >= 0x00a0 && code <= 0x024f;
3446
+ if (isPrintableAscii || isKorean || isCommonUnicode) {
3447
+ readable += 1;
3448
+ }
3449
+ }
3450
+ return readable / Math.max(1, text.length) >= 0.8;
3451
+ }
3452
+ static hasEnoughLetters(text) {
3453
+ const letters = (text.match(/[A-Za-z가-힣]/g) || []).length;
3454
+ return letters >= Math.max(3, Math.floor(text.length * 0.25));
3455
+ }
3456
+ static normalizeImageLabel(label, fallbackIndex) {
3457
+ const normalized = this.normalizeParagraphText(String(label || ''));
3458
+ if (!normalized) {
3459
+ return '';
3460
+ }
3461
+ if (/^(worddocument|data|properties_stream|package_stream|\x01compobj|\x01ole)$/i.test(normalized)) {
3462
+ return '';
3463
+ }
3464
+ return normalized;
3465
+ }
3466
+ static extractImages(cfb) {
3467
+ const images = [];
3468
+ for (const stream of cfb.listStreams()) {
3469
+ const buffer = cfb.getStream(stream.name);
3470
+ if (!buffer || buffer.length < 16) {
3471
+ continue;
3472
+ }
3473
+ const extracted = this.extractImageBuffer(buffer);
3474
+ if (!extracted) {
3475
+ continue;
3476
+ }
3477
+ images.push({
3478
+ src: `data:${extracted.mimeType};base64,${extracted.buffer.toString('base64')}`,
3479
+ alt: this.normalizeImageLabel(stream.name, images.length + 1)
3480
+ });
3481
+ if (images.length >= 8) {
3482
+ break;
3483
+ }
3484
+ }
3485
+ return images;
3486
+ }
3487
+ static extractEmbeddedWorkbookTables(cfb, objectPlacementMode) {
3488
+ if (!XLSX)
3489
+ return [];
3490
+ const workbook = cfb.getStream('Workbook');
3491
+ if (!workbook) {
3492
+ return [];
3493
+ }
3494
+ try {
3495
+ const parsed = XLSX.read(workbook, { type: 'buffer' });
3496
+ return parsed.SheetNames
3497
+ .map((sheetName) => {
3498
+ const sheet = parsed.Sheets[sheetName];
3499
+ const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, blankrows: false });
3500
+ const normalizedRows = rows
3501
+ .map((row) => row.map((cell) => this.normalizeParagraphText(String(cell ?? ''))))
3502
+ .filter((row) => row.some((cell) => cell.length > 0));
3503
+ if (normalizedRows.length < 2 || normalizedRows[0].length < 2) {
3504
+ return null;
3505
+ }
3506
+ return {
3507
+ title: `Embedded ${sheetName}`,
3508
+ rows: normalizedRows,
3509
+ showTable: !/^chart/i.test(sheetName),
3510
+ chart: this.buildEmbeddedChart(normalizedRows),
3511
+ objectPlacementMode
3512
+ };
3513
+ })
3514
+ .filter(Boolean)
3515
+ .slice(0, 3);
3516
+ }
3517
+ catch (error) {
3518
+ console.warn('[DOC] Failed to parse embedded workbook:', error);
3519
+ return [];
3520
+ }
3521
+ }
3522
+ static async extractEmbeddedPackageCharts(cfb, objectPlacementMode) {
3523
+ if (!JSZip)
3524
+ return [];
3525
+ const packageStream = cfb.getStream('package_stream');
3526
+ if (!packageStream) {
3527
+ return [];
3528
+ }
3529
+ try {
3530
+ const zip = await JSZip.loadAsync(packageStream);
3531
+ const mimeType = await zip.file('mimetype')?.async('string');
3532
+ if (!mimeType || !/opendocument\.chart/i.test(mimeType)) {
3533
+ return [];
3534
+ }
3535
+ const contentXml = await zip.file('content.xml')?.async('string');
3536
+ if (!contentXml) {
3537
+ return [];
3538
+ }
3539
+ const parsed = this.parseOdfChartContent(contentXml, objectPlacementMode);
3540
+ return parsed ? [parsed] : [];
3541
+ }
3542
+ catch (error) {
3543
+ console.warn('[DOC] Failed to parse embedded chart package:', error);
3544
+ return [];
3545
+ }
3546
+ }
3547
+ static parseOdfChartContent(contentXml, objectPlacementMode) {
3548
+ const rowMatches = Array.from(contentXml.matchAll(/<table:table-row[\s\S]*?<\/table:table-row>/g));
3549
+ if (rowMatches.length < 2) {
3550
+ return undefined;
3551
+ }
3552
+ const rows = rowMatches
3553
+ .map((match) => this.parseOdfTableRow(match[0]))
3554
+ .filter((row) => row.some((cell) => cell.length > 0));
3555
+ if (rows.length < 2 || rows[0].length < 2) {
3556
+ return undefined;
3557
+ }
3558
+ const firstSeries = contentXml.match(/<chart:series[^>]*chart:class="chart:([^"]+)"/i)?.[1];
3559
+ const chart = this.buildEmbeddedChart(rows);
3560
+ return {
3561
+ rows,
3562
+ showTable: false,
3563
+ objectPlacementMode,
3564
+ chart: chart
3565
+ ? {
3566
+ ...chart,
3567
+ type: firstSeries === 'line' ? 'line' : chart.type
3568
+ }
3569
+ : undefined
3570
+ };
3571
+ }
3572
+ static parseOdfTableRow(rowXml) {
3573
+ const cellMatches = Array.from(rowXml.matchAll(/<table:table-cell\b[\s\S]*?<\/table:table-cell>/g));
3574
+ return cellMatches.map((cellMatch) => {
3575
+ const cellXml = cellMatch[0];
3576
+ const paragraphMatches = Array.from(cellXml.matchAll(/<text:p(?:\b[^>]*)?>([\s\S]*?)<\/text:p>|<text:p\b[^>]*\/>/g));
3577
+ if (paragraphMatches.length === 0) {
3578
+ return '';
3579
+ }
3580
+ return paragraphMatches
3581
+ .map((paragraph) => this.decodeXmlEntities(this.normalizeParagraphText(paragraph[1] ?? '')))
3582
+ .join(' ')
3583
+ .trim();
3584
+ });
3585
+ }
3586
+ static buildEmbeddedChart(rows) {
3587
+ if (rows.length < 2 || rows[0].length < 3) {
3588
+ return undefined;
3589
+ }
3590
+ const header = rows[0];
3591
+ const seriesNames = header.slice(1).map((name, index) => name || `Series ${index + 1}`);
3592
+ const categories = rows.slice(1).map((row) => row[0]).filter(Boolean);
3593
+ if (categories.length === 0) {
3594
+ return undefined;
3595
+ }
3596
+ const palette = ['#004586', '#ff420e', '#ffd320', '#579d1c', '#7e57c2'];
3597
+ const series = seriesNames.map((name, seriesIndex) => {
3598
+ const values = rows.slice(1).map((row) => {
3599
+ const numeric = Number(row[seriesIndex + 1]);
3600
+ return Number.isFinite(numeric) ? numeric : 0;
3601
+ });
3602
+ return {
3603
+ name,
3604
+ values,
3605
+ color: palette[seriesIndex % palette.length]
3606
+ };
3607
+ });
3608
+ if (series.every((entry) => entry.values.every((value) => value === 0))) {
3609
+ return undefined;
3610
+ }
3611
+ return {
3612
+ type: this.inferEmbeddedChartType(categories, series),
3613
+ categories,
3614
+ series
3615
+ };
3616
+ }
3617
+ static inferEmbeddedChartType(categories, series) {
3618
+ const numericLikeCategories = categories.every((value) => /^-?\d+(\.\d+)?$/.test(value));
3619
+ if (numericLikeCategories || (categories.length >= 6 && series.length <= 2)) {
3620
+ return 'line';
3621
+ }
3622
+ return 'bar';
3623
+ }
3624
+ static renderEmbeddedChart(chart) {
3625
+ const width = 760;
3626
+ const height = 320;
3627
+ const margin = { top: 28, right: 160, bottom: 54, left: 46 };
3628
+ const plotWidth = width - margin.left - margin.right;
3629
+ const plotHeight = height - margin.top - margin.bottom;
3630
+ const maxValue = Math.max(1, ...chart.series.flatMap((series) => series.values));
3631
+ const groupWidth = plotWidth / Math.max(1, chart.categories.length);
3632
+ const barWidth = Math.max(12, (groupWidth * 0.78) / Math.max(1, chart.series.length));
3633
+ const groupOffset = (groupWidth - barWidth * chart.series.length) / 2;
3634
+ const ticks = 5;
3635
+ const parts = [];
3636
+ for (let i = 0; i <= ticks; i++) {
3637
+ const value = (maxValue / ticks) * i;
3638
+ const y = margin.top + plotHeight - (value / maxValue) * plotHeight;
3639
+ parts.push(`<line x1="${margin.left}" y1="${y}" x2="${margin.left + plotWidth}" y2="${y}" stroke="#d6dbe1" stroke-width="1" />`);
3640
+ parts.push(`<text x="${margin.left - 8}" y="${y + 4}" text-anchor="end" font-size="12" fill="#6b7280">${value.toFixed(0)}</text>`);
3641
+ }
3642
+ chart.categories.forEach((category, index) => {
3643
+ const x = margin.left + index * groupWidth + groupWidth / 2;
3644
+ parts.push(`<text x="${x}" y="${margin.top + plotHeight + 28}" text-anchor="middle" font-size="12" fill="#111827">${this.escapeHtml(category)}</text>`);
3645
+ });
3646
+ if (chart.type === 'line') {
3647
+ chart.series.forEach((series) => {
3648
+ const points = series.values.map((value, valueIndex) => {
3649
+ const x = margin.left + valueIndex * groupWidth + groupWidth / 2;
3650
+ const y = margin.top + plotHeight - (Math.max(0, value) / maxValue) * plotHeight;
3651
+ return `${x},${y}`;
3652
+ }).join(' ');
3653
+ parts.push(`<polyline points="${points}" fill="none" stroke="${series.color}" stroke-width="3" stroke-linejoin="round" stroke-linecap="round" />`);
3654
+ series.values.forEach((value, valueIndex) => {
3655
+ const x = margin.left + valueIndex * groupWidth + groupWidth / 2;
3656
+ const y = margin.top + plotHeight - (Math.max(0, value) / maxValue) * plotHeight;
3657
+ parts.push(`<circle cx="${x}" cy="${y}" r="4" fill="${series.color}" />`);
3658
+ });
3659
+ });
3660
+ }
3661
+ else {
3662
+ chart.series.forEach((series, seriesIndex) => {
3663
+ series.values.forEach((value, valueIndex) => {
3664
+ const x = margin.left + valueIndex * groupWidth + groupOffset + seriesIndex * barWidth;
3665
+ const barHeight = (Math.max(0, value) / maxValue) * plotHeight;
3666
+ const y = margin.top + plotHeight - barHeight;
3667
+ parts.push(`<rect x="${x}" y="${y}" width="${barWidth - 2}" height="${barHeight}" fill="${series.color}" rx="1" />`);
3668
+ });
3669
+ });
3670
+ }
3671
+ const legend = chart.series
3672
+ .map((series) => `<div class="ov-doc-embedded-chart-legend-item"><span class="ov-doc-embedded-chart-swatch" style="background:${series.color}"></span><span>${this.escapeHtml(series.name)}</span></div>`)
3673
+ .join('');
3674
+ return `
3675
+ <div class="ov-doc-embedded-chart">
3676
+ <div class="ov-doc-embedded-chart-frame">
3677
+ <svg class="ov-doc-embedded-chart-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMinYMin meet">
3678
+ ${parts.join('')}
3679
+ </svg>
3680
+ </div>
3681
+ <div class="ov-doc-embedded-chart-legend">${legend}</div>
3682
+ </div>
3683
+ `;
3684
+ }
3685
+ static extractImageBuffer(buffer) {
3686
+ const signatures = [
3687
+ {
3688
+ marker: Buffer.from([0xff, 0xd8, 0xff]),
3689
+ mimeType: 'image/jpeg',
3690
+ terminator: Buffer.from([0xff, 0xd9])
3691
+ },
3692
+ {
3693
+ marker: Buffer.from([0x89, 0x50, 0x4e, 0x47]),
3694
+ mimeType: 'image/png',
3695
+ terminator: Buffer.from([0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82])
3696
+ },
3697
+ {
3698
+ marker: Buffer.from('GIF87a', 'ascii'),
3699
+ mimeType: 'image/gif'
3700
+ },
3701
+ {
3702
+ marker: Buffer.from('GIF89a', 'ascii'),
3703
+ mimeType: 'image/gif'
3704
+ },
3705
+ {
3706
+ marker: Buffer.from([0x42, 0x4d]),
3707
+ mimeType: 'image/bmp'
3708
+ }
3709
+ ];
3710
+ for (const signature of signatures) {
3711
+ const start = buffer.indexOf(signature.marker);
3712
+ if (start < 0) {
3713
+ continue;
3714
+ }
3715
+ if ('terminator' in signature && signature.terminator) {
3716
+ const end = buffer.indexOf(signature.terminator, start + signature.marker.length);
3717
+ if (end > start) {
3718
+ return {
3719
+ buffer: buffer.subarray(start, end + signature.terminator.length),
3720
+ mimeType: signature.mimeType
3721
+ };
3722
+ }
3723
+ }
3724
+ return {
3725
+ buffer: buffer.subarray(start),
3726
+ mimeType: signature.mimeType
3727
+ };
3728
+ }
3729
+ return null;
3730
+ }
3731
+ static escapeHtml(value) {
3732
+ return value
3733
+ .replace(/&/g, '&amp;')
3734
+ .replace(/</g, '&lt;')
3735
+ .replace(/>/g, '&gt;')
3736
+ .replace(/"/g, '&quot;')
3737
+ .replace(/'/g, '&#39;');
3738
+ }
3739
+ static decodeXmlEntities(value) {
3740
+ return value
3741
+ .replace(/&lt;/g, '<')
3742
+ .replace(/&gt;/g, '>')
3743
+ .replace(/&quot;/g, '"')
3744
+ .replace(/&apos;/g, '\'')
3745
+ .replace(/&amp;/g, '&');
3746
+ }
3747
+ }