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,635 @@
1
+ """Markdown 与 Content List 共用的 block 级序列化。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+
8
+ from ....options import LatexDelimitersConfig
9
+ from ..common.index import strip_index_page_tail
10
+ from ..common.list_items import has_markdown_unordered_marker, reference_list_needs_bullets
11
+ from ..common.planner import PlannedBlock
12
+ from .assets import build_markdown_image, resolve_image_source
13
+ from .escaping import escape_standalone_marker_rule, escape_text_block_markdown_prefix
14
+ from .inline import (
15
+ render_inline_content,
16
+ render_inline_spans,
17
+ render_inline_spans_in_html_context,
18
+ render_internal_link,
19
+ render_joined_inline_contents,
20
+ )
21
+ from .table import _strip_embedded_images, format_embedded_html, render_html_table
22
+ from ...contracts import ImageRenderer
23
+ from ....schema import (
24
+ RAW_ALGORITHM,
25
+ AlgorithmBodyBlock,
26
+ BlockType,
27
+ ChartAnnotationBlock,
28
+ ChartBlock,
29
+ ChartBodyBlock,
30
+ CodeAnnotationBlock,
31
+ CodeBlock,
32
+ CodeBodyBlock,
33
+ DocTitleBlock,
34
+ EquationBlock,
35
+ ImageAnnotationBlock,
36
+ ImageBlock,
37
+ ImageBodyBlock,
38
+ InlineSpan,
39
+ IndexBlock,
40
+ ListBlock,
41
+ PageAuxTextBlock,
42
+ PageFootnoteBlock,
43
+ PageBlock,
44
+ ParagraphTitleBlock,
45
+ RefTextBlock,
46
+ TableAnnotationBlock,
47
+ TableBlock,
48
+ TableBodyBlock,
49
+ TextBlock,
50
+ )
51
+
52
+ _VALID_CODE_LANGUAGE_RE = re.compile(r"[A-Za-z0-9_.+#-]+")
53
+
54
+
55
+ def render_planned_block(
56
+ planned: PlannedBlock,
57
+ *,
58
+ delimiters: LatexDelimitersConfig,
59
+ asset_base_url: str,
60
+ image_renderer: ImageRenderer | None = None,
61
+ anchor_targets: set[str] | None = None,
62
+ emitted_anchors: set[str] | None = None,
63
+ ) -> str:
64
+ """按具体 Pydantic block 类型分发 Markdown 渲染。"""
65
+ block = planned.block
66
+ if isinstance(block, TextBlock):
67
+ content = render_joined_inline_contents(planned.text_contents or [block.content], delimiters)
68
+ rendered = escape_standalone_marker_rule(escape_text_block_markdown_prefix(content))
69
+ return _prepend_markdown_anchor(rendered, block.anchor, emitted_anchors)
70
+ if isinstance(block, RefTextBlock):
71
+ content = render_joined_inline_contents(planned.text_contents or [block.content], delimiters)
72
+ return escape_standalone_marker_rule(content)
73
+ if isinstance(block, (DocTitleBlock, ParagraphTitleBlock)):
74
+ return _render_title(block, delimiters, emitted_anchors)
75
+ if isinstance(block, PageFootnoteBlock):
76
+ return _render_page_footnote(block, delimiters, emitted_anchors)
77
+ if isinstance(block, PageAuxTextBlock):
78
+ content = escape_text_block_markdown_prefix(render_inline_content(block.content, delimiters))
79
+ return escape_standalone_marker_rule(content)
80
+ if isinstance(block, EquationBlock):
81
+ return _render_equation(block, delimiters, asset_base_url, image_renderer)
82
+ if isinstance(block, ListBlock):
83
+ return _render_list(block, delimiters)
84
+ if isinstance(block, IndexBlock):
85
+ return _render_index(block, delimiters, anchor_targets=anchor_targets)
86
+ if isinstance(block, ImageBlock):
87
+ return _render_image_block(block, delimiters, asset_base_url, image_renderer)
88
+ if isinstance(block, TableBlock):
89
+ return _render_table_block(block, delimiters, asset_base_url, image_renderer)
90
+ if isinstance(block, ChartBlock):
91
+ return _render_chart_block(block, delimiters, asset_base_url, image_renderer)
92
+ if isinstance(block, CodeBlock):
93
+ return _render_code_block(block, delimiters)
94
+ raise TypeError(f"Unsupported PageBlock type: {type(block).__name__}")
95
+
96
+
97
+ def _claim_markdown_anchor(anchor: str | None, emitted_anchors: set[str] | None) -> str:
98
+ """规范化 Markdown 目标,并在文档级集合中只登记首次出现的 anchor。"""
99
+ normalized = (anchor or "").strip()
100
+ if not normalized:
101
+ return ""
102
+ if emitted_anchors is not None:
103
+ if normalized in emitted_anchors:
104
+ return ""
105
+ emitted_anchors.add(normalized)
106
+ return normalized
107
+
108
+
109
+ def _prepend_markdown_anchor(content: str, anchor: str | None, emitted_anchors: set[str] | None) -> str:
110
+ """给非空 Markdown 正文添加独立 HTML anchor,避免污染正文文本。"""
111
+ if not content.strip():
112
+ return content
113
+ normalized = _claim_markdown_anchor(anchor, emitted_anchors)
114
+ if not normalized:
115
+ return content
116
+ return f'<a id="{html.escape(normalized, quote=True)}"></a>\n{content}'
117
+
118
+
119
+ def _render_page_footnote(
120
+ block: PageFootnoteBlock,
121
+ delimiters: LatexDelimitersConfig,
122
+ emitted_anchors: set[str] | None = None,
123
+ ) -> str:
124
+ """用非折叠的小字号浅色 HTML 标识输出页面脚注。"""
125
+ content = escape_text_block_markdown_prefix(render_inline_content(block.content, delimiters))
126
+ rendered = escape_standalone_marker_rule(content)
127
+ if not rendered.strip():
128
+ return ""
129
+ rendered = rendered.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "<br>")
130
+ attrs = [
131
+ 'class="docvortex-page-footnote"',
132
+ 'data-block-type="page_footnote"',
133
+ 'style="color:#6b7280"',
134
+ ]
135
+ anchor = _claim_markdown_anchor(block.anchor, emitted_anchors)
136
+ if anchor:
137
+ attrs.insert(0, f'id="{html.escape(anchor, quote=True)}"')
138
+ return f"<small><span {' '.join(attrs)}>{rendered}</span></small>"
139
+
140
+
141
+ def render_single_block(
142
+ block: PageBlock,
143
+ *,
144
+ delimiters: LatexDelimitersConfig,
145
+ asset_base_url: str,
146
+ image_renderer: ImageRenderer | None = None,
147
+ ) -> str:
148
+ """不执行续段合并或页面过滤,直接渲染一个顶层 block。"""
149
+ text_contents = [block.content] if isinstance(block, (TextBlock, RefTextBlock)) else []
150
+ planned = PlannedBlock(page_idx=0, block=block, text_contents=text_contents)
151
+ return render_planned_block(
152
+ planned,
153
+ delimiters=delimiters,
154
+ asset_base_url=asset_base_url,
155
+ image_renderer=image_renderer,
156
+ )
157
+
158
+
159
+ def _render_title(
160
+ block: DocTitleBlock | ParagraphTitleBlock,
161
+ delimiters: LatexDelimitersConfig,
162
+ emitted_anchors: set[str] | None = None,
163
+ ) -> str:
164
+ """渲染带可选 HTML anchor 的 Markdown 标题。"""
165
+ level = min(max(block.level, 1), 6)
166
+ title = f"{'#' * level} {render_title_inline_content(block, delimiters)}"
167
+ anchor = _claim_markdown_anchor(block.anchor, emitted_anchors)
168
+ if not anchor:
169
+ return title
170
+ return f'<a id="{html.escape(anchor, quote=True)}"></a>\n{title}'
171
+
172
+
173
+ def render_title_inline_content(
174
+ block: DocTitleBlock | ParagraphTitleBlock,
175
+ delimiters: LatexDelimitersConfig,
176
+ ) -> str:
177
+ """只渲染标题的行内语义,不添加 heading 标记或 HTML anchor。"""
178
+ return render_inline_content(block.content, delimiters)
179
+
180
+
181
+ def _render_equation(
182
+ block: EquationBlock,
183
+ delimiters: LatexDelimitersConfig,
184
+ asset_base_url: str,
185
+ image_renderer: ImageRenderer | None,
186
+ ) -> str:
187
+ """优先渲染行间 LaTeX,空公式内容回退到 image_renderer 或公式图片。"""
188
+ latex = block.content.strip()
189
+ if latex:
190
+ return f"{delimiters.display.left}\n{latex}\n{delimiters.display.right}"
191
+ if image_renderer is not None:
192
+ rendered = image_renderer(block)
193
+ if rendered:
194
+ return rendered
195
+ source = resolve_image_source(block, asset_base_url)
196
+ return build_markdown_image(source) if source else ""
197
+
198
+
199
+ def _render_list(block: ListBlock, delimiters: LatexDelimitersConfig, depth: int = 0) -> str:
200
+ """递归渲染列表,并给多数条目无数字前缀的参考文献补无序标记。"""
201
+ add_ref_bullets = reference_list_needs_bullets(block)
202
+ lines: list[str] = []
203
+ for child in block.content:
204
+ if isinstance(child, ListBlock):
205
+ nested = _render_list(child, delimiters, depth + 1)
206
+ if nested:
207
+ lines.extend(nested.splitlines())
208
+ continue
209
+ item = render_inline_content(child.content, delimiters)
210
+ if not item:
211
+ continue
212
+ item = escape_standalone_marker_rule(item)
213
+ indent = " " * depth
214
+ item_lines = item.splitlines() or [item]
215
+ if add_ref_bullets and not has_markdown_unordered_marker(child.content):
216
+ lines.append(f"{indent}- {item_lines[0]}")
217
+ lines.extend(f"{indent} {line}" for line in item_lines[1:])
218
+ else:
219
+ lines.extend(f"{indent}{line}" for line in item_lines)
220
+ return "\n".join(lines)
221
+
222
+
223
+ def _render_index(
224
+ block: IndexBlock,
225
+ delimiters: LatexDelimitersConfig,
226
+ depth: int = 0,
227
+ *,
228
+ anchor_targets: set[str] | None = None,
229
+ ) -> str:
230
+ """递归渲染目录列表,并给标题叶子添加内部 anchor 链接。"""
231
+ lines: list[str] = []
232
+ for child in block.content:
233
+ if isinstance(child, IndexBlock):
234
+ nested = _render_index(child, delimiters, depth + 1, anchor_targets=anchor_targets)
235
+ if nested:
236
+ lines.extend(nested.splitlines())
237
+ continue
238
+ content = strip_index_page_tail(child.content)
239
+ label = render_inline_content(content, delimiters).strip()
240
+ if not label:
241
+ continue
242
+ anchor = (child.anchor or "").strip()
243
+ if anchor and (anchor_targets is None or anchor in anchor_targets):
244
+ label = render_internal_link(label, anchor)
245
+ lines.append(f"{' ' * depth}- {label}")
246
+ return "\n".join(lines)
247
+
248
+
249
+ def _render_image_block(
250
+ block: ImageBlock,
251
+ delimiters: LatexDelimitersConfig,
252
+ asset_base_url: str,
253
+ image_renderer: ImageRenderer | None,
254
+ ) -> str:
255
+ """按原始子块顺序渲染图片主体及说明文本。"""
256
+ parts: list[str] = []
257
+ for child in block.content:
258
+ if isinstance(child, ImageBodyBlock):
259
+ body = _render_visual_body_child(
260
+ block,
261
+ child,
262
+ delimiters=delimiters,
263
+ asset_base_url=asset_base_url,
264
+ image_renderer=image_renderer,
265
+ )
266
+ elif isinstance(child, ImageAnnotationBlock):
267
+ body = render_visual_annotation(child, delimiters)
268
+ else:
269
+ raise TypeError(f"Unsupported image child: {type(child).__name__}")
270
+ if body:
271
+ parts.append(body)
272
+ return _join_visual_parts(parts)
273
+
274
+
275
+ def _render_chart_block(
276
+ block: ChartBlock,
277
+ delimiters: LatexDelimitersConfig,
278
+ asset_base_url: str,
279
+ image_renderer: ImageRenderer | None,
280
+ ) -> str:
281
+ """按原始子块顺序渲染图表图片、结构内容和说明文本。"""
282
+ parts: list[str] = []
283
+ for child in block.content:
284
+ if isinstance(child, ChartBodyBlock):
285
+ body = _render_visual_body_child(
286
+ block,
287
+ child,
288
+ delimiters=delimiters,
289
+ asset_base_url=asset_base_url,
290
+ image_renderer=image_renderer,
291
+ )
292
+ elif isinstance(child, ChartAnnotationBlock):
293
+ body = render_visual_annotation(child, delimiters)
294
+ else:
295
+ raise TypeError(f"Unsupported chart child: {type(child).__name__}")
296
+ if body:
297
+ parts.append(body)
298
+ return _join_visual_parts(parts)
299
+
300
+
301
+ def _render_media_body(
302
+ block: ImageBodyBlock,
303
+ *,
304
+ summary: str,
305
+ delimiters: LatexDelimitersConfig,
306
+ asset_base_url: str,
307
+ image_renderer: ImageRenderer | None,
308
+ parent_block: ImageBlock,
309
+ ) -> str:
310
+ """渲染图片载荷,并将识别内容放入折叠详情。image_renderer 优先于 image_path。"""
311
+ rendered_content = _render_media_content(block, delimiters, asset_base_url)
312
+ if image_renderer is not None:
313
+ image_ref = image_renderer(parent_block)
314
+ parts = [image_ref] if image_ref else []
315
+ else:
316
+ source = resolve_image_source(block, asset_base_url)
317
+ parts = [build_markdown_image(source)] if source else []
318
+ if rendered_content:
319
+ parts.append(_render_details(rendered_content, summary))
320
+ return "\n\n".join(part for part in parts if part)
321
+
322
+
323
+ def _render_media_content(
324
+ block: ImageBodyBlock,
325
+ delimiters: LatexDelimitersConfig,
326
+ asset_base_url: str,
327
+ ) -> str:
328
+ """渲染图片识别内容,不添加图片语法或折叠详情包装。"""
329
+ content = block.content.strip()
330
+ if not content:
331
+ return ""
332
+ return format_embedded_html(content, asset_base_url=asset_base_url, delimiters=delimiters)
333
+
334
+
335
+ def _render_chart_body(
336
+ block: ChartBodyBlock,
337
+ *,
338
+ summary: str,
339
+ delimiters: LatexDelimitersConfig,
340
+ asset_base_url: str,
341
+ image_renderer: ImageRenderer | None,
342
+ parent_block: ChartBlock,
343
+ ) -> str:
344
+ """渲染 chart 图片,并统一转换其 HTML 表格内容。内容为空时回退到 image_renderer 或图片。"""
345
+ rendered_content = _render_chart_content(block.content, delimiters, asset_base_url)
346
+ if rendered_content:
347
+ return _render_chart_with_details(rendered_content, block, asset_base_url, summary)
348
+ if image_renderer is not None:
349
+ rendered = image_renderer(parent_block)
350
+ if rendered:
351
+ return rendered
352
+ source = resolve_image_source(block, asset_base_url)
353
+ return build_markdown_image(source) if source else ""
354
+
355
+
356
+ def _render_chart_with_details(
357
+ rendered_content: str,
358
+ block: ChartBodyBlock,
359
+ asset_base_url: str,
360
+ summary: str,
361
+ ) -> str:
362
+ """chart 有结构化内容时,附上图片与折叠详情。"""
363
+ source = resolve_image_source(block, asset_base_url)
364
+ if source:
365
+ parts = [build_markdown_image(source), _render_details(rendered_content, summary)]
366
+ return "\n\n".join(part for part in parts if part)
367
+ return rendered_content
368
+
369
+
370
+ def _render_chart_content(
371
+ content: str,
372
+ delimiters: LatexDelimitersConfig,
373
+ asset_base_url: str,
374
+ ) -> str:
375
+ """把 chart 内容中的简单 HTML 表格转为 GFM,其他内容保持原表示。"""
376
+ normalized = content.strip()
377
+ if not normalized:
378
+ return ""
379
+ html_table = render_html_table(
380
+ normalized,
381
+ asset_base_url=asset_base_url,
382
+ delimiters=delimiters,
383
+ )
384
+ if html_table is not None:
385
+ return html_table
386
+ return format_embedded_html(normalized, asset_base_url=asset_base_url, delimiters=delimiters)
387
+
388
+
389
+ def _render_details(content: str, summary: str) -> str:
390
+ """构造保留已渲染视觉内容的折叠 HTML 详情块。"""
391
+ safe_summary = html.escape(summary, quote=False)
392
+ return f"<details>\n<summary>{safe_summary}</summary>\n\n{content.strip()}\n</details>"
393
+
394
+
395
+ def _render_table_block(
396
+ block: TableBlock,
397
+ delimiters: LatexDelimitersConfig,
398
+ asset_base_url: str,
399
+ image_renderer: ImageRenderer | None,
400
+ ) -> str:
401
+ """按原始子块顺序渲染表格主体及说明文本。"""
402
+ parts: list[str] = []
403
+ for child in block.content:
404
+ if isinstance(child, TableBodyBlock):
405
+ body = _render_visual_body_child(
406
+ block,
407
+ child,
408
+ delimiters=delimiters,
409
+ asset_base_url=asset_base_url,
410
+ image_renderer=image_renderer,
411
+ )
412
+ elif isinstance(child, TableAnnotationBlock):
413
+ body = render_visual_annotation(child, delimiters)
414
+ else:
415
+ raise TypeError(f"Unsupported table child: {type(child).__name__}")
416
+ if body:
417
+ parts.append(body)
418
+ return _join_visual_parts(parts)
419
+
420
+
421
+ def _render_table_body(
422
+ block: TableBodyBlock,
423
+ delimiters: LatexDelimitersConfig,
424
+ asset_base_url: str,
425
+ image_renderer: ImageRenderer | None,
426
+ parent_block: TableBlock,
427
+ ) -> str:
428
+ """按 HTML、空间投影文本、image_renderer、图片的优先级渲染表格主体。"""
429
+ rendered_content = _render_table_content(
430
+ block,
431
+ delimiters,
432
+ asset_base_url,
433
+ strip_embedded_images=image_renderer is not None,
434
+ )
435
+ if rendered_content:
436
+ return rendered_content
437
+ if image_renderer is not None:
438
+ rendered = image_renderer(parent_block)
439
+ if rendered:
440
+ return rendered
441
+ source = resolve_image_source(block, asset_base_url)
442
+ return build_markdown_image(source) if source else ""
443
+
444
+
445
+ def _render_table_content(
446
+ block: TableBodyBlock,
447
+ delimiters: LatexDelimitersConfig,
448
+ asset_base_url: str,
449
+ *,
450
+ strip_embedded_images: bool = False,
451
+ ) -> str:
452
+ """渲染表格结构内容,不执行空内容时的图片回退。"""
453
+ content = _strip_embedded_images(block.content) if strip_embedded_images else block.content
454
+ if not content:
455
+ return ""
456
+ html_table = render_html_table(
457
+ content,
458
+ asset_base_url=asset_base_url,
459
+ delimiters=delimiters,
460
+ )
461
+ if html_table is not None:
462
+ return html_table
463
+ return _render_fenced_content(content)
464
+
465
+
466
+ def _render_code_block(block: CodeBlock, delimiters: LatexDelimitersConfig) -> str:
467
+ """按父块 subtype 渲染普通代码或支持公式的算法。"""
468
+ parts: list[str] = []
469
+ for child in block.content:
470
+ if isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
471
+ body = _render_visual_body_child(
472
+ block,
473
+ child,
474
+ delimiters=delimiters,
475
+ asset_base_url="",
476
+ image_renderer=None,
477
+ )
478
+ elif isinstance(child, CodeAnnotationBlock):
479
+ body = render_visual_annotation(child, delimiters)
480
+ else:
481
+ raise TypeError(f"Unsupported code child: {type(child).__name__}")
482
+ if body:
483
+ parts.append(body)
484
+ return _join_visual_parts(parts)
485
+
486
+
487
+ def render_visual_body_content(
488
+ block: ImageBlock | TableBlock | ChartBlock | CodeBlock,
489
+ *,
490
+ delimiters: LatexDelimitersConfig,
491
+ asset_base_url: str,
492
+ ) -> str:
493
+ """查找视觉父块唯一 body,并只渲染可结构化消费的语义内容。"""
494
+ for child in block.content:
495
+ if isinstance(block, ImageBlock) and isinstance(child, ImageBodyBlock):
496
+ return _render_media_content(child, delimiters, asset_base_url)
497
+ if isinstance(block, TableBlock) and isinstance(child, TableBodyBlock):
498
+ return _render_table_content(child, delimiters, asset_base_url)
499
+ if isinstance(block, ChartBlock) and isinstance(child, ChartBodyBlock):
500
+ return _render_chart_content(child.content, delimiters, asset_base_url)
501
+ if isinstance(block, CodeBlock) and isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
502
+ return _render_code_semantic_content(block, child, delimiters)
503
+ raise ValueError(f"Missing visual body: {block.type}")
504
+
505
+
506
+ def _render_visual_body_child(
507
+ block: ImageBlock | TableBlock | ChartBlock | CodeBlock,
508
+ child: ImageBodyBlock | TableBodyBlock | ChartBodyBlock | CodeBodyBlock | AlgorithmBodyBlock,
509
+ *,
510
+ delimiters: LatexDelimitersConfig,
511
+ asset_base_url: str,
512
+ image_renderer: ImageRenderer | None = None,
513
+ ) -> str:
514
+ """按视觉父子类型组合渲染一个 body 子块。"""
515
+ if isinstance(block, ImageBlock) and isinstance(child, ImageBodyBlock):
516
+ return _render_media_body(
517
+ child,
518
+ summary=block.sub_type or "image content",
519
+ delimiters=delimiters,
520
+ asset_base_url=asset_base_url,
521
+ image_renderer=image_renderer,
522
+ parent_block=block,
523
+ )
524
+ if isinstance(block, TableBlock) and isinstance(child, TableBodyBlock):
525
+ return _render_table_body(child, delimiters, asset_base_url, image_renderer, block)
526
+ if isinstance(block, ChartBlock) and isinstance(child, ChartBodyBlock):
527
+ return _render_chart_body(
528
+ child,
529
+ summary=block.sub_type or "chart content",
530
+ delimiters=delimiters,
531
+ asset_base_url=asset_base_url,
532
+ image_renderer=image_renderer,
533
+ parent_block=block,
534
+ )
535
+ if isinstance(block, CodeBlock) and isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
536
+ return _render_code_body(block, child, delimiters)
537
+ raise TypeError(f"Unsupported visual body pair: {block.type}/{child.type}")
538
+
539
+
540
+ def render_visual_annotation(
541
+ block: ImageAnnotationBlock | TableAnnotationBlock | ChartAnnotationBlock | CodeAnnotationBlock,
542
+ delimiters: LatexDelimitersConfig,
543
+ ) -> str:
544
+ """把一个视觉说明子块渲染为独立 Markdown 字符串。"""
545
+ return escape_standalone_marker_rule(render_inline_content(block.content, delimiters))
546
+
547
+
548
+ def _render_code_body(
549
+ block: CodeBlock,
550
+ child: CodeBodyBlock | AlgorithmBodyBlock,
551
+ delimiters: LatexDelimitersConfig,
552
+ ) -> str:
553
+ """依据父块 subtype 渲染代码或算法 body。"""
554
+ if block.sub_type == RAW_ALGORITHM:
555
+ if not isinstance(child, AlgorithmBodyBlock):
556
+ raise TypeError("algorithm subtype requires AlgorithmBodyBlock")
557
+ return _render_algorithm_html(child.content, delimiters)
558
+ return _render_code_semantic_content(block, child, delimiters)
559
+
560
+
561
+ def _render_code_semantic_content(
562
+ block: CodeBlock,
563
+ child: CodeBodyBlock | AlgorithmBodyBlock,
564
+ delimiters: LatexDelimitersConfig,
565
+ ) -> str:
566
+ """把代码或算法 body 渲染为 Structured Content 可消费的类 Markdown 字符串。"""
567
+ if block.sub_type == BlockType.CODE:
568
+ if not isinstance(child, CodeBodyBlock):
569
+ raise TypeError("code subtype requires CodeBodyBlock")
570
+ return _render_fenced_content(child.content, _normalize_code_language(block.guess_lang))
571
+ if block.sub_type == RAW_ALGORITHM:
572
+ if not isinstance(child, AlgorithmBodyBlock):
573
+ raise TypeError("algorithm subtype requires AlgorithmBodyBlock")
574
+ return _render_algorithm_markdown(child.content, delimiters)
575
+ raise ValueError(f"Unsupported code subtype: {block.sub_type}")
576
+
577
+
578
+ def _normalize_code_language(language: str | None) -> str:
579
+ """校验 fenced code info string,非法值统一回退 txt。"""
580
+ normalized = (language or "").strip()
581
+ if not normalized or _VALID_CODE_LANGUAGE_RE.fullmatch(normalized) is None:
582
+ return "txt"
583
+ return normalized
584
+
585
+
586
+ def _render_fenced_content(content: str, language: str | None = None) -> str:
587
+ """使用长于正文反引号游程的围栏包裹原始内容。"""
588
+ longest = max((len(match.group(0)) for match in re.finditer(r"`+", content)), default=0)
589
+ fence = "`" * max(3, longest + 1)
590
+ opening = f"{fence}{language or ''}"
591
+ closing_prefix = "" if content.endswith("\n") else "\n"
592
+ return f"{opening}\n{content}{closing_prefix}{fence}"
593
+
594
+
595
+ def _render_algorithm_html(content: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
596
+ """参考 dev 实现渲染保留空白、上下标和行内公式的算法 HTML。"""
597
+ parts: list[str] = []
598
+ previous_equation = False
599
+ for span in content:
600
+ current_equation = str(span.type) == "equation_inline"
601
+ if previous_equation and current_equation:
602
+ parts.append(" ")
603
+ parts.append(render_inline_spans_in_html_context([span], delimiters))
604
+ previous_equation = current_equation
605
+ body = "".join(parts)
606
+ if not body.strip():
607
+ return ""
608
+ return f'<div class="docvortex-algorithm" style="white-space: pre-wrap; font-family:monospace;">\n{body}\n</div>'
609
+
610
+
611
+ def _render_algorithm_markdown(content: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
612
+ """把算法 Span 渲染为类 Markdown 内容,并分隔相邻行内公式。"""
613
+ parts: list[str] = []
614
+ previous_equation = False
615
+ for span in content:
616
+ current_equation = str(span.type) == "equation_inline"
617
+ if previous_equation and current_equation:
618
+ parts.append(" ")
619
+ parts.append(render_inline_spans([span], delimiters))
620
+ previous_equation = current_equation
621
+ return "".join(parts)
622
+
623
+
624
+ def _join_visual_parts(parts: list[str]) -> str:
625
+ """使用安全空行连接同一视觉父块中的有序子块。"""
626
+ return "\n\n".join(part.strip("\n") for part in parts if part and part.strip())
627
+
628
+
629
+ __all__ = [
630
+ "render_planned_block",
631
+ "render_single_block",
632
+ "render_title_inline_content",
633
+ "render_visual_annotation",
634
+ "render_visual_body_content",
635
+ ]
@@ -0,0 +1,51 @@
1
+ """Markdown 文本转义与 block 前缀保护。"""
2
+
3
+ import re
4
+
5
+ CONSERVATIVE_MARKDOWN_SPECIAL_CHARS = ("*", "_", "`", "~", "$")
6
+ TEXT_BLOCK_MARKDOWN_PREFIX_RE = re.compile(r"^(?P<indent>[ \t]{0,3})(?P<marker>#{1,6}|[+-])(?=[ \t])")
7
+
8
+
9
+ # DONE in render
10
+ def escape_conservative_markdown_text(content: str) -> str:
11
+ """Escape plain-text characters that carry inline Markdown semantics."""
12
+ if not content:
13
+ return content
14
+
15
+ escaped_chars = []
16
+ preceding_backslashes = 0
17
+
18
+ for char in content:
19
+ if char == "\\":
20
+ escaped_chars.append(char)
21
+ preceding_backslashes += 1
22
+ continue
23
+
24
+ if char in CONSERVATIVE_MARKDOWN_SPECIAL_CHARS and preceding_backslashes % 2 == 0:
25
+ escaped_chars.append("\\")
26
+
27
+ escaped_chars.append(char)
28
+ preceding_backslashes = 0
29
+
30
+ return "".join(escaped_chars)
31
+
32
+
33
+ # DONE in render
34
+ def escape_text_block_markdown_prefix(content: str) -> str:
35
+ """Escape a leading Markdown block marker in an assembled text block."""
36
+ if not content:
37
+ return content
38
+
39
+ match = TEXT_BLOCK_MARKDOWN_PREFIX_RE.match(content)
40
+ if not match:
41
+ return content
42
+
43
+ marker_start = match.start("marker")
44
+ return f"{content[:marker_start]}\\{content[marker_start:]}"
45
+
46
+
47
+ def escape_standalone_marker_rule(content: str) -> str:
48
+ """转义独立的下划线或短横线序列,避免被 Markdown 识别为分割线。"""
49
+ if content and (set(content) == {"_"} or set(content) == {"-"}):
50
+ return f"\\{content}"
51
+ return content