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,145 @@
1
+ """严格 InlineSpan 到安全 LaTeX 行内源码的转换。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from hashlib import sha256
7
+ import re
8
+ from typing import Iterable
9
+
10
+ from ....content.inline import join_inline_spans
11
+ from ....schema import CodeInlineSpan, EquationInlineSpan, HyperlinkSpan, InlineSpan, InlineStyle, TextSpan
12
+
13
+ _INVALID_TEXT_CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
14
+ _TEXT_ESCAPES = {
15
+ "\\": r"\textbackslash{}",
16
+ "{": r"\{",
17
+ "}": r"\}",
18
+ "$": r"\$",
19
+ "&": r"\&",
20
+ "#": r"\#",
21
+ "_": r"\_",
22
+ "%": r"\%",
23
+ "~": r"\textasciitilde{}",
24
+ "^": r"\textasciicircum{}",
25
+ }
26
+ _URL_ESCAPES = {
27
+ "\\": "/",
28
+ "{": r"\{",
29
+ "}": r"\}",
30
+ "$": r"\$",
31
+ "&": r"\&",
32
+ "#": r"\#",
33
+ "_": r"\_",
34
+ "%": r"\%",
35
+ "~": r"\string~",
36
+ "^": r"\string^",
37
+ }
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class LatexAnchorRegistry:
42
+ """把原始 anchor 映射为安全稳定 target,并只发射首次定义。"""
43
+
44
+ source_targets: set[str]
45
+ emitted: set[str] = field(default_factory=set)
46
+
47
+ def target_for(self, anchor: str | None) -> str | None:
48
+ """仅为真实可见目标返回稳定的 TeX target 名。"""
49
+ normalized = (anchor or "").strip()
50
+ if not normalized or normalized not in self.source_targets:
51
+ return None
52
+ digest = sha256(normalized.encode("utf-8")).hexdigest()[:24]
53
+ return f"docvortex-{digest}"
54
+
55
+ def emit_target(self, anchor: str | None) -> str:
56
+ """首次出现时输出 hypertarget,重复 anchor 不再重复定义。"""
57
+ target = self.target_for(anchor)
58
+ if target is None or target in self.emitted:
59
+ return ""
60
+ self.emitted.add(target)
61
+ return rf"\hypertarget{{{target}}}{{}}"
62
+
63
+
64
+ def escape_latex_text(content: str, *, preserve_newlines: bool = True) -> str:
65
+ """转义普通文本中的全部 TeX 控制字符并保留 Unicode。"""
66
+ normalized = _INVALID_TEXT_CONTROL_RE.sub("\ufffd", content).replace("\r\n", "\n").replace("\r", "\n")
67
+ parts: list[str] = []
68
+ for character in normalized:
69
+ if character == "\n":
70
+ parts.append(r"\DocVortexLineBreak{}" if preserve_newlines else " ")
71
+ else:
72
+ parts.append(_TEXT_ESCAPES.get(character, character))
73
+ return "".join(parts)
74
+
75
+
76
+ def escape_latex_url(target: str) -> str:
77
+ """转义 hyperref URL 参数,同时保留 URL 的可解析结构。"""
78
+ normalized = _INVALID_TEXT_CONTROL_RE.sub("", target).replace("\r", "").replace("\n", "")
79
+ return "".join(_URL_ESCAPES.get(character, character) for character in normalized)
80
+
81
+
82
+ def render_inline_spans(spans: Iterable[InlineSpan], anchors: LatexAnchorRegistry) -> str:
83
+ """按 span discriminator 渲染完整行内语义。"""
84
+ parts: list[str] = []
85
+ for span in spans:
86
+ if isinstance(span, TextSpan):
87
+ rendered = escape_latex_text(span.content)
88
+ parts.append(_apply_text_styles(rendered, span.styles))
89
+ elif isinstance(span, EquationInlineSpan):
90
+ parts.append(rf"\({span.content}\)")
91
+ elif isinstance(span, CodeInlineSpan):
92
+ parts.append(rf"\texttt{{{_escape_inline_code(span.content)}}}")
93
+ elif isinstance(span, HyperlinkSpan):
94
+ label = render_inline_spans(span.content, anchors)
95
+ if span.url.startswith("#"):
96
+ target = anchors.target_for(span.url[1:])
97
+ parts.append(rf"\hyperlink{{{target}}}{{{label}}}" if target else label)
98
+ else:
99
+ parts.append(rf"\href{{{escape_latex_url(span.url)}}}{{{label}}}")
100
+ else:
101
+ raise TypeError(f"Unsupported InlineSpan type: {type(span).__name__}")
102
+ return "".join(parts)
103
+
104
+
105
+ def render_joined_inline_contents(
106
+ contents: Iterable[Iterable[InlineSpan]],
107
+ anchors: LatexAnchorRegistry,
108
+ ) -> str:
109
+ """按共享物理段落边界规则合并并渲染多段 InlineSpan。"""
110
+ return render_inline_spans(join_inline_spans(contents), anchors)
111
+
112
+
113
+ def _apply_text_styles(content: str, styles: list[InlineStyle]) -> str:
114
+ """按公开固定顺序嵌套 LaTeX 文字样式。"""
115
+ wrappers = {
116
+ "bold": r"\textbf{%s}",
117
+ "italic": r"\textit{%s}",
118
+ "underline": r"\uline{%s}",
119
+ "emphasis": r"\CJKunderdot[format=\normalcolor]{%s}",
120
+ "strikethrough": r"\sout{%s}",
121
+ "superscript": r"\textsuperscript{%s}",
122
+ "subscript": r"\textsubscript{%s}",
123
+ }
124
+ rendered = content
125
+ for style in reversed(styles):
126
+ wrapper = wrappers.get(style)
127
+ if wrapper is None:
128
+ raise TypeError(f"Unsupported inline style: {style}")
129
+ rendered = wrapper % rendered
130
+ return rendered
131
+
132
+
133
+ def _escape_inline_code(content: str) -> str:
134
+ """转义行内代码,并把连续空格与 Tab 转为等宽可见空格。"""
135
+ expanded = content.replace("\t", " ")
136
+ return escape_latex_text(expanded).replace(" ", r"\ ")
137
+
138
+
139
+ __all__ = [
140
+ "LatexAnchorRegistry",
141
+ "escape_latex_text",
142
+ "escape_latex_url",
143
+ "render_inline_spans",
144
+ "render_joined_inline_contents",
145
+ ]
@@ -0,0 +1,506 @@
1
+ """严格 MiddleJson 到 TeX Live XeLaTeX 完整文档的渲染实现。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from bs4 import BeautifulSoup
8
+ from loguru import logger
9
+
10
+ from ....content.inline import inline_plain_text
11
+ from ....schema import (
12
+ PAGE_AUXILIARY_BLOCK_TYPES,
13
+ RAW_ALGORITHM,
14
+ AlgorithmBodyBlock,
15
+ BlockType,
16
+ ChartAnnotationBlock,
17
+ ChartBlock,
18
+ ChartBodyBlock,
19
+ CodeAnnotationBlock,
20
+ CodeBlock,
21
+ CodeBodyBlock,
22
+ DocTitleBlock,
23
+ EquationBlock,
24
+ ImageAnnotationBlock,
25
+ ImageBlock,
26
+ ImageBodyBlock,
27
+ ImagePayloadBlock,
28
+ IndexBlock,
29
+ InlineSpan,
30
+ ListBlock,
31
+ MiddleJson,
32
+ PageFootnoteBlock,
33
+ ParagraphTitleBlock,
34
+ RefTextBlock,
35
+ TableAnnotationBlock,
36
+ TableBlock,
37
+ TableBodyBlock,
38
+ TextBlock,
39
+ TitleBlockBase,
40
+ )
41
+ from ..common.index import strip_index_page_tail
42
+ from ..common.list_items import ListItem, parse_list_item_marker, reference_list_needs_bullets
43
+ from ..common.planner import PlannedBlock, build_render_plan
44
+ from .assets import normalize_asset_base_path, remote_block_image_url, resolve_block_image_path, tex_image_path
45
+ from .inline import LatexAnchorRegistry, escape_latex_text, escape_latex_url, render_inline_spans, render_joined_inline_contents
46
+ from .table import LatexTableError, LatexTableRenderer
47
+
48
+ _HTML_TABLE_RE = re.compile(r"<\s*table\b", re.IGNORECASE)
49
+ _TOP_LEVEL_DISPLAY_ENV_RE = re.compile(
50
+ r"^\s*\\begin\{(?P<name>align\*?|equation\*?|gather\*?|multline\*?)\}",
51
+ re.DOTALL,
52
+ )
53
+ _PREAMBLE_LINES = (
54
+ "% !TeX program = xelatex",
55
+ "% Generated by DocVortex. Compile with: latexmk -xelatex <file>.tex",
56
+ r"\documentclass[UTF8,fontset=fandol,11pt]{ctexart}",
57
+ r"\usepackage[a4paper,margin=20mm]{geometry}",
58
+ r"\usepackage{amsmath,amssymb}",
59
+ r"\usepackage{graphicx}",
60
+ r"\usepackage{longtable}",
61
+ r"\usepackage{array}",
62
+ r"\usepackage{booktabs}",
63
+ r"\usepackage{multirow}",
64
+ r"\usepackage{enumitem}",
65
+ r"\usepackage[table]{xcolor}",
66
+ r"\usepackage[normalem]{ulem}",
67
+ r"\usepackage{xeCJKfntef}",
68
+ r"\usepackage{fvextra}",
69
+ r"\usepackage[unicode,hidelinks]{hyperref}",
70
+ r"\definecolor{DocVortexGray}{HTML}{6B7280}",
71
+ r"\definecolor{DocVortexSurface}{HTML}{F3F4F6}",
72
+ r"\definecolor{DocVortexTableHeader}{HTML}{E5E7EB}",
73
+ r"\setlength{\parindent}{2em}",
74
+ r"\setlength{\parskip}{0.4em}",
75
+ r"\setlist{leftmargin=*,nosep}",
76
+ r"\setlength{\LTpre}{0.4em}",
77
+ r"\setlength{\LTpost}{0.6em}",
78
+ r"\newcommand{\DocVortexLineBreak}{\leavevmode\hfill\break}",
79
+ r"\newcommand{\DocVortexPlaceholder}[1]{\fcolorbox{DocVortexGray}{DocVortexSurface}{\textit{#1}}}",
80
+ )
81
+
82
+
83
+ class _LatexRenderer:
84
+ """保存一次完整 LaTeX 文档渲染所需的确定性状态。"""
85
+
86
+ def __init__(
87
+ self,
88
+ middle_json: MiddleJson,
89
+ *,
90
+ asset_base_path: str,
91
+ document_title: str | None,
92
+ ) -> None:
93
+ """初始化共享 planner、anchor、表格与代码环境上下文。"""
94
+ self.middle_json = middle_json
95
+ self.asset_base_path = normalize_asset_base_path(asset_base_path)
96
+ self.document_title = _resolve_document_title(middle_json, document_title)
97
+ self.anchors = LatexAnchorRegistry(_collect_anchor_targets(middle_json))
98
+ self.tables = LatexTableRenderer(asset_base_path=self.asset_base_path, anchors=self.anchors)
99
+ self.verbatim_counter = 0
100
+
101
+ def render(self) -> str:
102
+ """生成包含固定 TeX Live 导言区与正文环境的完整源码。"""
103
+ rendered_blocks: list[str] = []
104
+ for page in build_render_plan(self.middle_json):
105
+ for planned in page:
106
+ if planned.removed or planned.block.type in PAGE_AUXILIARY_BLOCK_TYPES:
107
+ continue
108
+ rendered = self._render_planned_block(planned)
109
+ if rendered.strip():
110
+ rendered_blocks.append(rendered.strip("\n"))
111
+ title = escape_latex_text(self.document_title, preserve_newlines=False)
112
+ lines = [*_PREAMBLE_LINES, rf"\hypersetup{{pdftitle={{{title}}}}}", r"\begin{document}"]
113
+ if rendered_blocks:
114
+ lines.extend(("", "\n\n".join(rendered_blocks), ""))
115
+ lines.append(r"\end{document}")
116
+ return "\n".join(lines) + "\n"
117
+
118
+ def _render_planned_block(self, planned: PlannedBlock) -> str:
119
+ """按严格 PageBlock 具体类型分发 LaTeX 渲染。"""
120
+ block = planned.block
121
+ if isinstance(block, TextBlock):
122
+ content = render_joined_inline_contents(planned.text_contents or [block.content], self.anchors)
123
+ return self._anchored_paragraph(content, block.anchor)
124
+ if isinstance(block, RefTextBlock):
125
+ content = render_joined_inline_contents(planned.text_contents or [block.content], self.anchors)
126
+ return _paragraph(content)
127
+ if isinstance(block, (DocTitleBlock, ParagraphTitleBlock)):
128
+ return self._render_title(block)
129
+ if isinstance(block, PageFootnoteBlock):
130
+ return self._render_page_footnote(block)
131
+ if isinstance(block, EquationBlock):
132
+ return self._render_equation(block)
133
+ if isinstance(block, ListBlock):
134
+ return self._render_list(block)
135
+ if isinstance(block, IndexBlock):
136
+ return self._render_index(block)
137
+ if isinstance(block, ImageBlock):
138
+ return self._render_image_block(block)
139
+ if isinstance(block, TableBlock):
140
+ return self._render_table_block(block)
141
+ if isinstance(block, ChartBlock):
142
+ return self._render_chart_block(block)
143
+ if isinstance(block, CodeBlock):
144
+ return self._render_code_block(block)
145
+ raise TypeError(f"Unsupported PageBlock type: {type(block).__name__}")
146
+
147
+ def _anchored_paragraph(self, content: str, anchor: str | None) -> str:
148
+ """给可见正文添加首次出现的文档级 hypertarget。"""
149
+ if not content.strip():
150
+ return ""
151
+ target = self.anchors.emit_target(anchor)
152
+ return "\n".join(part for part in (target, _paragraph(content)) if part)
153
+
154
+ def _render_title(self, block: DocTitleBlock | ParagraphTitleBlock) -> str:
155
+ """把全局标题层级映射为无编号 LaTeX 标题命令。"""
156
+ content = render_inline_spans(block.content, self.anchors)
157
+ if not content.strip():
158
+ return ""
159
+ target = self.anchors.emit_target(block.anchor)
160
+ command = {
161
+ 1: "section",
162
+ 2: "subsection",
163
+ 3: "subsubsection",
164
+ 4: "paragraph",
165
+ 5: "subparagraph",
166
+ }.get(block.level)
167
+ heading = rf"\{command}*{{{content}}}" if command is not None else rf"\par\noindent{{\small\bfseries {content}}}\par"
168
+ return "\n".join(part for part in (target, heading) if part)
169
+
170
+ def _render_page_footnote(self, block: PageFootnoteBlock) -> str:
171
+ """把页面脚注保留为可寻址的小字号灰色正文块。"""
172
+ content = render_inline_spans(block.content, self.anchors)
173
+ if not content.strip():
174
+ return ""
175
+ target = self.anchors.emit_target(block.anchor)
176
+ footnote = rf"{{\footnotesize\color{{DocVortexGray}} {content}\par}}"
177
+ return "\n".join(part for part in (target, footnote) if part)
178
+
179
+ def _render_equation(self, block: EquationBlock) -> str:
180
+ """原样输出块级 LaTeX;空公式按 sidecar 与占位顺序回退。"""
181
+ content = block.content.strip()
182
+ if content:
183
+ return _wrap_display_formula(content)
184
+ if not _has_image_payload(block):
185
+ return self._placeholder("formula unavailable") + r"\par"
186
+ return self._render_image_payload(block, alt_text="formula")
187
+
188
+ def _render_list(self, block: ListBlock) -> str:
189
+ """按共享 marker 分类递归输出原生或显式标记列表。"""
190
+ parsed_leaves = [
191
+ parse_list_item_marker(child.content)
192
+ for child in block.content
193
+ if not isinstance(child, ListBlock) and inline_plain_text(child.content).strip()
194
+ ]
195
+ add_reference_bullets = reference_list_needs_bullets(block)
196
+ environment, kind = _classify_list(parsed_leaves, add_reference_bullets)
197
+ options = "[leftmargin=*,nosep]"
198
+ if environment == "description":
199
+ options = "[style=nextline,leftmargin=3em,nosep]"
200
+ lines = [rf"\begin{{{environment}}}{options}"]
201
+ has_item = False
202
+ for child in block.content:
203
+ if isinstance(child, ListBlock):
204
+ nested = self._render_list(child)
205
+ if nested:
206
+ if not has_item:
207
+ lines.append(r"\item[]")
208
+ has_item = True
209
+ lines.append(nested)
210
+ continue
211
+ parsed = parse_list_item_marker(child.content)
212
+ item_spans, label = _list_item_content(parsed, child.content, kind, add_reference_bullets)
213
+ content = render_inline_spans(item_spans, self.anchors) or "~"
214
+ if environment == "description" or label is not None:
215
+ rendered_label = escape_latex_text(label or "", preserve_newlines=False)
216
+ lines.append(rf"\item[{{{rendered_label}}}] {content}")
217
+ else:
218
+ lines.append(rf"\item {content}")
219
+ has_item = True
220
+ lines.append(rf"\end{{{environment}}}")
221
+ return "\n".join(lines) if has_item else ""
222
+
223
+ def _render_index(self, block: IndexBlock) -> str:
224
+ """递归输出正文内目录,并仅链接真实 anchor target。"""
225
+ lines = [r"\begin{itemize}[leftmargin=*,nosep]"]
226
+ has_item = False
227
+ for child in block.content:
228
+ if isinstance(child, IndexBlock):
229
+ nested = self._render_index(child)
230
+ if nested:
231
+ if not has_item:
232
+ lines.append(r"\item[]")
233
+ has_item = True
234
+ lines.append(nested)
235
+ continue
236
+ content = strip_index_page_tail(child.content)
237
+ label = render_inline_spans(content, self.anchors).strip()
238
+ if not label:
239
+ continue
240
+ target = self.anchors.target_for(child.anchor)
241
+ if target is not None:
242
+ label = rf"\hyperlink{{{target}}}{{{label}}}"
243
+ lines.append(rf"\item {label}")
244
+ has_item = True
245
+ lines.append(r"\end{itemize}")
246
+ return "\n".join(lines) if has_item else ""
247
+
248
+ def _render_image_block(self, block: ImageBlock) -> str:
249
+ """按原始子块顺序输出图片主体及说明文本。"""
250
+ parts: list[str] = []
251
+ for child in block.content:
252
+ if isinstance(child, ImageBodyBlock):
253
+ alt = _plain_html_text(child.content) or block.sub_type or "image"
254
+ parts.append(self._render_image_payload(child, alt_text=alt))
255
+ elif isinstance(child, ImageAnnotationBlock):
256
+ parts.append(self._render_annotation(child))
257
+ else:
258
+ raise TypeError(f"Unsupported image child: {type(child).__name__}")
259
+ return _join_parts(parts)
260
+
261
+ def _render_table_block(self, block: TableBlock) -> str:
262
+ """优先输出原生 longtable,再回退图片、可见文本或占位。"""
263
+ parts: list[str] = []
264
+ for child in block.content:
265
+ if isinstance(child, TableBodyBlock):
266
+ parts.append(self._render_table_body(child))
267
+ elif isinstance(child, TableAnnotationBlock):
268
+ parts.append(self._render_annotation(child))
269
+ else:
270
+ raise TypeError(f"Unsupported table child: {type(child).__name__}")
271
+ return _join_parts(parts)
272
+
273
+ def _render_table_body(self, block: TableBodyBlock) -> str:
274
+ """执行结构化表格到 sidecar、文本、占位的固定回退链。"""
275
+ content = block.content.strip()
276
+ if content and _HTML_TABLE_RE.search(content):
277
+ try:
278
+ return self.tables.render(content)
279
+ except LatexTableError as exc:
280
+ logger.warning("LaTeX HTML table fallback: {}", exc)
281
+ image = self._render_local_image(block)
282
+ if image:
283
+ return image
284
+ plain = _plain_html_text(content)
285
+ if plain:
286
+ return self._render_verbatim(plain)
287
+ return self._placeholder("table unavailable")
288
+ if content:
289
+ return self._render_verbatim(block.content)
290
+ if not _has_image_payload(block):
291
+ return self._placeholder("table unavailable") + r"\par"
292
+ return self._render_image_payload(block, alt_text="table")
293
+
294
+ def _render_chart_block(self, block: ChartBlock) -> str:
295
+ """保留 chart 图片,并继续输出可物化的结构化表格。"""
296
+ parts: list[str] = []
297
+ for child in block.content:
298
+ if isinstance(child, ChartBodyBlock):
299
+ body_parts: list[str] = []
300
+ has_image_payload = bool(child.image_path or child.image_base64 or child.image_url)
301
+ local_image = self._render_local_image(child)
302
+ if local_image:
303
+ body_parts.append(local_image)
304
+ elif has_image_payload:
305
+ body_parts.append(self._render_image_payload(child, alt_text=block.sub_type or "chart"))
306
+ content = child.content.strip()
307
+ if content and _HTML_TABLE_RE.search(content):
308
+ try:
309
+ body_parts.append(self.tables.render(content))
310
+ except LatexTableError as exc:
311
+ logger.warning("LaTeX chart table fallback: {}", exc)
312
+ plain = _plain_html_text(content)
313
+ if plain:
314
+ body_parts.append(_paragraph(escape_latex_text(plain)))
315
+ elif content and not local_image:
316
+ body_parts.append(_paragraph(escape_latex_text(_plain_html_text(content))))
317
+ elif not has_image_payload and not content:
318
+ body_parts.append(self._placeholder("chart unavailable"))
319
+ parts.append(_join_parts(body_parts))
320
+ elif isinstance(child, ChartAnnotationBlock):
321
+ parts.append(self._render_annotation(child))
322
+ else:
323
+ raise TypeError(f"Unsupported chart child: {type(child).__name__}")
324
+ return _join_parts(parts)
325
+
326
+ def _render_code_block(self, block: CodeBlock) -> str:
327
+ """输出保留空白的代码或包含公式的算法正文及说明。"""
328
+ parts: list[str] = []
329
+ for child in block.content:
330
+ if isinstance(child, CodeBodyBlock):
331
+ if block.sub_type != BlockType.CODE:
332
+ raise TypeError("code_body requires code subtype")
333
+ parts.append(self._render_verbatim(child.content))
334
+ elif isinstance(child, AlgorithmBodyBlock):
335
+ if block.sub_type != RAW_ALGORITHM:
336
+ raise TypeError("algorithm_body requires algorithm subtype")
337
+ content = render_inline_spans(child.content, self.anchors)
338
+ parts.append("\n".join((r"\begin{quote}", rf"{{\small\ttfamily {content}\par}}", r"\end{quote}")))
339
+ elif isinstance(child, CodeAnnotationBlock):
340
+ parts.append(self._render_annotation(child))
341
+ else:
342
+ raise TypeError(f"Unsupported code child: {type(child).__name__}")
343
+ return _join_parts(parts)
344
+
345
+ def _render_annotation(
346
+ self,
347
+ block: ImageAnnotationBlock | TableAnnotationBlock | ChartAnnotationBlock | CodeAnnotationBlock,
348
+ ) -> str:
349
+ """按 caption/footnote discriminator 输出弱化说明样式。"""
350
+ content = render_inline_spans(block.content, self.anchors)
351
+ if not content.strip():
352
+ return ""
353
+ if str(block.type).endswith("caption"):
354
+ return rf"{{\small\itshape {content}\par}}"
355
+ return rf"{{\footnotesize\color{{DocVortexGray}} {content}\par}}"
356
+
357
+ def _render_image_payload(self, block: ImagePayloadBlock, *, alt_text: str) -> str:
358
+ """输出本地 sidecar;其它来源降级为链接或可见占位。"""
359
+ local = self._render_local_image(block)
360
+ if local:
361
+ return local
362
+ label = self._placeholder(f"image unavailable: {alt_text}")
363
+ remote_url = remote_block_image_url(block)
364
+ if remote_url:
365
+ return rf"\href{{{escape_latex_url(remote_url)}}}{{{label}}}\par"
366
+ return label + r"\par"
367
+
368
+ def _render_local_image(self, block: ImagePayloadBlock) -> str:
369
+ """若存在 XeLaTeX 支持的安全 sidecar,则生成有界 includegraphics。"""
370
+ path = resolve_block_image_path(block, self.asset_base_path)
371
+ if path is None:
372
+ return ""
373
+ return (
374
+ rf"\noindent\includegraphics[width=\linewidth,height=.8\textheight,keepaspectratio]"
375
+ rf"{{{tex_image_path(path)}}}\par"
376
+ )
377
+
378
+ def _placeholder(self, message: str) -> str:
379
+ """生成不暴露二进制载荷的稳定可见占位。"""
380
+ return rf"\DocVortexPlaceholder{{{escape_latex_text(message, preserve_newlines=False)}}}"
381
+
382
+ def _render_verbatim(self, content: str) -> str:
383
+ """为每个预格式块选择正文无法提前闭合的唯一 fvextra 环境。"""
384
+ normalized = content.replace("\r\n", "\n").replace("\r", "\n")
385
+ while True:
386
+ self.verbatim_counter += 1
387
+ environment = f"DocVortexVerbatim{self.verbatim_counter}"
388
+ if rf"\end{{{environment}}}" not in normalized:
389
+ break
390
+ definition = (
391
+ rf"\DefineVerbatimEnvironment{{{environment}}}{{Verbatim}}"
392
+ r"{breaklines=true,breakanywhere=true,fontsize=\small}"
393
+ )
394
+ return "\n".join((definition, rf"\begin{{{environment}}}", normalized, rf"\end{{{environment}}}"))
395
+
396
+
397
+ def render_latex(
398
+ middle_json: MiddleJson,
399
+ *,
400
+ asset_base_path: str = "",
401
+ document_title: str | None = None,
402
+ ) -> str:
403
+ """把严格 MiddleJson 纯函数式渲染为 TeX Live XeLaTeX 完整源码。"""
404
+ if not isinstance(middle_json, MiddleJson):
405
+ raise TypeError("render_latex expects a MiddleJson instance")
406
+ if not isinstance(asset_base_path, str):
407
+ raise TypeError("asset_base_path must be a string")
408
+ if document_title is not None and not isinstance(document_title, str):
409
+ raise TypeError("document_title must be a string or None")
410
+ return _LatexRenderer(
411
+ middle_json,
412
+ asset_base_path=asset_base_path,
413
+ document_title=document_title,
414
+ ).render()
415
+
416
+
417
+ def _resolve_document_title(middle_json: MiddleJson, explicit: str | None) -> str:
418
+ """按显式标题、首个文档标题和固定回退值解析 metadata title。"""
419
+ if explicit is not None and explicit.strip():
420
+ return explicit.strip()
421
+ for page in middle_json.pages:
422
+ for block in page.blocks:
423
+ if isinstance(block, DocTitleBlock):
424
+ title = inline_plain_text(block.content).strip()
425
+ if title:
426
+ return title
427
+ return "DocVortex Document"
428
+
429
+
430
+ def _collect_anchor_targets(middle_json: MiddleJson) -> set[str]:
431
+ """收集真实可见的正文、标题和页面脚注 anchor。"""
432
+ targets: set[str] = set()
433
+ for page in middle_json.pages:
434
+ for block in page.blocks:
435
+ if not isinstance(block, (TextBlock, TitleBlockBase, PageFootnoteBlock)):
436
+ continue
437
+ anchor = (block.anchor or "").strip()
438
+ if anchor and inline_plain_text(block.content).strip():
439
+ targets.add(anchor)
440
+ return targets
441
+
442
+
443
+ def _classify_list(items: list[ListItem], add_reference_bullets: bool) -> tuple[str, str]:
444
+ """根据直属 marker 选择 LaTeX 列表环境和内容策略。"""
445
+ if add_reference_bullets:
446
+ return "itemize", "reference"
447
+ if items and all(item.kind == "unordered" for item in items):
448
+ return "itemize", "unordered"
449
+ if items and all(item.kind == "ordered" for item in items):
450
+ styles = {item.ordered_style for item in items}
451
+ if len(styles) == 1:
452
+ return "enumerate", "ordered"
453
+ if items and all(item.kind == "none" for item in items):
454
+ return "itemize", "unmarked"
455
+ return "description", "explicit"
456
+
457
+
458
+ def _list_item_content(
459
+ item: ListItem,
460
+ original: list[InlineSpan],
461
+ kind: str,
462
+ add_reference_bullets: bool,
463
+ ) -> tuple[list[InlineSpan], str | None]:
464
+ """决定列表项应剥离、保留还是显式显示源 marker。"""
465
+ if add_reference_bullets:
466
+ return (item.body, None) if item.kind == "unordered" else (original, None)
467
+ if kind == "unordered":
468
+ return item.body, None
469
+ if kind == "ordered":
470
+ return item.body, item.marker
471
+ if kind == "unmarked":
472
+ return item.body, None
473
+ return item.body, item.marker
474
+
475
+
476
+ def _paragraph(content: str) -> str:
477
+ """把非空行内源码收束为一个普通 LaTeX 段落。"""
478
+ return f"{content}\\par" if content.strip() else ""
479
+
480
+
481
+ def _wrap_display_formula(content: str) -> str:
482
+ """保留完整 AMS display 环境,其它裸公式统一放入 equation*。"""
483
+ match = _TOP_LEVEL_DISPLAY_ENV_RE.match(content)
484
+ if match is not None and content.rstrip().endswith(rf"\end{{{match.group('name')}}}"):
485
+ return content
486
+ return "\n".join((r"\begin{equation*}", content, r"\end{equation*}"))
487
+
488
+
489
+ def _join_parts(parts: list[str]) -> str:
490
+ """按子块源顺序连接全部非空 LaTeX 片段。"""
491
+ return "\n\n".join(part for part in parts if part and part.strip())
492
+
493
+
494
+ def _plain_html_text(content: str) -> str:
495
+ """从 HTML 或普通字符串提取可见文本供替代信息使用。"""
496
+ if not content:
497
+ return ""
498
+ return BeautifulSoup(content, "html.parser").get_text("\n", strip=True)
499
+
500
+
501
+ def _has_image_payload(block: ImagePayloadBlock) -> bool:
502
+ """判断 block 是否声明任一种图片来源。"""
503
+ return bool(block.image_path or block.image_base64 or block.image_url)
504
+
505
+
506
+ __all__ = ["render_latex"]