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,327 @@
1
+ """解析 XHTML/HTML 使用的有限语义 CSS 子集。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+
8
+ from lxml import etree # type: ignore[reportMissingImports]
9
+
10
+ from docvortex.foundation.xml_names import local_name
11
+ from docvortex.foundation.type_identity import preserve_type_module
12
+
13
+
14
+ _CSS_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)
15
+ _CSS_IMPORTANT_RE = re.compile(r"!\s*important\s*$", re.IGNORECASE)
16
+ _TEXT_STYLE_FIELDS = ("bold", "italic", "underline", "strikethrough", "superscript", "subscript")
17
+ _VISIBILITY_FIELDS = ("display", "visibility", "opacity")
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class TextStyle:
22
+ """保存可投影到 Middle JSON 行内协议的文字样式。"""
23
+
24
+ bold: bool = False
25
+ italic: bool = False
26
+ underline: bool = False
27
+ strikethrough: bool = False
28
+ superscript: bool = False
29
+ subscript: bool = False
30
+
31
+ def merge(self, other: TextStyle) -> TextStyle:
32
+ """合并继承样式和当前元素显式开启的样式。"""
33
+ return TextStyle(
34
+ bold=self.bold or other.bold,
35
+ italic=self.italic or other.italic,
36
+ underline=self.underline or other.underline,
37
+ strikethrough=self.strikethrough or other.strikethrough,
38
+ superscript=self.superscript or other.superscript,
39
+ subscript=self.subscript or other.subscript,
40
+ )
41
+
42
+ def names(self) -> tuple[str, ...]:
43
+ """按稳定顺序返回现有行内协议识别的样式名称。"""
44
+ return tuple(
45
+ name
46
+ for enabled, name in (
47
+ (self.bold, "bold"),
48
+ (self.italic, "italic"),
49
+ (self.underline, "underline"),
50
+ (self.strikethrough, "strikethrough"),
51
+ (self.superscript, "superscript"),
52
+ (self.subscript, "subscript"),
53
+ )
54
+ if enabled
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True, slots=True)
59
+ class TextStyleDelta:
60
+ """保存 CSS 对各文字样式的显式开启、关闭或未声明状态。"""
61
+
62
+ bold: bool | None = None
63
+ italic: bool | None = None
64
+ underline: bool | None = None
65
+ strikethrough: bool | None = None
66
+ superscript: bool | None = None
67
+ subscript: bool | None = None
68
+
69
+ def apply(self, base: TextStyle) -> TextStyle:
70
+ """把当前声明覆盖到已解析的继承/标签样式。"""
71
+ return TextStyle(
72
+ bold=base.bold if self.bold is None else self.bold,
73
+ italic=base.italic if self.italic is None else self.italic,
74
+ underline=base.underline if self.underline is None else self.underline,
75
+ strikethrough=base.strikethrough if self.strikethrough is None else self.strikethrough,
76
+ superscript=base.superscript if self.superscript is None else self.superscript,
77
+ subscript=base.subscript if self.subscript is None else self.subscript,
78
+ )
79
+
80
+ def is_empty(self) -> bool:
81
+ """返回当前声明是否没有触及任何受支持样式。"""
82
+ return all(
83
+ value is None
84
+ for value in (
85
+ self.bold,
86
+ self.italic,
87
+ self.underline,
88
+ self.strikethrough,
89
+ self.superscript,
90
+ self.subscript,
91
+ )
92
+ )
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class ElementStyle:
97
+ """保存元素最终文字样式、整树隐藏状态和继承可见性。"""
98
+
99
+ text: TextStyle
100
+ subtree_hidden: bool = False
101
+ visibility_hidden: bool = False
102
+
103
+ @property
104
+ def hidden(self) -> bool:
105
+ """返回当前元素是否因任一种受支持的隐藏语义而不可见。"""
106
+ return self.subtree_hidden or self.visibility_hidden
107
+
108
+
109
+ @dataclass(slots=True)
110
+ class _SelectorCascade:
111
+ """按 selector 聚合各属性最后一次声明及其源码顺序。"""
112
+
113
+ priority: int
114
+ declarations: dict[str, tuple[bool, int, bool]] = field(default_factory=dict)
115
+ visibility: dict[str, tuple[bool, int, bool]] = field(default_factory=dict)
116
+
117
+ def update(self, parsed: _ParsedDeclarations, order: int) -> None:
118
+ """按 importance 和源码顺序更新同 selector 的逐属性级联结果。"""
119
+ for name, (important, value) in parsed.text.items():
120
+ current = self.declarations.get(name)
121
+ if current is None or (important, order) >= current[:2]:
122
+ self.declarations[name] = (important, order, value)
123
+ for name, (important, value) in parsed.visibility.items():
124
+ current = self.visibility.get(name)
125
+ if current is None or (important, order) >= current[:2]:
126
+ self.visibility[name] = (important, order, value)
127
+
128
+
129
+ @dataclass(frozen=True, slots=True)
130
+ class _ParsedDeclarations:
131
+ """保存已投影 CSS 属性的 importance 与布尔值。"""
132
+
133
+ text: dict[str, tuple[bool, bool]]
134
+ visibility: dict[str, tuple[bool, bool]]
135
+
136
+
137
+ def _numeric_font_weight(value: str) -> int | None:
138
+ """在整数转换前解析 CSS Fonts 允许的一到一千字重。"""
139
+ if not value.isascii() or not value.isdigit() or len(value) > 4:
140
+ return None
141
+ weight = int(value)
142
+ return weight if 1 <= weight <= 1_000 else None
143
+
144
+
145
+ def _parse_declarations(value: str) -> _ParsedDeclarations:
146
+ """从声明串逐属性提取字体语义、隐藏状态和 important 优先级。"""
147
+ text: dict[str, tuple[bool, bool]] = {}
148
+ visibility: dict[str, tuple[bool, bool]] = {}
149
+ for raw_declaration in value.split(";"):
150
+ if ":" not in raw_declaration:
151
+ continue
152
+ name, raw_value = raw_declaration.split(":", 1)
153
+ name = name.strip().casefold()
154
+ important_match = _CSS_IMPORTANT_RE.search(raw_value)
155
+ important = important_match is not None
156
+ normalized = raw_value[: important_match.start() if important_match is not None else None].strip().casefold()
157
+ text_updates: dict[str, bool] = {}
158
+ visibility_update: tuple[str, bool] | None = None
159
+ if name == "font-weight":
160
+ if normalized in {"bold", "bolder"}:
161
+ text_updates["bold"] = True
162
+ elif normalized in {"normal", "lighter"}:
163
+ text_updates["bold"] = False
164
+ elif (weight := _numeric_font_weight(normalized)) is not None:
165
+ text_updates["bold"] = weight >= 600
166
+ elif name == "font-style":
167
+ text_updates["italic"] = normalized in {"italic", "oblique"}
168
+ elif name in {"text-decoration", "text-decoration-line"}:
169
+ if normalized == "none":
170
+ text_updates["underline"] = False
171
+ text_updates["strikethrough"] = False
172
+ else:
173
+ text_updates["underline"] = "underline" in normalized
174
+ text_updates["strikethrough"] = "line-through" in normalized
175
+ elif name == "vertical-align":
176
+ text_updates["superscript"] = normalized in {"super", "text-top"}
177
+ text_updates["subscript"] = normalized in {"sub", "text-bottom"}
178
+ elif name == "display":
179
+ visibility_update = ("display", normalized == "none")
180
+ elif name == "visibility":
181
+ if normalized in {"hidden", "collapse"}:
182
+ visibility_update = ("visibility", True)
183
+ elif normalized in {"visible", "initial"}:
184
+ visibility_update = ("visibility", False)
185
+ elif name == "opacity":
186
+ try:
187
+ opacity = float(normalized)
188
+ except ValueError:
189
+ pass
190
+ else:
191
+ visibility_update = ("opacity", opacity <= 0)
192
+ for field_name, field_value in text_updates.items():
193
+ current = text.get(field_name)
194
+ if current is None or important or not current[0]:
195
+ text[field_name] = (important, field_value)
196
+ if visibility_update is not None:
197
+ field_name, field_value = visibility_update
198
+ current = visibility.get(field_name)
199
+ if current is None or important or not current[0]:
200
+ visibility[field_name] = (important, field_value)
201
+ return _ParsedDeclarations(text=text, visibility=visibility)
202
+
203
+
204
+ class MarkupStylesheet:
205
+ """保存按文档顺序解析的简单 tag/class CSS 规则。"""
206
+
207
+ def __init__(self) -> None:
208
+ """初始化按 tag、class 与 tag.class 分桶的 selector 索引。"""
209
+ self._tag_cascades: dict[str, _SelectorCascade] = {}
210
+ self._class_cascades: dict[str, _SelectorCascade] = {}
211
+ self._tag_class_cascades: dict[tuple[str, str], _SelectorCascade] = {}
212
+ self._source_order = 0
213
+
214
+ def _selector_cascade(self, tag: str | None, class_name: str | None, priority: int) -> _SelectorCascade:
215
+ """返回指定简单 selector 的聚合级联槽。"""
216
+ if class_name is None:
217
+ assert tag is not None
218
+ return self._tag_cascades.setdefault(tag, _SelectorCascade(priority))
219
+ if tag is None:
220
+ return self._class_cascades.setdefault(class_name, _SelectorCascade(priority))
221
+ return self._tag_class_cascades.setdefault((tag, class_name), _SelectorCascade(priority))
222
+
223
+ def add(self, css: str) -> None:
224
+ """追加一个 stylesheet 中受支持的简单 selector 规则。"""
225
+ normalized_css = _CSS_COMMENT_RE.sub("", css)
226
+ for chunk in normalized_css.split("}"):
227
+ if "{" not in chunk:
228
+ continue
229
+ selectors, declarations = chunk.split("{", 1)
230
+ parsed_declarations = _parse_declarations(declarations)
231
+ if not parsed_declarations.text and not parsed_declarations.visibility:
232
+ continue
233
+ for selector in selectors.split(","):
234
+ parsed = self._parse_selector(selector)
235
+ if parsed is None:
236
+ continue
237
+ tag, class_name, priority = parsed
238
+ cascade = self._selector_cascade(tag, class_name, priority)
239
+ cascade.update(parsed_declarations, self._source_order)
240
+ self._source_order += 1
241
+
242
+ @staticmethod
243
+ def _parse_selector(selector: str) -> tuple[str | None, str | None, int] | None:
244
+ """只接受 tag、.class 和 tag.class,拒绝组合器及伪类。"""
245
+ normalized = selector.strip()
246
+ if not normalized or any(token in normalized for token in (" ", ">", "+", "~", ":", "[", "#")):
247
+ return None
248
+ if "." in normalized:
249
+ tag_text, class_name = normalized.split(".", 1)
250
+ if not class_name or "." in class_name:
251
+ return None
252
+ tag = tag_text.casefold() or None
253
+ return tag, class_name, 10 + (1 if tag else 0)
254
+ return normalized.casefold(), None, 1
255
+
256
+ def resolve(
257
+ self,
258
+ element: etree._Element,
259
+ inherited: TextStyle,
260
+ inherited_visibility_hidden: bool = False,
261
+ ) -> ElementStyle:
262
+ """计算元素的继承样式、标签默认样式、CSS 规则和 inline style。"""
263
+ tag = local_name(element)
264
+ classes = frozenset((element.get("class") or "").split())
265
+ tag_style = TextStyle(
266
+ bold=tag in {"b", "strong"},
267
+ italic=tag in {"cite", "dfn", "em", "i", "var"},
268
+ strikethrough=tag in {"del", "s", "strike"},
269
+ superscript=tag == "sup",
270
+ subscript=tag == "sub",
271
+ )
272
+ style = inherited.merge(tag_style)
273
+ subtree_hidden = element.get("hidden") is not None or (element.get("aria-hidden") or "").casefold() == "true"
274
+ matching: list[_SelectorCascade] = []
275
+ if cascade := self._tag_cascades.get(tag):
276
+ matching.append(cascade)
277
+ for class_name in classes:
278
+ if cascade := self._class_cascades.get(class_name):
279
+ matching.append(cascade)
280
+ if cascade := self._tag_class_cascades.get((tag, class_name)):
281
+ matching.append(cascade)
282
+
283
+ inline = _parse_declarations(element.get("style") or "")
284
+ resolved_values: dict[str, bool] = {}
285
+ for name in _TEXT_STYLE_FIELDS:
286
+ candidates = [
287
+ (important, cascade.priority, order, value)
288
+ for cascade in matching
289
+ if (declaration := cascade.declarations.get(name)) is not None
290
+ for important, order, value in (declaration,)
291
+ ]
292
+ if (inline_declaration := inline.text.get(name)) is not None:
293
+ important, value = inline_declaration
294
+ candidates.append((important, 1_000, self._source_order, value))
295
+ if candidates:
296
+ resolved_values[name] = max(candidates, key=lambda item: item[:3])[3]
297
+ style = TextStyleDelta(**resolved_values).apply(style)
298
+
299
+ resolved_visibility: dict[str, bool] = {}
300
+ for name in _VISIBILITY_FIELDS:
301
+ candidates = [
302
+ (important, cascade.priority, order, value)
303
+ for cascade in matching
304
+ if (declaration := cascade.visibility.get(name)) is not None
305
+ for important, order, value in (declaration,)
306
+ ]
307
+ if (inline_declaration := inline.visibility.get(name)) is not None:
308
+ important, value = inline_declaration
309
+ candidates.append((important, 1_000, self._source_order, value))
310
+ if candidates:
311
+ resolved_visibility[name] = max(candidates, key=lambda item: item[:3])[3]
312
+ subtree_hidden = (
313
+ subtree_hidden or resolved_visibility.get("display", False) or resolved_visibility.get("opacity", False)
314
+ )
315
+ visibility_hidden = resolved_visibility.get("visibility", inherited_visibility_hidden)
316
+ return ElementStyle(style, subtree_hidden, visibility_hidden)
317
+
318
+
319
+ __all__ = ["ElementStyle", "MarkupStylesheet", "TextStyle", "TextStyleDelta"]
320
+
321
+ # 保持既有公开类型的 pickle 路径,所有旧、新入口指向同一个类。
322
+ preserve_type_module(TextStyle, "docvortex.analyzers.native._shared.markup.styles")
323
+ preserve_type_module(TextStyleDelta, "docvortex.analyzers.native._shared.markup.styles")
324
+ preserve_type_module(ElementStyle, "docvortex.analyzers.native._shared.markup.styles")
325
+ preserve_type_module(_SelectorCascade, "docvortex.analyzers.native._shared.markup.styles")
326
+ preserve_type_module(_ParsedDeclarations, "docvortex.analyzers.native._shared.markup.styles")
327
+ preserve_type_module(MarkupStylesheet, "docvortex.analyzers.native._shared.markup.styles")
@@ -0,0 +1,167 @@
1
+ """把常用 Presentation MathML 结构转换为 LaTeX。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from lxml import etree # type: ignore[reportMissingImports]
8
+
9
+ from docvortex.foundation.xml_names import local_name
10
+
11
+
12
+ _OPERATOR_MAP = {
13
+ "−": "-",
14
+ "×": r"\times ",
15
+ "÷": r"\div ",
16
+ "·": r"\cdot ",
17
+ "±": r"\pm ",
18
+ "∓": r"\mp ",
19
+ "∞": r"\infty ",
20
+ "≠": r"\ne ",
21
+ "≤": r"\le ",
22
+ "≥": r"\ge ",
23
+ "≈": r"\approx ",
24
+ "≡": r"\equiv ",
25
+ "∈": r"\in ",
26
+ "∉": r"\notin ",
27
+ "⊂": r"\subset ",
28
+ "⊆": r"\subseteq ",
29
+ "∪": r"\cup ",
30
+ "∩": r"\cap ",
31
+ "∑": r"\sum ",
32
+ "∏": r"\prod ",
33
+ "∫": r"\int ",
34
+ "∂": r"\partial ",
35
+ "√": r"\sqrt{}",
36
+ "→": r"\to ",
37
+ "←": r"\leftarrow ",
38
+ "↔": r"\leftrightarrow ",
39
+ }
40
+ _GREEK_MAP = {
41
+ "α": r"\alpha ",
42
+ "β": r"\beta ",
43
+ "γ": r"\gamma ",
44
+ "δ": r"\delta ",
45
+ "ε": r"\epsilon ",
46
+ "θ": r"\theta ",
47
+ "λ": r"\lambda ",
48
+ "μ": r"\mu ",
49
+ "π": r"\pi ",
50
+ "σ": r"\sigma ",
51
+ "φ": r"\phi ",
52
+ "ω": r"\omega ",
53
+ "Γ": r"\Gamma ",
54
+ "Δ": r"\Delta ",
55
+ "Θ": r"\Theta ",
56
+ "Λ": r"\Lambda ",
57
+ "Π": r"\Pi ",
58
+ "Σ": r"\Sigma ",
59
+ "Φ": r"\Phi ",
60
+ "Ω": r"\Omega ",
61
+ }
62
+ _LATEX_ESCAPE_RE = re.compile(r"([#$%&_{}])")
63
+ _LATEX_MATH_TOKEN_ESCAPES = {
64
+ "\\": r"\backslash{}",
65
+ "#": r"\#",
66
+ "$": r"\$",
67
+ "%": r"\%",
68
+ "&": r"\&",
69
+ "_": r"\_",
70
+ "^": r"\^{}",
71
+ "{": r"\{",
72
+ "}": r"\}",
73
+ "~": r"\~{}",
74
+ }
75
+
76
+
77
+ def _escape_text(value: str) -> str:
78
+ """转义进入 LaTeX 文本命令的保留字符。"""
79
+ return _LATEX_ESCAPE_RE.sub(r"\\\1", value)
80
+
81
+
82
+ def _escape_math_token(value: str) -> str:
83
+ """转义 MathML 标识符中的 TeX 控制字符,避免字面文本改变公式结构。"""
84
+ return "".join(_LATEX_MATH_TOKEN_ESCAPES.get(char, char) for char in value)
85
+
86
+
87
+ def _children(element: etree._Element) -> list[etree._Element]:
88
+ """返回当前元素的全部普通 XML 子元素。"""
89
+ return [child for child in element if isinstance(child.tag, str)]
90
+
91
+
92
+ def _join_children(element: etree._Element) -> str:
93
+ """按文档顺序拼接所有子 MathML 节点。"""
94
+ return "".join(_convert(child) for child in _children(element))
95
+
96
+
97
+ def _convert(element: etree._Element) -> str:
98
+ """递归转换一个常用 MathML 节点,未知容器保留其可解析子项。"""
99
+ name = local_name(element)
100
+ children = _children(element)
101
+ text = (element.text or "").strip()
102
+ if name == "semantics":
103
+ if not children or local_name(children[0]) in {"annotation", "annotation-xml"}:
104
+ return ""
105
+ return _convert(children[0])
106
+ if name in {"math", "mrow", "mstyle", "mpadded", "mphantom"}:
107
+ return _join_children(element)
108
+ if name in {"mi", "mn"}:
109
+ return _GREEK_MAP.get(text, _escape_math_token(text))
110
+ if name == "mo":
111
+ return _OPERATOR_MAP.get(text, text)
112
+ if name == "mtext":
113
+ return rf"\text{{{_escape_text(text)}}}"
114
+ if name == "mspace":
115
+ return r"\,"
116
+ if name == "mfrac" and len(children) >= 2:
117
+ return rf"\frac{{{_convert(children[0])}}}{{{_convert(children[1])}}}"
118
+ if name == "msqrt":
119
+ return rf"\sqrt{{{_join_children(element)}}}"
120
+ if name == "mroot" and len(children) >= 2:
121
+ return rf"\sqrt[{_convert(children[1])}]{{{_convert(children[0])}}}"
122
+ if name == "msup" and len(children) >= 2:
123
+ return rf"{{{_convert(children[0])}}}^{{{_convert(children[1])}}}"
124
+ if name == "msub" and len(children) >= 2:
125
+ return rf"{{{_convert(children[0])}}}_{{{_convert(children[1])}}}"
126
+ if name == "msubsup" and len(children) >= 3:
127
+ return rf"{{{_convert(children[0])}}}_{{{_convert(children[1])}}}^{{{_convert(children[2])}}}"
128
+ if name == "mover" and len(children) >= 2:
129
+ return rf"\overset{{{_convert(children[1])}}}{{{_convert(children[0])}}}"
130
+ if name == "munder" and len(children) >= 2:
131
+ return rf"\underset{{{_convert(children[1])}}}{{{_convert(children[0])}}}"
132
+ if name == "munderover" and len(children) >= 3:
133
+ base = _convert(children[0])
134
+ return rf"\underset{{{_convert(children[1])}}}{{\overset{{{_convert(children[2])}}}{{{base}}}}}"
135
+ if name == "mfenced":
136
+ opening = element.get("open", "(")
137
+ closing = element.get("close", ")")
138
+ separators = element.get("separators", ",") or ","
139
+ values = [_convert(child) for child in children]
140
+ return rf"\left{opening}{separators[0].join(values)}\right{closing}"
141
+ if name == "mtable":
142
+ rows = [_convert(child) for child in children if local_name(child) in {"mtr", "mlabeledtr"}]
143
+ return r"\begin{matrix}" + r" \\ ".join(rows) + r"\end{matrix}"
144
+ if name in {"mtr", "mlabeledtr"}:
145
+ return " & ".join(_convert(child) for child in children)
146
+ if name == "mtd":
147
+ return _join_children(element)
148
+ if name == "annotation" and "tex" in (element.get("encoding", "").casefold()):
149
+ return text
150
+ return _join_children(element) or text
151
+
152
+
153
+ def mathml_to_latex(math_element: etree._Element) -> str | None:
154
+ """转换 MathML 根节点,并优先采用生产者保留的 TeX annotation。"""
155
+ for annotation in math_element.iter():
156
+ if not isinstance(annotation.tag, str):
157
+ continue
158
+ if local_name(annotation) != "annotation":
159
+ continue
160
+ encoding = (annotation.get("encoding") or "").casefold()
161
+ if "tex" in encoding and (annotation.text or "").strip():
162
+ return (annotation.text or "").strip()
163
+ latex = _convert(math_element).strip()
164
+ return latex or None
165
+
166
+
167
+ __all__ = ["mathml_to_latex"]
@@ -0,0 +1,188 @@
1
+ """PDF 模型输出的可见文字清洗;原始字符、布局证据及其它输入格式不在此处理。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Sequence
6
+ import re
7
+ from typing import Any
8
+
9
+ from ..foundation.text import full_to_half_exclude_marks
10
+ from ..schema import BlockType, RAW_CAPTION, RAW_FOOTNOTE, RAW_PHONETIC
11
+
12
+ _FULLWIDTH_MODEL_TEXT = re.compile("[A-Za-z0-9:./\-_%+=@#&*]")
13
+ _PDF_SYMBOL_TRANSLATION = str.maketrans(
14
+ {
15
+ ":": ":",
16
+ ".": ".",
17
+ "/": "/",
18
+ "\": "\\",
19
+ "-": "-",
20
+ "_": "_",
21
+ "%": "%",
22
+ "+": "+",
23
+ "=": "=",
24
+ "@": "@",
25
+ "#": "#",
26
+ "&": "&",
27
+ "*": "*",
28
+ }
29
+ )
30
+ _FORMULA_OPENING = re.compile(r"\\[\(\[]")
31
+ _NATURAL_LANGUAGE_TYPES = frozenset(
32
+ {
33
+ BlockType.TEXT,
34
+ BlockType.DOC_TITLE,
35
+ BlockType.PARAGRAPH_TITLE,
36
+ BlockType.ASIDE_TEXT,
37
+ BlockType.HEADER,
38
+ BlockType.FOOTER,
39
+ BlockType.PAGE_NUMBER,
40
+ BlockType.PAGE_FOOTNOTE,
41
+ BlockType.REF_TEXT,
42
+ BlockType.LIST,
43
+ BlockType.INDEX,
44
+ BlockType.IMAGE_CAPTION,
45
+ BlockType.IMAGE_FOOTNOTE,
46
+ BlockType.TABLE_CAPTION,
47
+ BlockType.TABLE_FOOTNOTE,
48
+ BlockType.CHART_CAPTION,
49
+ BlockType.CHART_FOOTNOTE,
50
+ BlockType.CODE_CAPTION,
51
+ BlockType.CODE_FOOTNOTE,
52
+ RAW_CAPTION,
53
+ RAW_FOOTNOTE,
54
+ RAW_PHONETIC,
55
+ }
56
+ )
57
+ _OPAQUE_HTML_TAGS = frozenset(
58
+ {"eq", "math", "pre", "code", "script", "style", "svg", "template", "textarea", "object", "embed", "canvas", "iframe"}
59
+ )
60
+
61
+
62
+ def _normalize_plain_text(content: str) -> str:
63
+ """按一对一码点映射转换英数和 PDF 符号白名单,不改变通用英数工具的契约。"""
64
+ return full_to_half_exclude_marks(content).translate(_PDF_SYMBOL_TRANSLATION)
65
+
66
+
67
+ def _normalize_text(content: str) -> str:
68
+ """转换公式范围之外的英数及白名单符号;未闭合公式保护到逻辑文字段末尾。"""
69
+ if not _FULLWIDTH_MODEL_TEXT.search(content):
70
+ return content
71
+ parts: list[str] = []
72
+ cursor = 0
73
+ while match := _FORMULA_OPENING.search(content, cursor):
74
+ parts.append(_normalize_plain_text(content[cursor : match.start()]))
75
+ closing = r"\)" if match.group() == r"\(" else r"\]"
76
+ end = content.find(closing, match.end())
77
+ if end < 0:
78
+ parts.append(content[match.start() :])
79
+ return "".join(parts)
80
+ cursor = end + len(closing)
81
+ parts.append(content[match.start() : cursor])
82
+ parts.append(_normalize_plain_text(content[cursor:]))
83
+ return "".join(parts)
84
+
85
+
86
+ def _normalize_parts(parts: Sequence[str]) -> list[str]:
87
+ """先识别跨节点公式,再按原长度分回节点,保持样式与链接的边界。"""
88
+ source = "".join(parts)
89
+ normalized = _normalize_text(source)
90
+ if source == normalized:
91
+ return list(parts)
92
+ # 英数及符号映射始终一对一;不要使用可能扩展字符的整体 Unicode 规范化。
93
+ result: list[str] = []
94
+ offset = 0
95
+ for part in parts:
96
+ result.append(normalized[offset : offset + len(part)])
97
+ offset += len(part)
98
+ return result
99
+
100
+
101
+ def _span_parts(spans: list[Any]) -> Iterator[tuple[dict[str, Any] | None, str]]:
102
+ """递归遍历可见 TextSpan;不读取 URL,并用不可见屏障隔离公式和代码载荷。"""
103
+ for span in spans:
104
+ if not isinstance(span, dict):
105
+ yield None, "\0"
106
+ continue
107
+ content = span.get("content")
108
+ if span.get("type") == "text" and isinstance(content, str):
109
+ yield span, content
110
+ elif span.get("type") == "hyperlink" and isinstance(content, list):
111
+ yield from _span_parts(content)
112
+ else:
113
+ yield None, "\0"
114
+
115
+
116
+ def _normalize_spans(spans: list[Any]) -> None:
117
+ """只更新文字叶子的内容,既不重建 Span,也不合并相邻等样式片段。"""
118
+ entries = list(_span_parts(spans))
119
+ normalized = _normalize_parts([text for _span, text in entries])
120
+ for (span, original), replacement in zip(entries, normalized):
121
+ if span is not None and replacement != original:
122
+ span["content"] = replacement
123
+
124
+
125
+ def _is_opaque_html_node(node: Any) -> bool:
126
+ """识别现有 HTML 公式、代码及非文本载体,保持它们的内容与属性原样。"""
127
+ name = str(node.name).split(":")[-1].lower()
128
+ return (
129
+ name in _OPAQUE_HTML_TAGS
130
+ or node.get("data-block-type") in {"equation", "code", "code_body", "algorithm", "algorithm_body"}
131
+ or node.has_attr("data-docvortex-latex")
132
+ or node.has_attr("data-formula-display")
133
+ or "docvortex-math" in (node.get("class") or [])
134
+ )
135
+
136
+
137
+ def _normalize_table(markup: str) -> str:
138
+ """只修改单元格的可见文本节点;没有实际变化时保留原 HTML 字节表示。"""
139
+ if not _FULLWIDTH_MODEL_TEXT.search(markup) and "&#" not in markup:
140
+ return markup
141
+ # 与现有表格处理保持同一 HTML 解析器,公开模块导入不触发 HTML 依赖。
142
+ from bs4 import BeautifulSoup, NavigableString, Tag
143
+
144
+ soup = BeautifulSoup(markup, "html.parser")
145
+ changed = False
146
+
147
+ def cell_parts(node: Any) -> Iterator[tuple[NavigableString | None, str]]:
148
+ """样式标签保持透明,嵌套表格由其自身单元格处理,换行和载荷不能拼出定界符。"""
149
+ if type(node) is NavigableString:
150
+ yield node, str(node)
151
+ elif isinstance(node, Tag):
152
+ if _is_opaque_html_node(node) or node.name in {"table", "br", "hr", "img"}:
153
+ yield None, "\0"
154
+ else:
155
+ for child in node.children:
156
+ yield from cell_parts(child)
157
+
158
+ for cell in soup.find_all(["td", "th"]):
159
+ if any(isinstance(parent, Tag) and _is_opaque_html_node(parent) for parent in (cell, *cell.parents)):
160
+ continue
161
+ entries = [part for child in cell.children for part in cell_parts(child)]
162
+ normalized = _normalize_parts([text for _node, text in entries])
163
+ for (node, original), replacement in zip(entries, normalized):
164
+ if node is not None and replacement != original:
165
+ node.replace_with(NavigableString(replacement))
166
+ changed = True
167
+ return str(soup) if changed else markup
168
+
169
+
170
+ def normalize_pdf_model_text(model_list: list[list[dict[str, Any]]]) -> None:
171
+ """统一 PDF 自然语言及表格可见英数与白名单符号,保留公式、代码、URL 和结构。
172
+
173
+ 应在样式、上下标和链接匹配结束后、ModelJson 构造前调用;函数幂等,不修改
174
+ 原始字符证据,也不会将字符串转换为 Span 或清理其它模型元数据。
175
+ """
176
+ for page in model_list:
177
+ for block in page:
178
+ kind, content = block.get("type"), block.get("content")
179
+ if kind in _NATURAL_LANGUAGE_TYPES:
180
+ if isinstance(content, str):
181
+ block["content"] = _normalize_text(content)
182
+ elif isinstance(content, list):
183
+ _normalize_spans(content)
184
+ elif kind in {BlockType.TABLE, BlockType.TABLE_BODY} and isinstance(content, str):
185
+ block["content"] = _normalize_table(content)
186
+
187
+
188
+ __all__ = ["normalize_pdf_model_text"]