docvortex 0.2.1__py3-none-any.whl

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 (364) hide show
  1. docvortex/__init__.py +21 -0
  2. docvortex/analyzers/__init__.py +3 -0
  3. docvortex/analyzers/native/__init__.py +37 -0
  4. docvortex/analyzers/native/_shared/__init__.py +3 -0
  5. docvortex/analyzers/native/_shared/hyperlink.py +16 -0
  6. docvortex/analyzers/native/_shared/image.py +8 -0
  7. docvortex/analyzers/native/_shared/markup/__init__.py +47 -0
  8. docvortex/analyzers/native/_shared/markup/anchors.py +30 -0
  9. docvortex/analyzers/native/_shared/markup/formula.py +37 -0
  10. docvortex/analyzers/native/_shared/markup/projector.py +57 -0
  11. docvortex/analyzers/native/_shared/markup/styles.py +19 -0
  12. docvortex/analyzers/native/_shared/mathml.py +17 -0
  13. docvortex/analyzers/native/_shared/names.py +7 -0
  14. docvortex/analyzers/native/_shared/xycut.py +414 -0
  15. docvortex/analyzers/native/contracts.py +44 -0
  16. docvortex/analyzers/native/csv.py +351 -0
  17. docvortex/analyzers/native/epub/__init__.py +18 -0
  18. docvortex/analyzers/native/epub/constants.py +45 -0
  19. docvortex/analyzers/native/epub/converter.py +80 -0
  20. docvortex/analyzers/native/epub/errors.py +20 -0
  21. docvortex/analyzers/native/epub/metadata.py +31 -0
  22. docvortex/analyzers/native/epub/package.py +649 -0
  23. docvortex/analyzers/native/epub/xhtml.py +306 -0
  24. docvortex/analyzers/native/html/__init__.py +6 -0
  25. docvortex/analyzers/native/html/anchors.py +271 -0
  26. docvortex/analyzers/native/html/constants.py +25 -0
  27. docvortex/analyzers/native/html/contracts.py +7 -0
  28. docvortex/analyzers/native/html/converter.py +117 -0
  29. docvortex/analyzers/native/html/document.py +389 -0
  30. docvortex/analyzers/native/html/errors.py +12 -0
  31. docvortex/analyzers/native/html/resources.py +365 -0
  32. docvortex/analyzers/native/html/selector.py +421 -0
  33. docvortex/analyzers/native/models.py +209 -0
  34. docvortex/analyzers/native/ofd/__init__.py +14 -0
  35. docvortex/analyzers/native/ofd/constants.py +59 -0
  36. docvortex/analyzers/native/ofd/converter.py +31 -0
  37. docvortex/analyzers/native/ofd/errors.py +16 -0
  38. docvortex/analyzers/native/ofd/geometry.py +215 -0
  39. docvortex/analyzers/native/ofd/images.py +124 -0
  40. docvortex/analyzers/native/ofd/metadata.py +53 -0
  41. docvortex/analyzers/native/ofd/models.py +170 -0
  42. docvortex/analyzers/native/ofd/package.py +346 -0
  43. docvortex/analyzers/native/ofd/path.py +201 -0
  44. docvortex/analyzers/native/ofd/reading_order.py +293 -0
  45. docvortex/analyzers/native/ofd/resources.py +122 -0
  46. docvortex/analyzers/native/ofd/scene.py +347 -0
  47. docvortex/analyzers/native/ofd/table.py +240 -0
  48. docvortex/analyzers/native/ofd/text.py +521 -0
  49. docvortex/analyzers/native/office/__init__.py +3 -0
  50. docvortex/analyzers/native/office/doc/__init__.py +3 -0
  51. docvortex/analyzers/native/office/doc/bookmarks.py +81 -0
  52. docvortex/analyzers/native/office/doc/doc_converter.py +573 -0
  53. docvortex/analyzers/native/office/doc/fib.py +261 -0
  54. docvortex/analyzers/native/office/doc/fields.py +101 -0
  55. docvortex/analyzers/native/office/doc/formatting.py +171 -0
  56. docvortex/analyzers/native/office/doc/images.py +162 -0
  57. docvortex/analyzers/native/office/doc/lists.py +356 -0
  58. docvortex/analyzers/native/office/doc/models.py +164 -0
  59. docvortex/analyzers/native/office/doc/parser.py +851 -0
  60. docvortex/analyzers/native/office/doc/pieces.py +256 -0
  61. docvortex/analyzers/native/office/doc/records.py +40 -0
  62. docvortex/analyzers/native/office/doc/sprm.py +269 -0
  63. docvortex/analyzers/native/office/doc/styles.py +215 -0
  64. docvortex/analyzers/native/office/docx/__init__.py +3 -0
  65. docvortex/analyzers/native/office/docx/context.py +71 -0
  66. docvortex/analyzers/native/office/docx/docx_converter.py +614 -0
  67. docvortex/analyzers/native/office/docx/equationxml.py +119 -0
  68. docvortex/analyzers/native/office/docx/fields.py +816 -0
  69. docvortex/analyzers/native/office/docx/formatting_types.py +23 -0
  70. docvortex/analyzers/native/office/docx/main.py +50 -0
  71. docvortex/analyzers/native/office/docx/numbering.py +491 -0
  72. docvortex/analyzers/native/office/docx/office_xml.py +57 -0
  73. docvortex/analyzers/native/office/docx/package_normalizer.py +248 -0
  74. docvortex/analyzers/native/office/docx/resources.py +526 -0
  75. docvortex/analyzers/native/office/docx/styles.py +693 -0
  76. docvortex/analyzers/native/office/docx/tables.py +515 -0
  77. docvortex/analyzers/native/office/equation/__init__.py +3 -0
  78. docvortex/analyzers/native/office/equation/image.py +470 -0
  79. docvortex/analyzers/native/office/equation/latex_dict.py +324 -0
  80. docvortex/analyzers/native/office/equation/mtef.py +885 -0
  81. docvortex/analyzers/native/office/equation/mtef_v5.py +941 -0
  82. docvortex/analyzers/native/office/equation/omml.py +561 -0
  83. docvortex/analyzers/native/office/equation/ooxml.py +62 -0
  84. docvortex/analyzers/native/office/errors.py +33 -0
  85. docvortex/analyzers/native/office/image.py +307 -0
  86. docvortex/analyzers/native/office/legacy/__init__.py +3 -0
  87. docvortex/analyzers/native/office/legacy/binary.py +43 -0
  88. docvortex/analyzers/native/office/legacy/officeart.py +362 -0
  89. docvortex/analyzers/native/office/legacy/ole.py +110 -0
  90. docvortex/analyzers/native/office/limits.py +12 -0
  91. docvortex/analyzers/native/office/odf/__init__.py +3 -0
  92. docvortex/analyzers/native/office/odf/chart.py +76 -0
  93. docvortex/analyzers/native/office/odf/constants.py +70 -0
  94. docvortex/analyzers/native/office/odf/converters.py +403 -0
  95. docvortex/analyzers/native/office/odf/errors.py +18 -0
  96. docvortex/analyzers/native/office/odf/metadata.py +91 -0
  97. docvortex/analyzers/native/office/odf/models.py +176 -0
  98. docvortex/analyzers/native/office/odf/package.py +270 -0
  99. docvortex/analyzers/native/office/odf/styles.py +329 -0
  100. docvortex/analyzers/native/office/odf/table.py +469 -0
  101. docvortex/analyzers/native/office/odf/text.py +1002 -0
  102. docvortex/analyzers/native/office/ooxml_chart.py +1016 -0
  103. docvortex/analyzers/native/office/opc.py +38 -0
  104. docvortex/analyzers/native/office/ppt/__init__.py +3 -0
  105. docvortex/analyzers/native/office/ppt/models.py +118 -0
  106. docvortex/analyzers/native/office/ppt/parser.py +1895 -0
  107. docvortex/analyzers/native/office/ppt/ppt_converter.py +292 -0
  108. docvortex/analyzers/native/office/ppt/records.py +131 -0
  109. docvortex/analyzers/native/office/ppt/style_text.py +247 -0
  110. docvortex/analyzers/native/office/pptx/__init__.py +3 -0
  111. docvortex/analyzers/native/office/pptx/context.py +113 -0
  112. docvortex/analyzers/native/office/pptx/lists.py +558 -0
  113. docvortex/analyzers/native/office/pptx/main.py +20 -0
  114. docvortex/analyzers/native/office/pptx/package_normalizer.py +321 -0
  115. docvortex/analyzers/native/office/pptx/pptx_converter.py +323 -0
  116. docvortex/analyzers/native/office/pptx/resources.py +329 -0
  117. docvortex/analyzers/native/office/pptx/shapes.py +393 -0
  118. docvortex/analyzers/native/office/pptx/text_styles.py +625 -0
  119. docvortex/analyzers/native/office/pptx/titles.py +178 -0
  120. docvortex/analyzers/native/office/rich_text.py +420 -0
  121. docvortex/analyzers/native/office/rtf/__init__.py +3 -0
  122. docvortex/analyzers/native/office/rtf/converter.py +708 -0
  123. docvortex/analyzers/native/office/rtf/lexer.py +213 -0
  124. docvortex/analyzers/native/office/rtf/math.py +339 -0
  125. docvortex/analyzers/native/office/rtf/models.py +185 -0
  126. docvortex/analyzers/native/office/rtf/parser.py +1553 -0
  127. docvortex/analyzers/native/office/spreadsheet/__init__.py +3 -0
  128. docvortex/analyzers/native/office/spreadsheet/html.py +81 -0
  129. docvortex/analyzers/native/office/spreadsheet/models.py +79 -0
  130. docvortex/analyzers/native/office/spreadsheet/projector.py +928 -0
  131. docvortex/analyzers/native/office/streams.py +18 -0
  132. docvortex/analyzers/native/office/xls/__init__.py +3 -0
  133. docvortex/analyzers/native/office/xls/chart.py +132 -0
  134. docvortex/analyzers/native/office/xls/embedded_chart.py +299 -0
  135. docvortex/analyzers/native/office/xls/models.py +109 -0
  136. docvortex/analyzers/native/office/xls/number_format.py +521 -0
  137. docvortex/analyzers/native/office/xls/parser.py +1145 -0
  138. docvortex/analyzers/native/office/xls/records.py +201 -0
  139. docvortex/analyzers/native/office/xls/strings.py +205 -0
  140. docvortex/analyzers/native/office/xls/xls_converter.py +354 -0
  141. docvortex/analyzers/native/office/xlsx/__init__.py +3 -0
  142. docvortex/analyzers/native/office/xlsx/main.py +20 -0
  143. docvortex/analyzers/native/office/xlsx/ooxml_ole.py +522 -0
  144. docvortex/analyzers/native/office/xlsx/package_normalizer.py +310 -0
  145. docvortex/analyzers/native/office/xlsx/xlsx_converter.py +716 -0
  146. docvortex/analyzers/native/pdf/__init__.py +3 -0
  147. docvortex/analyzers/native/pdf/auxiliary_text.py +1671 -0
  148. docvortex/analyzers/native/pdf/char_geometry.py +1662 -0
  149. docvortex/analyzers/native/pdf/code_blocks.py +535 -0
  150. docvortex/analyzers/native/pdf/formulas.py +1985 -0
  151. docvortex/analyzers/native/pdf/geometry.py +281 -0
  152. docvortex/analyzers/native/pdf/graphics.py +1501 -0
  153. docvortex/analyzers/native/pdf/index_blocks.py +268 -0
  154. docvortex/analyzers/native/pdf/inline/__init__.py +3 -0
  155. docvortex/analyzers/native/pdf/inline/common.py +112 -0
  156. docvortex/analyzers/native/pdf/inline/detection.py +590 -0
  157. docvortex/analyzers/native/pdf/inline/matching.py +1185 -0
  158. docvortex/analyzers/native/pdf/inline/materialize.py +457 -0
  159. docvortex/analyzers/native/pdf/inline/scripts.py +975 -0
  160. docvortex/analyzers/native/pdf/inline/types.py +385 -0
  161. docvortex/analyzers/native/pdf/line_layout.py +1106 -0
  162. docvortex/analyzers/native/pdf/line_merging.py +1223 -0
  163. docvortex/analyzers/native/pdf/models.py +246 -0
  164. docvortex/analyzers/native/pdf/native_text.py +1004 -0
  165. docvortex/analyzers/native/pdf/pipeline.py +1390 -0
  166. docvortex/analyzers/native/pdf/script_geometry.py +636 -0
  167. docvortex/analyzers/native/pdf/shared.py +30 -0
  168. docvortex/analyzers/native/pdf/spatial_text.py +383 -0
  169. docvortex/analyzers/native/pdf/table_annotations.py +446 -0
  170. docvortex/analyzers/native/pdf/table_constants.py +48 -0
  171. docvortex/analyzers/native/pdf/table_detection.py +227 -0
  172. docvortex/analyzers/native/pdf/table_filled_grid.py +218 -0
  173. docvortex/analyzers/native/pdf/table_geometry.py +40 -0
  174. docvortex/analyzers/native/pdf/table_materialization.py +444 -0
  175. docvortex/analyzers/native/pdf/table_recovery/__init__.py +15 -0
  176. docvortex/analyzers/native/pdf/table_recovery/candidate.py +356 -0
  177. docvortex/analyzers/native/pdf/table_recovery/contracts.py +154 -0
  178. docvortex/analyzers/native/pdf/table_recovery/engine.py +609 -0
  179. docvortex/analyzers/native/pdf/table_recovery/geometry.py +164 -0
  180. docvortex/analyzers/native/pdf/table_recovery/sparse_common.py +85 -0
  181. docvortex/analyzers/native/pdf/table_recovery/sparse_hybrid.py +804 -0
  182. docvortex/analyzers/native/pdf/table_recovery/sparse_multiline.py +1122 -0
  183. docvortex/analyzers/native/pdf/table_recovery/text.py +414 -0
  184. docvortex/analyzers/native/pdf/table_recovery/text_grid.py +618 -0
  185. docvortex/analyzers/native/pdf/table_recovery/vector.py +1931 -0
  186. docvortex/analyzers/native/pdf/table_rows.py +34 -0
  187. docvortex/analyzers/native/pdf/table_rules.py +1129 -0
  188. docvortex/analyzers/native/pdf/table_text_styles.py +283 -0
  189. docvortex/analyzers/native/pdf/tables.py +147 -0
  190. docvortex/analyzers/native/pdf/text_assembly/__init__.py +3 -0
  191. docvortex/analyzers/native/pdf/text_assembly/annotations.py +581 -0
  192. docvortex/analyzers/native/pdf/text_assembly/assembly.py +292 -0
  193. docvortex/analyzers/native/pdf/text_assembly/common.py +477 -0
  194. docvortex/analyzers/native/pdf/text_assembly/footnotes.py +394 -0
  195. docvortex/analyzers/native/pdf/text_assembly/merging.py +1274 -0
  196. docvortex/analyzers/native/pdf/text_assembly/rows.py +692 -0
  197. docvortex/analyzers/native/pdf/text_blocks.py +82 -0
  198. docvortex/analyzers/native/pdf/text_styles.py +55 -0
  199. docvortex/analyzers/native/pdf/title_analysis/__init__.py +3 -0
  200. docvortex/analyzers/native/pdf/title_analysis/body_profile.py +215 -0
  201. docvortex/analyzers/native/pdf/title_analysis/common.py +117 -0
  202. docvortex/analyzers/native/pdf/title_analysis/document_profile.py +164 -0
  203. docvortex/analyzers/native/pdf/title_analysis/lane_titles.py +758 -0
  204. docvortex/analyzers/native/pdf/title_analysis/page_titles.py +1024 -0
  205. docvortex/analyzers/native/pdf/title_analysis/prototype.py +194 -0
  206. docvortex/analyzers/native/pdf/title_analysis/structural.py +1081 -0
  207. docvortex/analyzers/native/pdf/titles.py +75 -0
  208. docvortex/analyzers/native/pdf/typography.py +19 -0
  209. docvortex/analyzers/native/pdf/visual_annotations.py +1262 -0
  210. docvortex/api.py +180 -0
  211. docvortex/assets/__init__.py +5 -0
  212. docvortex/assets/store.py +51 -0
  213. docvortex/cli.py +54 -0
  214. docvortex/codecs/__init__.py +3 -0
  215. docvortex/codecs/html/__init__.py +22 -0
  216. docvortex/codecs/html/contracts.py +236 -0
  217. docvortex/codecs/html/materializer.py +331 -0
  218. docvortex/codecs/html/parser.py +763 -0
  219. docvortex/codecs/html/resources.py +34 -0
  220. docvortex/codecs/json.py +17 -0
  221. docvortex/content/__init__.py +5 -0
  222. docvortex/content/inline.py +248 -0
  223. docvortex/content/markup/__init__.py +44 -0
  224. docvortex/content/markup/anchors.py +188 -0
  225. docvortex/content/markup/formula.py +280 -0
  226. docvortex/content/markup/projector.py +1237 -0
  227. docvortex/content/markup/styles.py +327 -0
  228. docvortex/content/mathml.py +167 -0
  229. docvortex/content/normalization.py +188 -0
  230. docvortex/content/spans.py +183 -0
  231. docvortex/content/table/__init__.py +18 -0
  232. docvortex/content/table/blocks.py +152 -0
  233. docvortex/content/table/content.py +425 -0
  234. docvortex/content/table/document.py +104 -0
  235. docvortex/content/table/html.py +399 -0
  236. docvortex/content/table/models.py +76 -0
  237. docvortex/content/table/rules.py +42 -0
  238. docvortex/content/table/structure.py +221 -0
  239. docvortex/content/tree.py +5 -0
  240. docvortex/document/__init__.py +3 -0
  241. docvortex/document/contracts.py +22 -0
  242. docvortex/document/detection.py +389 -0
  243. docvortex/document/filetypes.py +175 -0
  244. docvortex/document/page_range.py +167 -0
  245. docvortex/document/pdf/__init__.py +19 -0
  246. docvortex/document/pdf/classify.py +1138 -0
  247. docvortex/document/pdf/constants.py +132 -0
  248. docvortex/document/pdf/diagnostics.py +350 -0
  249. docvortex/document/pdf/document.py +582 -0
  250. docvortex/document/pdf/font_runtime.py +335 -0
  251. docvortex/document/pdf/geometry.py +31 -0
  252. docvortex/document/pdf/images.py +654 -0
  253. docvortex/document/pdf/native_annotations.py +367 -0
  254. docvortex/document/pdf/native_contracts.py +169 -0
  255. docvortex/document/pdf/native_coordinates.py +216 -0
  256. docvortex/document/pdf/native_lifecycle.py +16 -0
  257. docvortex/document/pdf/native_objects.py +902 -0
  258. docvortex/document/pdf/native_text_geometry.py +315 -0
  259. docvortex/document/pdf/pdfium.py +325 -0
  260. docvortex/document/pdf/raster.py +46 -0
  261. docvortex/document/pdf/text/__init__.py +62 -0
  262. docvortex/document/pdf/text/contracts.py +211 -0
  263. docvortex/document/pdf/text/extract.py +165 -0
  264. docvortex/document/pdf/text/geometry.py +16 -0
  265. docvortex/document/pdf/text/groups.py +162 -0
  266. docvortex/document/pdf/visual_geometry.py +201 -0
  267. docvortex/document/pdf/visuals.py +343 -0
  268. docvortex/document/source.py +92 -0
  269. docvortex/errors.py +21 -0
  270. docvortex/export/__init__.py +3 -0
  271. docvortex/export/bundle.py +98 -0
  272. docvortex/export/files.py +66 -0
  273. docvortex/export/middle.py +208 -0
  274. docvortex/foundation/__init__.py +3 -0
  275. docvortex/foundation/geometry.py +125 -0
  276. docvortex/foundation/hyperlink.py +65 -0
  277. docvortex/foundation/image.py +48 -0
  278. docvortex/foundation/image_encoding.py +30 -0
  279. docvortex/foundation/image_payload.py +280 -0
  280. docvortex/foundation/language.py +92 -0
  281. docvortex/foundation/platform.py +38 -0
  282. docvortex/foundation/text.py +153 -0
  283. docvortex/foundation/type_identity.py +20 -0
  284. docvortex/foundation/xml_names.py +20 -0
  285. docvortex/options.py +30 -0
  286. docvortex/postprocess/__init__.py +3 -0
  287. docvortex/postprocess/content.py +53 -0
  288. docvortex/postprocess/document.py +19 -0
  289. docvortex/postprocess/lists.py +236 -0
  290. docvortex/postprocess/page_blocks.py +214 -0
  291. docvortex/postprocess/pages.py +95 -0
  292. docvortex/postprocess/paragraphs.py +580 -0
  293. docvortex/postprocess/visual.py +715 -0
  294. docvortex/render/__init__.py +48 -0
  295. docvortex/render/_internal/__init__.py +3 -0
  296. docvortex/render/_internal/common/__init__.py +3 -0
  297. docvortex/render/_internal/common/context.py +43 -0
  298. docvortex/render/_internal/common/html_table.py +178 -0
  299. docvortex/render/_internal/common/index.py +33 -0
  300. docvortex/render/_internal/common/list_items.py +158 -0
  301. docvortex/render/_internal/common/planner.py +140 -0
  302. docvortex/render/_internal/docx/__init__.py +3 -0
  303. docvortex/render/_internal/docx/assets.py +202 -0
  304. docvortex/render/_internal/docx/inline.py +434 -0
  305. docvortex/render/_internal/docx/math.py +220 -0
  306. docvortex/render/_internal/docx/renderer.py +905 -0
  307. docvortex/render/_internal/docx/styles.py +195 -0
  308. docvortex/render/_internal/docx/table.py +442 -0
  309. docvortex/render/_internal/epub/__init__.py +5 -0
  310. docvortex/render/_internal/epub/assets.py +173 -0
  311. docvortex/render/_internal/epub/package.py +249 -0
  312. docvortex/render/_internal/epub/renderer.py +1156 -0
  313. docvortex/render/_internal/html/__init__.py +3 -0
  314. docvortex/render/_internal/html/inline.py +346 -0
  315. docvortex/render/_internal/html/renderer.py +1041 -0
  316. docvortex/render/_internal/html/sanitizer.py +478 -0
  317. docvortex/render/_internal/html/table.py +121 -0
  318. docvortex/render/_internal/latex/__init__.py +1 -0
  319. docvortex/render/_internal/latex/assets.py +85 -0
  320. docvortex/render/_internal/latex/inline.py +145 -0
  321. docvortex/render/_internal/latex/renderer.py +506 -0
  322. docvortex/render/_internal/latex/table.py +347 -0
  323. docvortex/render/_internal/markdown/__init__.py +3 -0
  324. docvortex/render/_internal/markdown/assets.py +78 -0
  325. docvortex/render/_internal/markdown/blocks.py +635 -0
  326. docvortex/render/_internal/markdown/escaping.py +51 -0
  327. docvortex/render/_internal/markdown/inline.py +260 -0
  328. docvortex/render/_internal/markdown/renderer.py +93 -0
  329. docvortex/render/_internal/markdown/table.py +281 -0
  330. docvortex/render/_internal/pdf/__init__.py +3 -0
  331. docvortex/render/_internal/pdf/assets.py +197 -0
  332. docvortex/render/_internal/pdf/formula.py +417 -0
  333. docvortex/render/_internal/pdf/inline.py +343 -0
  334. docvortex/render/_internal/pdf/renderer.py +734 -0
  335. docvortex/render/_internal/pdf/styles.py +206 -0
  336. docvortex/render/_internal/pdf/table.py +272 -0
  337. docvortex/render/_internal/structured_content/__init__.py +3 -0
  338. docvortex/render/_internal/structured_content/renderer.py +193 -0
  339. docvortex/render/api.py +199 -0
  340. docvortex/render/contracts.py +205 -0
  341. docvortex/render/docx.py +38 -0
  342. docvortex/render/epub.py +35 -0
  343. docvortex/render/fragments.py +70 -0
  344. docvortex/render/html.py +29 -0
  345. docvortex/render/latex.py +24 -0
  346. docvortex/render/markdown.py +50 -0
  347. docvortex/render/pdf.py +25 -0
  348. docvortex/render/structured_content.py +23 -0
  349. docvortex/resources/epub/docvortex.css +91 -0
  350. docvortex/resources/fasttext-langdetect/lid.176.ftz +0 -0
  351. docvortex/resources/fonts/DroidSansFallbackFull.ttf +0 -0
  352. docvortex/resources/fonts/NOTICE +190 -0
  353. docvortex/resources/fonts/manifest.json +9 -0
  354. docvortex/resources/html/docvortex.css +601 -0
  355. docvortex/resources/html/docvortex.min.css +1 -0
  356. docvortex/result.py +91 -0
  357. docvortex/schema.py +1141 -0
  358. docvortex/version.py +3 -0
  359. docvortex-0.2.1.dist-info/METADATA +193 -0
  360. docvortex-0.2.1.dist-info/RECORD +364 -0
  361. docvortex-0.2.1.dist-info/WHEEL +5 -0
  362. docvortex-0.2.1.dist-info/entry_points.txt +2 -0
  363. docvortex-0.2.1.dist-info/licenses/LICENSE.md +21 -0
  364. docvortex-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,403 @@
1
+ """ODT、ODS、ODP 到 DocVortex raw model-list 的原生 converter。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Any, BinaryIO, Iterator
8
+
9
+ from lxml import etree # type: ignore[reportMissingImports]
10
+
11
+ from .....schema import BlockType
12
+ from .....content.spans import inline_span_plain_text, text_spans
13
+ from .constants import OdfSuffix, qname
14
+ from .models import InlineNote
15
+ from .package import OdfPackage
16
+ from .styles import OdfStyles
17
+ from .table import OdfTableExpansionBudget, parse_table_grid, split_table_regions, table_grid_to_html
18
+ from .text import (
19
+ OdfBlockParser,
20
+ OdfMasterPageChange,
21
+ OdfTextExpansionBudget,
22
+ collect_emittable_anchor_targets,
23
+ flatten_block_text,
24
+ )
25
+
26
+
27
+ _LENGTH_RE = re.compile(r"^\s*(?P<value>[+-]?(?:\d+(?:\.\d*)?|\.\d+))(?P<unit>cm|mm|in|pt|pc|px)?\s*$")
28
+ _LENGTH_TO_PT = {"": 1.0, "pt": 1.0, "pc": 12.0, "in": 72.0, "cm": 72.0 / 2.54, "mm": 72.0 / 25.4, "px": 0.75}
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class _OdfContext:
33
+ """保存 converter 一次调用内共享的包、内容树、样式和正文。"""
34
+
35
+ package: OdfPackage
36
+ content_root: etree._Element
37
+ styles: OdfStyles
38
+ body: etree._Element
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class _PositionedBlocks:
43
+ """保存幻灯片对象的阅读顺序坐标、XML 序号和 raw blocks。"""
44
+
45
+ y: float
46
+ x: float
47
+ order: int
48
+ title: bool
49
+ blocks: list[dict[str, Any]]
50
+
51
+
52
+ def _open_context(file_binary: BinaryIO, suffix: OdfSuffix) -> _OdfContext:
53
+ """读取调用方流并建立已验证 ODF 包、样式和正文上下文。"""
54
+ package = OdfPackage(file_binary.read())
55
+ try:
56
+ content_root = package.validate_document(suffix)
57
+ styles_root = package.xml_part("styles.xml")
58
+ styles = OdfStyles(styles_root, content_root)
59
+ return _OdfContext(
60
+ package=package,
61
+ content_root=content_root,
62
+ styles=styles,
63
+ body=package.body_element(content_root, suffix),
64
+ )
65
+ except Exception:
66
+ package.close()
67
+ raise
68
+
69
+
70
+ def _new_page(
71
+ pages: list[list[dict[str, Any]]],
72
+ page_masters: list[str | None],
73
+ master_name: str | None,
74
+ ) -> None:
75
+ """追加一个新逻辑页及其 master-page 归属。"""
76
+ pages.append([])
77
+ page_masters.append(master_name)
78
+
79
+
80
+ def _flush_notes(parser: OdfBlockParser, page: list[dict[str, Any]]) -> None:
81
+ """把解析器累计脚注追加为当前页面的 PAGE_FOOTNOTE blocks。"""
82
+ for note in parser.drain_notes():
83
+ page.append({"type": BlockType.PAGE_FOOTNOTE, "content": text_spans(note)})
84
+
85
+
86
+ def _append_flow_items(
87
+ items: list[dict[str, Any] | InlineNote],
88
+ *,
89
+ parser: OdfBlockParser,
90
+ page: list[dict[str, Any]],
91
+ ) -> None:
92
+ """把段落结果和脚注按统一流语义追加到当前章节页。"""
93
+ for item in items:
94
+ if isinstance(item, InlineNote):
95
+ parser.notes.append(item.content)
96
+ else:
97
+ page.append(item)
98
+
99
+
100
+ def _master_auxiliary_blocks(
101
+ master_page: etree._Element | None,
102
+ *,
103
+ package: OdfPackage,
104
+ styles: OdfStyles,
105
+ anchor_targets: frozenset[str],
106
+ text_expansion_budget: OdfTextExpansionBudget,
107
+ table_expansion_budget: OdfTableExpansionBudget,
108
+ ) -> list[dict[str, Any]]:
109
+ """从 master-page 的 header/footer 中提取页面辅助文本。"""
110
+ if master_page is None:
111
+ return []
112
+ parser = OdfBlockParser(
113
+ package,
114
+ styles,
115
+ anchor_targets=anchor_targets,
116
+ text_expansion_budget=text_expansion_budget,
117
+ table_expansion_budget=table_expansion_budget,
118
+ )
119
+ result: list[dict[str, Any]] = []
120
+ for tag_name, block_type in (("header", BlockType.HEADER), ("footer", BlockType.FOOTER)):
121
+ element = master_page.find(qname("style", tag_name))
122
+ if element is None:
123
+ element = master_page.find(qname("style", f"{tag_name}-left"))
124
+ if element is None:
125
+ continue
126
+ for block in parser.parse_container(element):
127
+ content = block.get("content")
128
+ if isinstance(content, list) and inline_span_plain_text(span for span in content if isinstance(span, dict)).strip():
129
+ result.append({"type": block_type, "content": content})
130
+ return result
131
+
132
+
133
+ def _parse_odt_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
134
+ """仅按 master-page 章节变化递归构造 ODT 逻辑页。"""
135
+ anchor_targets = collect_emittable_anchor_targets(context.content_root, context.styles)
136
+ text_expansion_budget = OdfTextExpansionBudget()
137
+ parser = OdfBlockParser(
138
+ context.package,
139
+ context.styles,
140
+ anchor_targets=anchor_targets,
141
+ text_expansion_budget=text_expansion_budget,
142
+ )
143
+ pages: list[list[dict[str, Any]]] = [[]]
144
+ page_masters: list[str | None] = [None]
145
+ current_master: str | None = None
146
+
147
+ def apply_master_page(requested_master: str | None) -> None:
148
+ """按段落或列表事件切换 ODT 虚拟页及其 master-page。"""
149
+ nonlocal current_master
150
+ master_changed = requested_master is not None and current_master is not None and requested_master != current_master
151
+ if master_changed and pages[-1]:
152
+ _flush_notes(parser, pages[-1])
153
+ _new_page(pages, page_masters, requested_master)
154
+ if requested_master is not None:
155
+ current_master = requested_master
156
+ page_masters[-1] = current_master
157
+
158
+ def walk(parent: etree._Element) -> None:
159
+ """递归遍历 ODT block 容器并维护当前页与 master-page。"""
160
+ for child in parent:
161
+ if not isinstance(child.tag, str):
162
+ continue
163
+ if child.tag in {qname("text", "p"), qname("text", "h")}:
164
+ requested_master = context.styles.paragraph_master_page_name(child.get(qname("text", "style-name")))
165
+ apply_master_page(requested_master)
166
+ _append_flow_items(
167
+ parser.parse_paragraph(child),
168
+ parser=parser,
169
+ page=pages[-1],
170
+ )
171
+ elif child.tag in {
172
+ qname("text", "section"),
173
+ qname("text", "index-body"),
174
+ qname("text", "index-title"),
175
+ }:
176
+ walk(child)
177
+ elif child.tag == qname("text", "list"):
178
+ for item in parser.parse_list_blocks(child, emit_master_page_changes=True):
179
+ if isinstance(item, OdfMasterPageChange):
180
+ apply_master_page(item.master_page_name)
181
+ elif isinstance(item, InlineNote):
182
+ parser.notes.append(item.content)
183
+ else:
184
+ pages[-1].append(item)
185
+ else:
186
+ pages[-1].extend(parser.parse_element(child))
187
+
188
+ walk(context.body)
189
+ _flush_notes(parser, pages[-1])
190
+ while len(pages) > 1 and not pages[-1]:
191
+ pages.pop()
192
+ page_masters.pop()
193
+ for page, master_name in zip(pages, page_masters, strict=True):
194
+ page.extend(
195
+ _master_auxiliary_blocks(
196
+ context.styles.master_page(master_name),
197
+ package=context.package,
198
+ styles=context.styles,
199
+ anchor_targets=anchor_targets,
200
+ text_expansion_budget=text_expansion_budget,
201
+ table_expansion_budget=parser.table_expansion_budget,
202
+ )
203
+ )
204
+ return pages or [[]]
205
+
206
+
207
+ def _length_to_points(value: str | None) -> float:
208
+ """把 ODF SVG 长度转换为用于阅读顺序比较的 point。"""
209
+ match = _LENGTH_RE.match(value or "")
210
+ if match is None:
211
+ return 0.0
212
+ return float(match.group("value")) * _LENGTH_TO_PT.get(match.group("unit") or "", 1.0)
213
+
214
+
215
+ def _iter_slide_shapes(
216
+ parent: etree._Element,
217
+ *,
218
+ x_offset: float = 0.0,
219
+ y_offset: float = 0.0,
220
+ ) -> Iterator[tuple[etree._Element, float, float]]:
221
+ """递归展开幻灯片 group,并产出可见 shape 及近似绝对坐标。"""
222
+ for child in parent:
223
+ if not isinstance(child.tag, str) or child.tag == qname("presentation", "notes"):
224
+ continue
225
+ if child.tag == qname("draw", "g"):
226
+ group_x = x_offset + _length_to_points(child.get(qname("svg", "x")))
227
+ group_y = y_offset + _length_to_points(child.get(qname("svg", "y")))
228
+ yield from _iter_slide_shapes(child, x_offset=group_x, y_offset=group_y)
229
+ continue
230
+ if child.tag in {
231
+ qname("draw", "frame"),
232
+ qname("draw", "custom-shape"),
233
+ qname("draw", "rect"),
234
+ qname("draw", "ellipse"),
235
+ qname("draw", "caption"),
236
+ }:
237
+ yield (
238
+ child,
239
+ x_offset + _length_to_points(child.get(qname("svg", "x"))),
240
+ y_offset + _length_to_points(child.get(qname("svg", "y"))),
241
+ )
242
+
243
+
244
+ def _shape_blocks(shape: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
245
+ """把 frame 或带文本 custom-shape 转为页面 raw blocks。"""
246
+ if shape.tag == qname("draw", "frame"):
247
+ return parser.parse_frame_blocks(shape)
248
+ return parser.parse_container(shape)
249
+
250
+
251
+ def _notes_blocks(page: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
252
+ """提取 ODP speaker notes,并聚合为页面脚注。"""
253
+ notes = page.find(qname("presentation", "notes"))
254
+ if notes is None:
255
+ return []
256
+ blocks: list[dict[str, Any]] = []
257
+ for frame in notes.iter(qname("draw", "frame")):
258
+ blocks.extend(parser.parse_frame_blocks(frame))
259
+ visible = flatten_block_text(blocks)
260
+ return [{"type": BlockType.PAGE_FOOTNOTE, "content": text_spans(visible)}] if visible else []
261
+
262
+
263
+ def _parse_odp_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
264
+ """保持一页一 slide,并按坐标和 XML 顺序构造 ODP model-list。"""
265
+ parser = OdfBlockParser(context.package, context.styles)
266
+ pages: list[list[dict[str, Any]]] = []
267
+ document_title_emitted = False
268
+ for page in context.body:
269
+ if page.tag != qname("draw", "page"):
270
+ continue
271
+ if not context.styles.drawing_page_is_visible(page):
272
+ continue
273
+ positioned: list[_PositionedBlocks] = []
274
+ for order, (shape, x, y) in enumerate(_iter_slide_shapes(page)):
275
+ presentation_class = shape.get(qname("presentation", "class"), "")
276
+ if presentation_class in {"page-number", "date-time", "footer", "header"}:
277
+ continue
278
+ blocks = _shape_blocks(shape, parser)
279
+ if not blocks:
280
+ continue
281
+ positioned.append(
282
+ _PositionedBlocks(
283
+ y=y,
284
+ x=x,
285
+ order=order,
286
+ title=presentation_class in {"title", "subtitle"},
287
+ blocks=blocks,
288
+ )
289
+ )
290
+ title_entries = sorted((item for item in positioned if item.title), key=lambda item: (item.y, item.x, item.order))
291
+ body_entries = sorted((item for item in positioned if not item.title), key=lambda item: (item.y, item.x, item.order))
292
+ output: list[dict[str, Any]] = []
293
+ for entry in title_entries:
294
+ visible = flatten_block_text(entry.blocks)
295
+ if visible:
296
+ title_type = BlockType.DOC_TITLE if not document_title_emitted else BlockType.PARAGRAPH_TITLE
297
+ output.append(
298
+ {
299
+ "type": title_type,
300
+ "level": 1 if title_type == BlockType.DOC_TITLE else 2,
301
+ "content": text_spans(visible.replace("\n", " ")),
302
+ }
303
+ )
304
+ document_title_emitted = True
305
+ output.extend(
306
+ block for block in entry.blocks if block.get("type") in {BlockType.IMAGE, BlockType.TABLE, BlockType.CHART}
307
+ )
308
+ for entry in body_entries:
309
+ output.extend(entry.blocks)
310
+ _flush_notes(parser, output)
311
+ output.extend(_notes_blocks(page, parser))
312
+ _flush_notes(parser, output)
313
+ pages.append(output)
314
+ return pages or [[]]
315
+
316
+
317
+ def _sheet_blocks(sheet: etree._Element, parser: OdfBlockParser) -> list[dict[str, Any]]:
318
+ """把一个可见 ODS sheet 拆为数据区域和锚定视觉对象。"""
319
+ grid = parse_table_grid(sheet, parser.render_cell_html, expansion_budget=parser.table_expansion_budget)
320
+ blocks: list[dict[str, Any]] = []
321
+ for region in split_table_regions(grid):
322
+ content = table_grid_to_html(region)
323
+ if content:
324
+ blocks.append({"type": BlockType.TABLE, "content": content})
325
+ blocks.extend(parser.drain_cell_visuals())
326
+ for shapes in sheet.iter(qname("table", "shapes")):
327
+ for frame in shapes.iter(qname("draw", "frame")):
328
+ for block in parser.parse_frame_blocks(frame):
329
+ if block.get("type") in {BlockType.IMAGE, BlockType.CHART, BlockType.EQUATION}:
330
+ blocks.append(block)
331
+ _flush_notes(parser, blocks)
332
+ return blocks
333
+
334
+
335
+ def _parse_ods_pages(context: _OdfContext) -> list[list[dict[str, Any]]]:
336
+ """保持一页一可见 sheet,并在多表时添加工作表标题。"""
337
+ parser = OdfBlockParser(context.package, context.styles, collect_cell_visuals=True)
338
+ sheet_pages: list[tuple[str, list[dict[str, Any]]]] = []
339
+ for sheet in context.body:
340
+ if sheet.tag != qname("table", "table"):
341
+ continue
342
+ if sheet.get(qname("table", "display"), "true").casefold() == "false":
343
+ continue
344
+ if not context.styles.table_is_visible(sheet.get(qname("table", "style-name"))):
345
+ continue
346
+ name = sheet.get(qname("table", "name"), "Sheet")
347
+ sheet_pages.append((name, _sheet_blocks(sheet, parser)))
348
+ if sum(bool(blocks) for _, blocks in sheet_pages) > 1:
349
+ for name, blocks in sheet_pages:
350
+ if blocks:
351
+ blocks.insert(0, {"type": BlockType.PARAGRAPH_TITLE, "level": 2, "content": text_spans(name)})
352
+ return [blocks for _, blocks in sheet_pages] or [[]]
353
+
354
+
355
+ class OdtConverter:
356
+ """把 OpenDocument Text 转换为 DocVortex 分页 raw blocks。"""
357
+
358
+ def __init__(self) -> None:
359
+ """初始化空分页结果,等待 convert 填充。"""
360
+ self.pages: list[list[dict[str, Any]]] = []
361
+
362
+ def convert(self, file_binary: BinaryIO) -> None:
363
+ """解析调用方持有的 ODT 流,并保持调用方流打开。"""
364
+ context = _open_context(file_binary, "odt")
365
+ try:
366
+ self.pages = _parse_odt_pages(context)
367
+ finally:
368
+ context.package.close()
369
+
370
+
371
+ class OdpConverter:
372
+ """把 OpenDocument Presentation 转换为逐幻灯片 raw blocks。"""
373
+
374
+ def __init__(self) -> None:
375
+ """初始化空幻灯片结果,等待 convert 填充。"""
376
+ self.pages: list[list[dict[str, Any]]] = []
377
+
378
+ def convert(self, file_binary: BinaryIO) -> None:
379
+ """解析调用方持有的 ODP 流,并保持调用方流打开。"""
380
+ context = _open_context(file_binary, "odp")
381
+ try:
382
+ self.pages = _parse_odp_pages(context)
383
+ finally:
384
+ context.package.close()
385
+
386
+
387
+ class OdsConverter:
388
+ """把 OpenDocument Spreadsheet 转换为逐可见工作表 raw blocks。"""
389
+
390
+ def __init__(self) -> None:
391
+ """初始化空工作表结果,等待 convert 填充。"""
392
+ self.pages: list[list[dict[str, Any]]] = []
393
+
394
+ def convert(self, file_binary: BinaryIO) -> None:
395
+ """解析调用方持有的 ODS 流,并保持调用方流打开。"""
396
+ context = _open_context(file_binary, "ods")
397
+ try:
398
+ self.pages = _parse_ods_pages(context)
399
+ finally:
400
+ context.package.close()
401
+
402
+
403
+ __all__ = ["OdpConverter", "OdsConverter", "OdtConverter"]
@@ -0,0 +1,18 @@
1
+ """OpenDocument 内部稳定错误类型。"""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class OdfParseError(ValueError):
7
+ """表示 OpenDocument 包或语义结构不可解析。"""
8
+
9
+
10
+ class OdfResourceLimitError(OdfParseError):
11
+ """表示 OpenDocument 输入超过固定安全边界。"""
12
+
13
+
14
+ class OdfEncryptedError(OdfParseError):
15
+ """表示 OpenDocument 包包含不支持的加密成员。"""
16
+
17
+
18
+ __all__ = ["OdfEncryptedError", "OdfParseError", "OdfResourceLimitError"]
@@ -0,0 +1,91 @@
1
+ """读取 OpenDocument meta.xml 与结构页数。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import BinaryIO, Final
6
+
7
+ from lxml import etree # type: ignore[reportMissingImports]
8
+
9
+ from .constants import OdfSuffix, qname
10
+ from .package import OdfPackage
11
+ from .styles import OdfStyles
12
+
13
+
14
+ MAX_ODT_METADATA_PAGE_COUNT: Final = 10_000
15
+
16
+
17
+ def _first_text(root: etree._Element | None, *tags: str) -> str | None:
18
+ """返回多个候选标签中首个非空文本。"""
19
+ if root is None:
20
+ return None
21
+ for tag in tags:
22
+ element = root.find(f".//{tag}")
23
+ if element is not None:
24
+ value = "".join(element.itertext()).strip()
25
+ if value:
26
+ return value
27
+ return None
28
+
29
+
30
+ def _odt_page_count(meta_root: etree._Element | None) -> int | None:
31
+ """读取 ODT 生产者记录的布局页数,缺失或非法时返回空。"""
32
+ if meta_root is None:
33
+ return None
34
+ statistic = meta_root.find(f".//{qname('meta', 'document-statistic')}")
35
+ if statistic is None:
36
+ return None
37
+ try:
38
+ value = int(statistic.get(qname("meta", "page-count"), ""))
39
+ except ValueError:
40
+ return None
41
+ return min(value, MAX_ODT_METADATA_PAGE_COUNT) if value >= 1 else None
42
+
43
+
44
+ def _visible_sheet_count(body: etree._Element, styles: OdfStyles) -> int:
45
+ """统计未被 table:display 或表格样式隐藏的 ODS 工作表。"""
46
+ count = 0
47
+ for sheet in body:
48
+ if sheet.tag != qname("table", "table"):
49
+ continue
50
+ if sheet.get(qname("table", "display"), "true").casefold() == "false":
51
+ continue
52
+ if styles.table_is_visible(sheet.get(qname("table", "style-name"))):
53
+ count += 1
54
+ return count
55
+
56
+
57
+ def extract_odf_metadata(file_binary: BinaryIO, suffix: OdfSuffix) -> dict[str, object | None]:
58
+ """提取 ODF 标题作者等元数据及稳定文档页数。"""
59
+ package = OdfPackage(file_binary.read())
60
+ try:
61
+ content_root = package.validate_document(suffix)
62
+ styles_root = package.xml_part("styles.xml")
63
+ styles = OdfStyles(styles_root, content_root)
64
+ body = package.body_element(content_root, suffix)
65
+ meta_root = package.xml_part("meta.xml")
66
+ keywords = []
67
+ if meta_root is not None:
68
+ for keyword in meta_root.iter(qname("meta", "keyword")):
69
+ value = "".join(keyword.itertext()).strip()
70
+ if value:
71
+ keywords.append(value)
72
+ if suffix == "odt":
73
+ page_count = _odt_page_count(meta_root)
74
+ elif suffix == "odp":
75
+ page_count = sum(
76
+ 1 for child in body if child.tag == qname("draw", "page") and styles.drawing_page_is_visible(child)
77
+ )
78
+ else:
79
+ page_count = _visible_sheet_count(body, styles)
80
+ return {
81
+ "page_count": page_count or 1,
82
+ "title": _first_text(meta_root, qname("dc", "title")),
83
+ "author": _first_text(meta_root, qname("dc", "creator"), qname("meta", "initial-creator")),
84
+ "subject": _first_text(meta_root, qname("dc", "subject")),
85
+ "keywords": ", ".join(keywords) or None,
86
+ }
87
+ finally:
88
+ package.close()
89
+
90
+
91
+ __all__ = ["extract_odf_metadata"]
@@ -0,0 +1,176 @@
1
+ """OpenDocument 内部行内、样式与表格模型。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, TypeAlias, Union
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class TextStyle:
11
+ """保存可继承的 ODF 行内样式最终值。"""
12
+
13
+ bold: bool = False
14
+ italic: bool = False
15
+ underline: bool = False
16
+ strikethrough: bool = False
17
+ superscript: bool = False
18
+ subscript: bool = False
19
+
20
+ def names(self) -> tuple[str, ...]:
21
+ """按 DocVortex 内联协议的稳定顺序返回已启用样式名。"""
22
+ result: list[str] = []
23
+ if self.bold:
24
+ result.append("bold")
25
+ if self.italic:
26
+ result.append("italic")
27
+ if self.underline:
28
+ result.append("underline")
29
+ if self.strikethrough:
30
+ result.append("strikethrough")
31
+ if self.superscript:
32
+ result.append("superscript")
33
+ if self.subscript:
34
+ result.append("subscript")
35
+ return tuple(result)
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class TextStyleDelta:
40
+ """保存 ODF 样式层级中可显式覆盖的三态字段。"""
41
+
42
+ bold: bool | None = None
43
+ italic: bool | None = None
44
+ underline: bool | None = None
45
+ strikethrough: bool | None = None
46
+ superscript: bool | None = None
47
+ subscript: bool | None = None
48
+
49
+ def merge(self, child: TextStyleDelta) -> TextStyleDelta:
50
+ """用子样式的非空字段覆盖当前样式。"""
51
+ return TextStyleDelta(
52
+ bold=self.bold if child.bold is None else child.bold,
53
+ italic=self.italic if child.italic is None else child.italic,
54
+ underline=self.underline if child.underline is None else child.underline,
55
+ strikethrough=self.strikethrough if child.strikethrough is None else child.strikethrough,
56
+ superscript=self.superscript if child.superscript is None else child.superscript,
57
+ subscript=self.subscript if child.subscript is None else child.subscript,
58
+ )
59
+
60
+ def resolve(self) -> TextStyle:
61
+ """把未声明字段按关闭处理并返回最终样式。"""
62
+ return TextStyle(
63
+ bold=bool(self.bold),
64
+ italic=bool(self.italic),
65
+ underline=bool(self.underline),
66
+ strikethrough=bool(self.strikethrough),
67
+ superscript=bool(self.superscript),
68
+ subscript=bool(self.subscript),
69
+ )
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class ListLevel:
74
+ """保存一个 ODF 列表层级的通用编号语义。"""
75
+
76
+ ordered: bool = False
77
+ start: int = 1
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class InlineText:
82
+ """保存带样式和可选超链接的行内文本。"""
83
+
84
+ text: str
85
+ style: TextStyle = TextStyle()
86
+ hyperlink: str | None = None
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class InlineMath:
91
+ """保存不含外围标记的行内 LaTeX。"""
92
+
93
+ latex: str
94
+
95
+
96
+ @dataclass(frozen=True, slots=True)
97
+ class InlineBreak:
98
+ """表示段内显式换行。"""
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class InlineNote:
103
+ """保存应随当前行内内容归属的 ODF note body。"""
104
+
105
+ content: str
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class InlineImage:
110
+ """保存表格单元格中允许内联呈现的图片 data URI。"""
111
+
112
+ data_uri: str
113
+ alt: str = ""
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class InlineBlockGroup:
118
+ """在行内流中保存段外 block 及其与内联图片的配对关系。"""
119
+
120
+ blocks: tuple[dict[str, Any], ...]
121
+ inline_image_rendered: bool = False
122
+
123
+
124
+ InlineAtom: TypeAlias = Union[
125
+ InlineText,
126
+ InlineMath,
127
+ InlineBreak,
128
+ InlineNote,
129
+ InlineImage,
130
+ InlineBlockGroup,
131
+ ]
132
+
133
+
134
+ @dataclass(slots=True)
135
+ class GridCell:
136
+ """保存 ODF 表格原点单元格的 HTML 与跨度。"""
137
+
138
+ html: str = ""
139
+ row_span: int = 1
140
+ col_span: int = 1
141
+ header: bool = False
142
+
143
+ @property
144
+ def has_content(self) -> bool:
145
+ """返回单元格是否包含可见或结构化 HTML。"""
146
+ return bool(self.html.strip())
147
+
148
+
149
+ @dataclass(slots=True)
150
+ class TableGrid:
151
+ """保存带合并占位的 ODF 二维表格。"""
152
+
153
+ rows: list[list[GridCell | None]] = field(default_factory=list)
154
+ header_rows: int = 0
155
+ covered: set[tuple[int, int]] = field(default_factory=set)
156
+
157
+ @property
158
+ def width(self) -> int:
159
+ """返回网格最大视觉列数。"""
160
+ return max((len(row) for row in self.rows), default=0)
161
+
162
+
163
+ __all__ = [
164
+ "GridCell",
165
+ "InlineAtom",
166
+ "InlineBlockGroup",
167
+ "InlineBreak",
168
+ "InlineImage",
169
+ "InlineMath",
170
+ "InlineNote",
171
+ "InlineText",
172
+ "ListLevel",
173
+ "TableGrid",
174
+ "TextStyle",
175
+ "TextStyleDelta",
176
+ ]