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,260 @@
1
+ """Middle JSON 2.0 行内 Span 到 Markdown 的安全序列化。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+ from collections.abc import Sequence
8
+
9
+ from ....content.inline import join_inline_spans
10
+ from ....options import LatexDelimitersConfig
11
+ from ....schema import CodeInlineSpan, EquationInlineSpan, HyperlinkSpan, InlineSpan, TextSpan
12
+ from .escaping import escape_conservative_markdown_text
13
+
14
+ _HTML_LIKE_TEXT_RE = re.compile(r"</?[A-Za-z][^<>\n]*>|<!--.*?-->|<![A-Za-z][^<>\n]*>|<\?[^<>\n]*\?>")
15
+ _ENTITY_LIKE_RE = re.compile(r"&(?:#[xX][0-9A-Fa-f]+|#[0-9]+|[A-Za-z][A-Za-z0-9]+);?")
16
+ _SIMPLE_STYLE_WRAPPERS = {
17
+ frozenset({"bold"}): "**",
18
+ frozenset({"italic"}): "*",
19
+ frozenset({"strikethrough"}): "~~",
20
+ frozenset({"bold", "italic"}): "***",
21
+ }
22
+
23
+
24
+ def render_inline_content(content: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
25
+ """把一段 MiddleJson 行内内容渲染为 Markdown。"""
26
+ return render_inline_spans(content, delimiters)
27
+
28
+
29
+ def render_joined_inline_contents(contents: list[list[InlineSpan]], delimiters: LatexDelimitersConfig) -> str:
30
+ """按物理段落边界规则合并多段 content 后渲染 Markdown。"""
31
+ return render_inline_spans(join_inline_spans(contents), delimiters)
32
+
33
+
34
+ def render_inline_spans(spans: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
35
+ """把行内 Span 序列化为 Markdown 与必要的安全 HTML。"""
36
+ return "".join(_render_inline_span(span, delimiters) for span in spans)
37
+
38
+
39
+ def render_inline_spans_in_html_context(spans: list[InlineSpan], delimiters: LatexDelimitersConfig) -> str:
40
+ """把 Markdown raw HTML 容器内的 Span 全部序列化为安全 HTML 行内语法。"""
41
+ return "".join(_render_inline_span_in_html_context(span, delimiters) for span in spans)
42
+
43
+
44
+ def _render_inline_span(span: InlineSpan, delimiters: LatexDelimitersConfig) -> str:
45
+ """渲染单个结构化行内 Span。"""
46
+ if isinstance(span, TextSpan):
47
+ content = _escape_plain_markdown_text(span.content)
48
+ return _apply_styles(content, span.content, span.styles)
49
+ if isinstance(span, CodeInlineSpan):
50
+ return _render_inline_code(span.content)
51
+ if isinstance(span, EquationInlineSpan):
52
+ return f"{delimiters.inline.left}{span.content}{delimiters.inline.right}"
53
+ if isinstance(span, HyperlinkSpan):
54
+ label = render_inline_spans(list(span.content), delimiters)
55
+ return _render_link(label, span.url, _requires_html_link(list(span.content)))
56
+ raise TypeError(f"Unsupported inline span: {type(span).__name__}")
57
+
58
+
59
+ def _render_inline_span_in_html_context(span: InlineSpan, delimiters: LatexDelimitersConfig) -> str:
60
+ """渲染一个嵌入 Markdown raw HTML block 的结构化行内 Span。"""
61
+ if isinstance(span, TextSpan):
62
+ return _apply_html_styles(html.escape(span.content, quote=False), span.styles)
63
+ if isinstance(span, CodeInlineSpan):
64
+ return f"<code>{html.escape(span.content, quote=False)}</code>"
65
+ if isinstance(span, EquationInlineSpan):
66
+ return html.escape(f"{delimiters.inline.left}{span.content}{delimiters.inline.right}", quote=False)
67
+ if isinstance(span, HyperlinkSpan):
68
+ label = render_inline_spans_in_html_context(list(span.content), delimiters)
69
+ return _render_link(label, span.url, True)
70
+ raise TypeError(f"Unsupported inline span: {type(span).__name__}")
71
+
72
+
73
+ def _render_inline_code(content: str) -> str:
74
+ """选择长于内容中反引号游程的 fence,稳定输出 Markdown 行内代码。"""
75
+ normalized = content.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
76
+ longest = max((len(match.group(0)) for match in re.finditer(r"`+", normalized)), default=0)
77
+ fence = "`" * (longest + 1)
78
+ if normalized.startswith(("`", " ")) or normalized.endswith(("`", " ")):
79
+ return f"{fence} {normalized} {fence}"
80
+ return f"{fence}{normalized}{fence}"
81
+
82
+
83
+ def _escape_plain_markdown_text(content: str) -> str:
84
+ """转义 Markdown 符号,并把普通文字中的标签外观保持为惰性实体。"""
85
+ parts: list[str] = []
86
+ cursor = 0
87
+ for match in _HTML_LIKE_TEXT_RE.finditer(content):
88
+ parts.append(_escape_entity_like_text(escape_conservative_markdown_text(content[cursor : match.start()])))
89
+ parts.append(html.escape(match.group(0), quote=False))
90
+ cursor = match.end()
91
+ parts.append(_escape_entity_like_text(escape_conservative_markdown_text(content[cursor:])))
92
+ return "".join(parts)
93
+
94
+
95
+ def _escape_entity_like_text(content: str) -> str:
96
+ """保护会被下游 Markdown 解析器当作 HTML 实体的字面量文本。"""
97
+
98
+ def replace(match: re.Match[str]) -> str:
99
+ """只保护确实会被 HTML 实体解码器改写的候选。"""
100
+ candidate = match.group(0)
101
+ return f"&amp;{candidate[1:]}" if html.unescape(candidate) != candidate else candidate
102
+
103
+ return _ENTITY_LIKE_RE.sub(replace, content)
104
+
105
+
106
+ def _apply_styles(content: str, plain_text: str, styles: Sequence[str]) -> str:
107
+ """按样式复杂度选择 Markdown wrapper 或安全 HTML 标签。"""
108
+ if not content or not styles:
109
+ return content
110
+ marker = _get_visible_space_marker(styles)
111
+ if marker is not None:
112
+ rendered_markers = _render_visible_space_marker_text(content, plain_text, styles, marker)
113
+ if rendered_markers is not None:
114
+ return rendered_markers
115
+ if plain_text and not plain_text.strip() and any(style in styles for style in ("underline", "strikethrough", "emphasis")):
116
+ return _render_visible_whitespace(plain_text, styles)
117
+
118
+ return _apply_style_wrappers(content, styles)
119
+
120
+
121
+ def _get_visible_space_marker(styles: Sequence[str]) -> str | None:
122
+ """按 dev 规则选择可见空格 marker,下划线优先于删除线。"""
123
+ if "underline" in styles:
124
+ return "_"
125
+ if "strikethrough" in styles:
126
+ return "-"
127
+ return None
128
+
129
+
130
+ def _render_visible_space_marker_text(
131
+ content: str,
132
+ plain_text: str,
133
+ styles: Sequence[str],
134
+ marker: str,
135
+ ) -> str | None:
136
+ """把纯 ASCII 空格或非空文本首尾空格转换为可见 marker。"""
137
+ if not plain_text:
138
+ return None
139
+ style_key = frozenset(styles)
140
+ force_html = style_key not in _SIMPLE_STYLE_WRAPPERS
141
+ if all(char == " " for char in plain_text):
142
+ ignored_style = "underline" if marker == "_" else "strikethrough"
143
+ remaining_styles = [style for style in styles if style != ignored_style]
144
+ return _apply_style_wrappers(
145
+ marker * len(plain_text),
146
+ remaining_styles,
147
+ force_html=force_html,
148
+ )
149
+
150
+ leading_count = len(plain_text) - len(plain_text.lstrip(" "))
151
+ trailing_count = len(plain_text) - len(plain_text.rstrip(" "))
152
+ if leading_count == 0 and trailing_count == 0:
153
+ return None
154
+ if not content.startswith(" " * leading_count) or not content.endswith(" " * trailing_count):
155
+ return None
156
+ content_end = len(content) - trailing_count if trailing_count else len(content)
157
+ core = content[leading_count:content_end]
158
+ rendered = f"{marker * leading_count}{core}{marker * trailing_count}"
159
+ return _apply_style_wrappers(rendered, styles, force_html=force_html)
160
+
161
+
162
+ def _apply_style_wrappers(
163
+ content: str,
164
+ styles: Sequence[str],
165
+ *,
166
+ force_html: bool = False,
167
+ ) -> str:
168
+ """给已处理空格的内容添加 Markdown 或 HTML 样式 wrapper。"""
169
+ if not content or not styles:
170
+ return content
171
+
172
+ leading = content[: len(content) - len(content.lstrip(" \t"))]
173
+ trailing = content[len(content.rstrip(" \t")) :]
174
+ core = content.strip(" \t")
175
+ if not core:
176
+ return content
177
+
178
+ style_key = frozenset(styles)
179
+ wrapper = _SIMPLE_STYLE_WRAPPERS.get(style_key)
180
+ if wrapper is not None and not force_html:
181
+ return f"{leading}{wrapper}{core}{wrapper}{trailing}"
182
+ return f"{leading}{_apply_html_styles(core, styles)}{trailing}"
183
+
184
+
185
+ def render_styled_markdown_text(content: str, styles: Sequence[str]) -> str:
186
+ """按正文相同规则把已转义文字渲染为 Markdown 或安全 HTML 样式。"""
187
+ return _apply_style_wrappers(content, styles)
188
+
189
+
190
+ def markdown_styles_require_html(styles: Sequence[str]) -> bool:
191
+ """判断样式组合是否必须整体使用 HTML 标签表达。"""
192
+ return bool(styles) and frozenset(styles) not in _SIMPLE_STYLE_WRAPPERS
193
+
194
+
195
+ def _render_visible_whitespace(content: str, styles: Sequence[str]) -> str:
196
+ """使用原 HTML 规则保留非 ASCII marker 场景的可见空白。"""
197
+ visible = "".join("<br>" if char == "\n" else "&nbsp;" for char in content.expandtabs(4))
198
+ return _apply_html_styles(visible, styles)
199
+
200
+
201
+ def _apply_html_styles(content: str, styles: Sequence[str]) -> str:
202
+ """按稳定顺序给复杂样式添加 HTML wrapper。"""
203
+ if "superscript" in styles:
204
+ content = f"<sup>{content}</sup>"
205
+ elif "subscript" in styles:
206
+ content = f"<sub>{content}</sub>"
207
+ if "underline" in styles:
208
+ content = f"<u>{content}</u>"
209
+ if "bold" in styles:
210
+ content = f"<strong>{content}</strong>"
211
+ if "italic" in styles:
212
+ content = f"<em>{content}</em>"
213
+ if "strikethrough" in styles:
214
+ content = f"<s>{content}</s>"
215
+ if "emphasis" in styles:
216
+ content = f'<span style="text-emphasis: dot; text-emphasis-position: under;">{content}</span>'
217
+ return content
218
+
219
+
220
+ def _requires_html_link(spans: list[InlineSpan]) -> bool:
221
+ """判断链接标签是否含不适合嵌入 Markdown link 的复杂样式。"""
222
+ for span in spans:
223
+ if isinstance(span, TextSpan):
224
+ if span.styles and frozenset(span.styles) not in _SIMPLE_STYLE_WRAPPERS:
225
+ return True
226
+ return False
227
+
228
+
229
+ def _render_link(label: str, url: str, use_html: bool) -> str:
230
+ """按标签复杂度输出 Markdown 或 HTML 超链接。"""
231
+ if not label:
232
+ return ""
233
+ if not url or url == ".":
234
+ return label
235
+ if use_html:
236
+ return f'<a href="{html.escape(url, quote=True)}">{label}</a>'
237
+ safe_url = url.replace("\\", "%5C").replace(" ", "%20").replace("(", "%28").replace(")", "%29")
238
+ return f"[{_escape_markdown_link_label(label)}]({safe_url})"
239
+
240
+
241
+ def render_internal_link(label: str, anchor: str) -> str:
242
+ """把已渲染目录标签包装为当前文档内锚点链接。"""
243
+ safe_anchor = anchor.replace(" ", "%20").replace("(", "%28").replace(")", "%29")
244
+ return f"[{_escape_markdown_link_label(label)}](#{safe_anchor})"
245
+
246
+
247
+ def _escape_markdown_link_label(label: str) -> str:
248
+ """转义 Markdown link 标签中的方括号并保留既有反斜杠。"""
249
+ return re.sub(r"(?<!\\)([\[\]])", r"\\\1", label)
250
+
251
+
252
+ __all__ = [
253
+ "markdown_styles_require_html",
254
+ "render_inline_content",
255
+ "render_inline_spans",
256
+ "render_inline_spans_in_html_context",
257
+ "render_internal_link",
258
+ "render_joined_inline_contents",
259
+ "render_styled_markdown_text",
260
+ ]
@@ -0,0 +1,93 @@
1
+ """严格 MiddleJson 到 Markdown 的公共渲染实现。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ....options import LatexDelimitersConfig
6
+ from ....content.inline import inline_plain_text
7
+ from ....schema import PAGE_AUXILIARY_BLOCK_TYPES, MiddleJson, PageFootnoteBlock, TextBlock, TitleBlockBase
8
+ from ...contracts import ImageRenderer, RenderMode
9
+ from ..common.planner import PlannedBlock, build_render_plan
10
+ from .blocks import render_planned_block
11
+
12
+ _PAGE_SEPARATOR = "\n\n---\n\n"
13
+
14
+
15
+ def render_markdown(
16
+ middle_json: MiddleJson,
17
+ *,
18
+ mode: RenderMode = RenderMode.DEFAULT,
19
+ asset_base_url: str = "",
20
+ image_renderer: ImageRenderer | None = None,
21
+ latex_delimiters: LatexDelimitersConfig | None = None,
22
+ ) -> str:
23
+ """把严格 MiddleJson 纯函数式渲染为 Markdown 字符串。"""
24
+ if not isinstance(middle_json, MiddleJson):
25
+ raise TypeError("render_markdown expects a MiddleJson instance")
26
+ if not isinstance(mode, RenderMode):
27
+ raise TypeError("mode must be a RenderMode value")
28
+
29
+ delimiters = latex_delimiters or LatexDelimitersConfig()
30
+ planned_pages = build_render_plan(middle_json, mode)
31
+ anchor_targets = _collect_markdown_anchor_targets(middle_json)
32
+ emitted_anchors: set[str] = set()
33
+ rendered_pages = [
34
+ _render_page(
35
+ page,
36
+ mode=mode,
37
+ delimiters=delimiters,
38
+ asset_base_url=asset_base_url,
39
+ image_renderer=image_renderer,
40
+ anchor_targets=anchor_targets,
41
+ emitted_anchors=emitted_anchors,
42
+ )
43
+ for page in planned_pages
44
+ ]
45
+ if mode is RenderMode.FULL:
46
+ return _PAGE_SEPARATOR.join(rendered_pages)
47
+ return "\n\n".join(page for page in rendered_pages if page)
48
+
49
+
50
+ def _render_page(
51
+ planned_blocks: list[PlannedBlock],
52
+ *,
53
+ mode: RenderMode,
54
+ delimiters: LatexDelimitersConfig,
55
+ asset_base_url: str,
56
+ image_renderer: ImageRenderer | None = None,
57
+ anchor_targets: set[str] | None = None,
58
+ emitted_anchors: set[str] | None = None,
59
+ ) -> str:
60
+ """渲染单页逻辑块,并在默认模式中过滤重复页元素。"""
61
+ rendered: list[str] = []
62
+ for planned in planned_blocks:
63
+ if planned.removed:
64
+ continue
65
+ if mode is RenderMode.DEFAULT and planned.block.type in PAGE_AUXILIARY_BLOCK_TYPES:
66
+ continue
67
+ text = render_planned_block(
68
+ planned,
69
+ delimiters=delimiters,
70
+ asset_base_url=asset_base_url,
71
+ image_renderer=image_renderer,
72
+ anchor_targets=anchor_targets,
73
+ emitted_anchors=emitted_anchors,
74
+ )
75
+ if text and text.strip():
76
+ rendered.append(text.strip("\n"))
77
+ return "\n\n".join(rendered)
78
+
79
+
80
+ def _collect_markdown_anchor_targets(middle_json: MiddleJson) -> set[str]:
81
+ """收集真实可见的顶层正文、标题和页面脚注 anchor,供目录链接判定。"""
82
+ targets: set[str] = set()
83
+ for page in middle_json.pages:
84
+ for block in page.blocks:
85
+ if not isinstance(block, (TextBlock, TitleBlockBase, PageFootnoteBlock)):
86
+ continue
87
+ anchor = (block.anchor or "").strip()
88
+ if anchor and inline_plain_text(block.content).strip():
89
+ targets.add(anchor)
90
+ return targets
91
+
92
+
93
+ __all__ = ["render_markdown"]
@@ -0,0 +1,281 @@
1
+ """Markdown renderer 的 HTML 表格判型与无损 GFM 转换。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+
8
+ from bs4 import BeautifulSoup
9
+ from bs4.element import NavigableString, Tag
10
+
11
+ from ....options import LatexDelimitersConfig
12
+ from .assets import prefix_html_image_sources
13
+ from .inline import markdown_styles_require_html, render_styled_markdown_text
14
+
15
+ _INLINE_EQ_RE = re.compile(r"<eq>(?P<latex>.*?)</eq>", re.IGNORECASE | re.DOTALL)
16
+ _GFM_FORMULA_PIPE_RE = re.compile(r"(?P<slashes>\\*)\|")
17
+ _COMPLEX_CELL_TAGS = {
18
+ "blockquote",
19
+ "div",
20
+ "dl",
21
+ "figure",
22
+ "h1",
23
+ "h2",
24
+ "h3",
25
+ "h4",
26
+ "h5",
27
+ "h6",
28
+ "img",
29
+ "li",
30
+ "math",
31
+ "object",
32
+ "ol",
33
+ "pre",
34
+ "svg",
35
+ "table",
36
+ "ul",
37
+ }
38
+ _ALLOWED_INLINE_TAGS = {
39
+ "a",
40
+ "b",
41
+ "br",
42
+ "code",
43
+ "em",
44
+ "eq",
45
+ "i",
46
+ "p",
47
+ "s",
48
+ "span",
49
+ "strong",
50
+ "sub",
51
+ "sup",
52
+ "u",
53
+ }
54
+ _STYLE_TAGS = {
55
+ "b": "bold",
56
+ "strong": "bold",
57
+ "em": "italic",
58
+ "i": "italic",
59
+ "u": "underline",
60
+ "s": "strikethrough",
61
+ "sup": "superscript",
62
+ "sub": "subscript",
63
+ }
64
+
65
+
66
+ def _strip_embedded_images(markup: str) -> str:
67
+ """移除自定义图片 renderer 接管的 HTML 图片,并识别清理后的空内容。"""
68
+ soup = BeautifulSoup(markup, "html.parser")
69
+ images = soup.find_all("img")
70
+ if not images:
71
+ return markup
72
+ for image in images:
73
+ image.decompose()
74
+ if not soup.get_text(strip=True):
75
+ return ""
76
+ return str(soup)
77
+
78
+
79
+ def render_html_table(
80
+ content: str,
81
+ *,
82
+ asset_base_url: str,
83
+ delimiters: LatexDelimitersConfig,
84
+ ) -> str | None:
85
+ """将 HTML table 按复杂度输出原 HTML 或转换为 GFM 表格。"""
86
+ prefixed = prefix_html_image_sources(content, asset_base_url)
87
+ soup = BeautifulSoup(prefixed, "html.parser")
88
+ tables = soup.find_all("table")
89
+ if not tables:
90
+ return None
91
+ table = tables[0]
92
+ if len(tables) != 1 or _is_complex_table(table):
93
+ return format_embedded_html(prefixed, asset_base_url="", delimiters=delimiters).strip()
94
+ markdown = _convert_simple_table(table, delimiters)
95
+ if markdown is not None:
96
+ return markdown
97
+ return format_embedded_html(prefixed, asset_base_url="", delimiters=delimiters).strip()
98
+
99
+
100
+ def format_embedded_html(
101
+ markup: str,
102
+ *,
103
+ asset_base_url: str,
104
+ delimiters: LatexDelimitersConfig,
105
+ ) -> str:
106
+ """统一处理嵌入 HTML 的图片地址与行内公式标签。"""
107
+ prefixed = prefix_html_image_sources(markup, asset_base_url)
108
+
109
+ def _replace_inline_equation(match: re.Match[str]) -> str:
110
+ """把单个 HTML eq 标签替换为配置的行内公式定界符。"""
111
+ return f" {delimiters.inline.left}{html.unescape(match.group('latex')).strip()}{delimiters.inline.right} "
112
+
113
+ return _INLINE_EQ_RE.sub(_replace_inline_equation, prefixed)
114
+
115
+
116
+ def _is_complex_table(table: Tag) -> bool:
117
+ """判断表格是否包含 GFM 无法无损表达的结构。"""
118
+ if table.find("table") is not None:
119
+ return True
120
+ thead = table.find("thead")
121
+ if thead is not None and len(thead.find_all("tr", recursive=False)) > 1:
122
+ return True
123
+ for cell in table.find_all(("th", "td")):
124
+ if cell.has_attr("rowspan") or cell.has_attr("colspan"):
125
+ return True
126
+ if len(cell.find_all("p")) > 1:
127
+ return True
128
+ for descendant in cell.descendants:
129
+ if not isinstance(descendant, Tag):
130
+ continue
131
+ if descendant.name in _COMPLEX_CELL_TAGS:
132
+ return True
133
+ if descendant.name not in _ALLOWED_INLINE_TAGS:
134
+ return True
135
+ if descendant.name == "span" and descendant.attrs:
136
+ return True
137
+ return False
138
+
139
+
140
+ def _convert_simple_table(table: Tag, delimiters: LatexDelimitersConfig) -> str | None:
141
+ """把已确认简单的单层 HTML table 转换为 GFM。"""
142
+ rows = table.find_all("tr")
143
+ if not rows:
144
+ return None
145
+ rendered_rows: list[list[str]] = []
146
+ header_flags: list[list[bool]] = []
147
+ for row in rows:
148
+ cells = row.find_all(("th", "td"), recursive=False)
149
+ if not cells:
150
+ return None
151
+ rendered_rows.append([_normalize_cell_text(_render_inline_children(cell, delimiters)) for cell in cells])
152
+ header_flags.append([cell.name == "th" for cell in cells])
153
+
154
+ width = max(len(row) for row in rendered_rows)
155
+ for row in rendered_rows:
156
+ row.extend([""] * (width - len(row)))
157
+ header_index = _detect_header_index(table, header_flags)
158
+ header = rendered_rows[header_index]
159
+ body = rendered_rows[:header_index] + rendered_rows[header_index + 1 :]
160
+ return "\n".join(
161
+ [
162
+ _format_markdown_row(header),
163
+ _format_markdown_row(["---"] * width),
164
+ *[_format_markdown_row(row) for row in body],
165
+ ]
166
+ )
167
+
168
+
169
+ def _detect_header_index(table: Tag, header_flags: list[list[bool]]) -> int:
170
+ """优先选择 thead 或全 th 行,否则使用首行作为 GFM 表头。"""
171
+ thead = table.find("thead")
172
+ if thead is not None and thead.find("tr", recursive=False) is not None:
173
+ return 0
174
+ for index, flags in enumerate(header_flags):
175
+ if flags and all(flags):
176
+ return index
177
+ return 0
178
+
179
+
180
+ def _render_inline_children(
181
+ node: Tag,
182
+ delimiters: LatexDelimitersConfig,
183
+ inherited_styles: tuple[str, ...] = (),
184
+ ) -> str:
185
+ """递归渲染简单单元格中的安全行内 HTML。"""
186
+ parts: list[str] = []
187
+ for child in node.children:
188
+ if isinstance(child, NavigableString):
189
+ escaped = _escape_cell_text(str(child))
190
+ parts.append(render_styled_markdown_text(escaped, inherited_styles))
191
+ continue
192
+ if not isinstance(child, Tag):
193
+ continue
194
+ name = child.name
195
+ style = _STYLE_TAGS.get(name)
196
+ styles = tuple(dict.fromkeys((*inherited_styles, style))) if style is not None else inherited_styles
197
+ rendered = _render_inline_children(child, delimiters, styles)
198
+ if name in {"p", "span"}:
199
+ parts.append(rendered)
200
+ elif name == "br":
201
+ parts.append("<br>")
202
+ elif name == "code":
203
+ parts.append(_render_inline_code(child.get_text()))
204
+ elif name == "eq":
205
+ latex = html.unescape(child.get_text()).strip()
206
+ escaped_latex = _escape_gfm_formula_pipes(latex)
207
+ parts.append(f"{delimiters.inline.left}{escaped_latex}{delimiters.inline.right}" if escaped_latex else "")
208
+ elif name == "a":
209
+ href = str(child.get("href", "")).strip()
210
+ if not href:
211
+ parts.append(rendered)
212
+ elif _node_has_complex_text_style(child, inherited_styles):
213
+ parts.append(f'<a href="{html.escape(href, quote=True)}">{rendered}</a>')
214
+ else:
215
+ parts.append(f"[{rendered}]({_escape_link_url(href)})")
216
+ elif name in _STYLE_TAGS:
217
+ parts.append(rendered)
218
+ else:
219
+ parts.append(rendered)
220
+ return "".join(parts)
221
+
222
+
223
+ def _node_has_complex_text_style(node: Tag, inherited_styles: tuple[str, ...]) -> bool:
224
+ """判断节点后代是否含必须用 HTML 表达的有效文字样式组合。"""
225
+ for child in node.children:
226
+ if isinstance(child, NavigableString):
227
+ if str(child) and markdown_styles_require_html(inherited_styles):
228
+ return True
229
+ continue
230
+ if not isinstance(child, Tag):
231
+ continue
232
+ style = _STYLE_TAGS.get(child.name)
233
+ styles = tuple(dict.fromkeys((*inherited_styles, style))) if style is not None else inherited_styles
234
+ if _node_has_complex_text_style(child, styles):
235
+ return True
236
+ return False
237
+
238
+
239
+ def _render_inline_code(content: str) -> str:
240
+ """使用足够长的反引号包装表格单元格内代码。"""
241
+ longest = max((len(match.group(0)) for match in re.finditer(r"`+", content)), default=0)
242
+ fence = "`" * max(1, longest + 1)
243
+ return f"{fence}{_escape_cell_text(content)}{fence}"
244
+
245
+
246
+ def _escape_gfm_formula_pipes(latex: str) -> str:
247
+ """转义公式竖线,并保证 GFM 解析后恢复原始反斜杠数量。"""
248
+
249
+ def _replace(match: re.Match[str]) -> str:
250
+ """把竖线前 n 个反斜杠扩展为 2n+1 个 Markdown 反斜杠。"""
251
+ slash_count = len(match.group("slashes"))
252
+ return "\\" * (2 * slash_count + 1) + "|"
253
+
254
+ return _GFM_FORMULA_PIPE_RE.sub(_replace, latex)
255
+
256
+
257
+ def _escape_link_url(url: str) -> str:
258
+ """转义 GFM 表格链接目标中的空格、反斜杠和括号。"""
259
+ return url.replace("\\", "%5C").replace(" ", "%20").replace("(", "%28").replace(")", "%29").replace("|", "%7C")
260
+
261
+
262
+ def _escape_cell_text(content: str) -> str:
263
+ """转义 GFM 单元格中的 HTML、反斜杠与管道符,避免源文本注入活动标签。"""
264
+ escaped_html = content.replace("&", "&amp;").replace("<", "&lt;")
265
+ return escaped_html.replace("\\", "\\\\").replace("|", r"\|")
266
+
267
+
268
+ def _normalize_cell_text(content: str) -> str:
269
+ """压缩普通空白,同时保留显式 br 换行。"""
270
+ content = re.sub(r"[ \t\r\f\v]+", " ", content)
271
+ content = re.sub(r" *\n+ *", " ", content)
272
+ content = re.sub(r" *<br> *", "<br>", content)
273
+ return content.strip()
274
+
275
+
276
+ def _format_markdown_row(row: list[str]) -> str:
277
+ """把一行单元格格式化为 GFM 行。"""
278
+ return f"| {' | '.join(row)} |"
279
+
280
+
281
+ __all__ = ["format_embedded_html", "render_html_table"]
@@ -0,0 +1,3 @@
1
+ """PDF 私有渲染实现。"""
2
+
3
+ __all__: list[str] = []