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,1041 @@
1
+ """严格 MiddleJson 到兼容 HTML 的公共渲染实现。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import re
7
+ from functools import lru_cache
8
+ from importlib import resources
9
+ from urllib.parse import quote
10
+
11
+ from bs4 import BeautifulSoup
12
+ from loguru import logger
13
+
14
+ from ..common.index import strip_index_page_tail
15
+ from ....content.inline import inline_plain_text, normalize_inline_spans
16
+ from ....codecs.html import DOCVORTEX_HTML_VERSION
17
+ from ....codecs.html.contracts import (
18
+ WIRE_BLOCK_CLASS,
19
+ WIRE_DOCUMENT_CLASS,
20
+ WIRE_INDEX_CLASS,
21
+ WIRE_LIST_CONTENT_CLASS,
22
+ WIRE_LIST_MARKER_CLASS,
23
+ WIRE_PAGE_BREAK_CLASS,
24
+ WIRE_PAGE_CLASS,
25
+ WIRE_VISUAL_BODY_CLASS,
26
+ )
27
+ from ..common.list_items import ListItem, parse_list_item_marker, reference_list_needs_bullets
28
+ from ..common.planner import PlannedBlock, build_render_plan
29
+ from .inline import (
30
+ HtmlInlineResult,
31
+ render_inline_content_html,
32
+ render_inline_spans_html,
33
+ render_joined_inline_contents_html,
34
+ render_math_html,
35
+ )
36
+ from .sanitizer import is_supported_html_markup, sanitize_html_fragment, sanitize_image_source
37
+ from .table import looks_like_gfm_table, render_gfm_table_html
38
+ from ...contracts import RenderMode
39
+ from ....schema import (
40
+ PAGE_AUXILIARY_BLOCK_TYPES,
41
+ RAW_ALGORITHM,
42
+ AlgorithmBodyBlock,
43
+ BlockBase,
44
+ BlockType,
45
+ ChartAnnotationBlock,
46
+ ChartBlock,
47
+ ChartBodyBlock,
48
+ CodeAnnotationBlock,
49
+ CodeBlock,
50
+ CodeBodyBlock,
51
+ DocTitleBlock,
52
+ EquationBlock,
53
+ ImageAnnotationBlock,
54
+ ImageBlock,
55
+ ImageBodyBlock,
56
+ InlineSpan,
57
+ IndexBlock,
58
+ ListBlock,
59
+ MiddleJson,
60
+ PageAuxTextBlock,
61
+ PageFootnoteBlock,
62
+ ParagraphTitleBlock,
63
+ RefTextBlock,
64
+ TableAnnotationBlock,
65
+ TableBlock,
66
+ TableBodyBlock,
67
+ TextBlock,
68
+ TextSpan,
69
+ TitleBlockBase,
70
+ )
71
+
72
+ _STYLE_RESOURCE_NAME = "docvortex.min.css"
73
+ _MATHJAX_URL = "https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-chtml.js"
74
+ _MATHJAX_INTEGRITY = "sha384-zAhQQhdaMeHsMProNntGGg6nOUVcfuF9F22C3d1qJ9NZAVzCplXk1X85D2O5iufn"
75
+ _PRISM_CORE_URL = "https://cdn.jsdelivr.net/npm/prismjs@1.30.0/components/prism-core.min.js"
76
+ _PRISM_CORE_INTEGRITY = "sha384-zLRFO4dwowZvh8kzutOb5AWhH7f39HeJp+N7PtHF1SQtTBnifRx0AtmvTYs3F4YV"
77
+ _PRISM_AUTOLOADER_URL = "https://cdn.jsdelivr.net/npm/prismjs@1.30.0/plugins/autoloader/prism-autoloader.min.js"
78
+ _PRISM_AUTOLOADER_INTEGRITY = "sha384-Uq05+JLko69eOiPr39ta9bh7kld5PKZoU+fF7g0EXTAriEollhZ+DrN8Q/Oi8J2Q"
79
+ _PRISM_LANGUAGES_PATH = "https://cdn.jsdelivr.net/npm/prismjs@1.30.0/components/"
80
+ _MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@11.16.1/dist/mermaid.min.js"
81
+ _MERMAID_INTEGRITY = "sha384-aBQXj4hK6Jm05i7aQAsUV3bLdSUrHX1BGYfMB0166TtWt/RRaw+h0Eelme9OCOvy"
82
+ _MERMAID_MAX_TEXT_SIZE = 50_000
83
+ _MERMAID_FLOWCHART_HEADER_RE = re.compile(
84
+ r"(?:graph|flowchart)\s+(?:TB|TD|BT|RL|LR)\b",
85
+ re.IGNORECASE,
86
+ )
87
+ _SAFE_PRISM_LANGUAGE_RE = re.compile(r"[a-z0-9][a-z0-9-]{0,31}")
88
+ _BLOCKED_PRISM_LANGUAGE_NAMES = {"constructor", "prototype"}
89
+ _PRISM_LANGUAGE_ALIASES = {
90
+ "c#": "csharp",
91
+ "c++": "cpp",
92
+ "html": "markup",
93
+ "js": "javascript",
94
+ "py": "python",
95
+ "sh": "bash",
96
+ "shell": "bash",
97
+ "none": "",
98
+ "text": "",
99
+ "txt": "",
100
+ "ts": "typescript",
101
+ "xml": "markup",
102
+ "yml": "yaml",
103
+ }
104
+
105
+
106
+ class _HtmlRenderer:
107
+ """维护一次 HTML 渲染所需的 anchor、公式与代码资源状态。"""
108
+
109
+ def __init__(
110
+ self,
111
+ middle_json: MiddleJson,
112
+ *,
113
+ mode: RenderMode,
114
+ asset_base_url: str,
115
+ standalone: bool,
116
+ document_title: str | None,
117
+ ) -> None:
118
+ """保存严格输入和调用选项,并预收集真实正文标题 anchor。"""
119
+ self.middle_json = middle_json
120
+ self.mode = mode
121
+ self.asset_base_url = asset_base_url
122
+ self.standalone = standalone
123
+ self.document_title = document_title
124
+ self.anchor_targets = _collect_document_anchor_ids(middle_json)
125
+ self.emitted_anchors: set[str] = set()
126
+ self.has_math = False
127
+ self.has_prism = False
128
+ self.has_mermaid = False
129
+
130
+ def render(self) -> str:
131
+ """生成 fragment article,并按需包装完整 HTML 文档。"""
132
+ planned_pages = build_render_plan(self.middle_json, self.mode)
133
+ if self.mode is RenderMode.FULL:
134
+ body = self._render_full_pages(planned_pages)
135
+ else:
136
+ body = self._render_default_pages(planned_pages)
137
+ article = (
138
+ f'<article class="{WIRE_DOCUMENT_CLASS} {WIRE_DOCUMENT_CLASS}--{self.mode.value}" '
139
+ f'data-docvortex-html-version="{DOCVORTEX_HTML_VERSION}" data-render-mode="{self.mode.value}">\n{body}\n</article>'
140
+ )
141
+ if not self.standalone:
142
+ return article
143
+ # 最终依赖以真实 DOM 为准,避免回退分支或普通文本中的 class 字样误触发。
144
+ article_soup = BeautifulSoup(article, "html.parser")
145
+ self.has_math = article_soup.select_one(".docvortex-math") is not None
146
+ self.has_prism = any(
147
+ any(str(class_name).startswith("language-") for class_name in code.get("class", []))
148
+ for code in article_soup.find_all("code")
149
+ if code.get_text()
150
+ )
151
+ self.has_mermaid = article_soup.select_one(".docvortex-flowchart") is not None
152
+ return self._render_standalone(article)
153
+
154
+ def _render_default_pages(self, planned_pages: list[list[PlannedBlock]]) -> str:
155
+ """把 DEFAULT 计划展平为无页面边界的连续阅读内容。"""
156
+ rendered: list[str] = []
157
+ for page in planned_pages:
158
+ for planned in page:
159
+ block = self._render_planned_block(planned)
160
+ if block:
161
+ rendered.append(block)
162
+ return "\n".join(rendered)
163
+
164
+ def _render_full_pages(self, planned_pages: list[list[PlannedBlock]]) -> str:
165
+ """为 FULL 计划保留所有页面 section 和相邻页面分隔。"""
166
+ sections: list[str] = []
167
+ for page in planned_pages:
168
+ page_idx = page[0].page_idx if page else None
169
+ if page_idx is None:
170
+ page_position = len(sections)
171
+ page_idx = self.middle_json.pages[page_position].page_idx
172
+ blocks = [rendered for planned in page if (rendered := self._render_planned_block(planned))]
173
+ rendered_blocks = "\n".join(blocks)
174
+ sections.append(f'<section class="{WIRE_PAGE_CLASS}" data-page-idx="{page_idx}">\n{rendered_blocks}\n</section>')
175
+ return f'\n<hr class="{WIRE_PAGE_BREAK_CLASS}" aria-hidden="true">\n'.join(sections)
176
+
177
+ def _render_planned_block(self, planned: PlannedBlock) -> str:
178
+ """过滤计划块、分派类型 visitor,并添加稳定来源元数据。"""
179
+ if planned.removed:
180
+ return ""
181
+ block = planned.block
182
+ if self.mode is RenderMode.DEFAULT and block.type in PAGE_AUXILIARY_BLOCK_TYPES:
183
+ return ""
184
+ content = self._render_block_content(planned)
185
+ if not content or not content.strip():
186
+ return ""
187
+ attrs = [
188
+ f'class="{WIRE_BLOCK_CLASS}"',
189
+ f'data-page-idx="{planned.page_idx}"',
190
+ ]
191
+ attrs.extend(_wire_block_attributes(block))
192
+ return f"<div {' '.join(attrs)}>\n{content}\n</div>"
193
+
194
+ def _render_block_content(self, planned: PlannedBlock) -> str:
195
+ """按严格 PageBlock 具体类型返回对应语义 HTML。"""
196
+ block = planned.block
197
+ if isinstance(block, TextBlock):
198
+ rendered = render_joined_inline_contents_html(planned.text_contents or [block.content])
199
+ self._observe_inline(rendered)
200
+ if not rendered.html:
201
+ return ""
202
+ attrs = ['class="docvortex-text"']
203
+ self._append_anchor_attribute(attrs, block.anchor, block_type=str(block.type))
204
+ return f"<p {' '.join(attrs)}>{rendered.html}</p>"
205
+ if isinstance(block, RefTextBlock):
206
+ rendered = render_joined_inline_contents_html(planned.text_contents or [block.content])
207
+ self._observe_inline(rendered)
208
+ return f'<p class="docvortex-ref-text">{rendered.html}</p>' if rendered.html else ""
209
+ if isinstance(block, (DocTitleBlock, ParagraphTitleBlock)):
210
+ return self._render_title(block)
211
+ if isinstance(block, PageFootnoteBlock):
212
+ rendered = render_inline_content_html(block.content)
213
+ self._observe_inline(rendered)
214
+ if not rendered.html:
215
+ return ""
216
+ attrs = ['class="docvortex-page-footnote"', 'data-block-type="page_footnote"']
217
+ self._append_anchor_attribute(attrs, block.anchor, block_type=str(block.type))
218
+ return f"<div {' '.join(attrs)}>{rendered.html}</div>"
219
+ if isinstance(block, PageAuxTextBlock):
220
+ rendered = render_inline_content_html(block.content)
221
+ self._observe_inline(rendered)
222
+ if not rendered.html:
223
+ return ""
224
+ class_name = html.escape(str(block.type).replace("_", "-"), quote=True)
225
+ attrs = [f'class="docvortex-page-aux docvortex-page-aux--{class_name}"']
226
+ return f"<div {' '.join(attrs)}>{rendered.html}</div>"
227
+ if isinstance(block, EquationBlock):
228
+ return self._render_equation(block)
229
+ if isinstance(block, ListBlock):
230
+ return self._render_list(block)
231
+ if isinstance(block, IndexBlock):
232
+ return self._render_index(block)
233
+ if isinstance(block, ImageBlock):
234
+ return self._render_image_block(block)
235
+ if isinstance(block, TableBlock):
236
+ return self._render_table_block(block)
237
+ if isinstance(block, ChartBlock):
238
+ return self._render_chart_block(block)
239
+ if isinstance(block, CodeBlock):
240
+ return self._render_code_block(block)
241
+ raise TypeError(f"Unsupported PageBlock type: {type(block).__name__}")
242
+
243
+ def _render_title(self, block: DocTitleBlock | ParagraphTitleBlock) -> str:
244
+ """渲染标题,并保证正文 anchor 只在首次出现时生成 id。"""
245
+ rendered = render_inline_content_html(block.content)
246
+ self._observe_inline(rendered)
247
+ if not rendered.html:
248
+ return ""
249
+ level = min(max(block.level, 1), 6)
250
+ attrs = [f'class="docvortex-heading docvortex-heading--{level}"', f'data-heading-level="{block.level}"']
251
+ self._append_anchor_attribute(attrs, block.anchor, block_type=str(block.type))
252
+ return f"<h{level} {' '.join(attrs)}>{rendered.html}</h{level}>"
253
+
254
+ def _append_anchor_attribute(
255
+ self,
256
+ attrs: list[str],
257
+ anchor: str | None,
258
+ *,
259
+ block_type: str,
260
+ ) -> None:
261
+ """给标题或页面脚注追加唯一 HTML id,重复目标只保留首次输出。"""
262
+ normalized = _anchor_key(anchor)
263
+ if not normalized:
264
+ return
265
+ if normalized in self.emitted_anchors:
266
+ logger.warning("Duplicate HTML {} anchor omitted: {!r}", block_type, normalized)
267
+ return
268
+ anchor_id = self.anchor_targets.get(normalized)
269
+ if anchor_id:
270
+ attrs.append(f'id="{html.escape(anchor_id, quote=True)}"')
271
+ self.emitted_anchors.add(normalized)
272
+
273
+ def _render_equation(self, block: EquationBlock) -> str:
274
+ """优先输出行间公式,空 LaTeX 时回退到安全图片来源。"""
275
+ if block.content.strip():
276
+ rendered = render_math_html(block.content, display=True)
277
+ self._observe_inline(rendered)
278
+ return rendered.html
279
+ source = self._safe_block_image_source(block)
280
+ return self._render_image(source, alt="formula", class_name="docvortex-equation-image") if source else ""
281
+
282
+ def _render_list(self, block: ListBlock) -> str:
283
+ """按直属 marker 类型渲染一层列表,并递归挂接嵌套列表。"""
284
+ parsed_leaves = [
285
+ parse_list_item_marker(child.content)
286
+ for child in block.content
287
+ if not isinstance(child, ListBlock) and inline_plain_text(child.content).strip()
288
+ ]
289
+ add_reference_bullets = reference_list_needs_bullets(block)
290
+ container_tag, list_type, class_name = _classify_list(parsed_leaves, add_reference_bullets)
291
+ items: list[dict[str, object]] = []
292
+ expected_value: int | None = None
293
+
294
+ for child in block.content:
295
+ if isinstance(child, ListBlock):
296
+ nested = self._render_list(child)
297
+ if not nested:
298
+ continue
299
+ if not items:
300
+ items.append({"content": "", "attrs": ['class="docvortex-list-item--orphan"'], "nested": []})
301
+ nested_items = items[-1]["nested"]
302
+ assert isinstance(nested_items, list)
303
+ nested_items.append(nested)
304
+ continue
305
+
306
+ parsed = parse_list_item_marker(child.content)
307
+ item_content, marker = _list_item_content(
308
+ parsed,
309
+ add_reference_bullets,
310
+ explicit_markers=class_name == "docvortex-list--explicit",
311
+ )
312
+ rendered = render_inline_content_html(item_content)
313
+ self._observe_inline(rendered)
314
+ if not rendered.html and not marker:
315
+ items.append(
316
+ {
317
+ "content": "",
318
+ "attrs": ['class="docvortex-list-item--markerless"', *_wire_block_attributes(child)],
319
+ "nested": [],
320
+ }
321
+ )
322
+ continue
323
+
324
+ attrs = _wire_block_attributes(child)
325
+ if class_name == "docvortex-list--explicit":
326
+ attrs.append('class="docvortex-list-item--explicit"')
327
+ if container_tag == "ol" and parsed.kind == "ordered" and parsed.value is not None:
328
+ if expected_value is None:
329
+ expected_value = parsed.value
330
+ if parsed.value != expected_value:
331
+ attrs.append(f'value="{parsed.value}"')
332
+ expected_value = parsed.value + 1
333
+ marker_html = (
334
+ f'<span class="{WIRE_LIST_MARKER_CLASS}">{html.escape(_replace_html_controls(marker), quote=False)}</span>'
335
+ if marker
336
+ else ""
337
+ )
338
+ if class_name == "docvortex-list--explicit" and not marker_html:
339
+ marker_html = f'<span class="{WIRE_LIST_MARKER_CLASS}"></span>'
340
+ items.append(
341
+ {
342
+ "content": f'{marker_html}<span class="{WIRE_LIST_CONTENT_CLASS}">{rendered.html}</span>',
343
+ "attrs": attrs,
344
+ "nested": [],
345
+ }
346
+ )
347
+
348
+ rendered_items: list[str] = []
349
+ for item in items:
350
+ nested = item["nested"]
351
+ assert isinstance(nested, list)
352
+ item_content = str(item["content"])
353
+ if not item_content and not nested:
354
+ continue
355
+ item_attrs = item["attrs"]
356
+ assert isinstance(item_attrs, list)
357
+ attrs_html = f" {' '.join(str(value) for value in item_attrs)}" if item_attrs else ""
358
+ rendered_items.append(f"<li{attrs_html}>{item_content}{''.join(str(value) for value in nested)}</li>")
359
+ if not rendered_items:
360
+ return ""
361
+
362
+ container_attrs = [f'class="docvortex-list {class_name}"', *_wire_block_attributes(block)]
363
+ if list_type:
364
+ container_attrs.append(f'type="{list_type}"')
365
+ if container_tag == "ol" and parsed_leaves and parsed_leaves[0].value not in (None, 1):
366
+ container_attrs.append(f'start="{parsed_leaves[0].value}"')
367
+ return f"<{container_tag} {' '.join(container_attrs)}>{''.join(rendered_items)}</{container_tag}>"
368
+
369
+ def _render_index(self, block: IndexBlock) -> str:
370
+ """递归渲染目录,并仅链接到真实正文标题 anchor。"""
371
+ items: list[dict[str, object]] = []
372
+ for child in block.content:
373
+ if isinstance(child, IndexBlock):
374
+ nested = self._render_index_list(child)
375
+ if not nested:
376
+ continue
377
+ if not items:
378
+ items.append({"content": "", "attrs": ['class="docvortex-list-item--orphan"'], "nested": []})
379
+ nested_items = items[-1]["nested"]
380
+ assert isinstance(nested_items, list)
381
+ nested_items.append(nested)
382
+ continue
383
+ rendered, attrs = self._render_index_leaf(child)
384
+ items.append({"content": rendered, "attrs": attrs, "nested": []})
385
+ inner = _serialize_index_items(items)
386
+ attrs = [f'class="{WIRE_INDEX_CLASS}"', 'aria-label="Table of contents"', *_wire_block_attributes(block)]
387
+ return f"<nav {' '.join(attrs)}><ul>{inner}</ul></nav>" if inner else ""
388
+
389
+ def _render_index_list(self, block: IndexBlock) -> str:
390
+ """渲染一个嵌套 IndexBlock 为可挂接到父项的 ul。"""
391
+ items: list[dict[str, object]] = []
392
+ for child in block.content:
393
+ if isinstance(child, IndexBlock):
394
+ nested = self._render_index_list(child)
395
+ if nested:
396
+ if not items:
397
+ items.append({"content": "", "attrs": ['class="docvortex-list-item--orphan"'], "nested": []})
398
+ nested_items = items[-1]["nested"]
399
+ assert isinstance(nested_items, list)
400
+ nested_items.append(nested)
401
+ continue
402
+ rendered, attrs = self._render_index_leaf(child)
403
+ items.append({"content": rendered, "attrs": attrs, "nested": []})
404
+ inner = _serialize_index_items(items)
405
+ attrs = _wire_block_attributes(block)
406
+ return f"<ul {' '.join(attrs)}>{inner}</ul>" if inner else ""
407
+
408
+ def _render_index_leaf(self, block: TextBlock | TitleBlockBase) -> tuple[str, list[str]]:
409
+ """渲染目录叶子,并在 anchor 命中正文标题时生成内部链接。"""
410
+ content = strip_index_page_tail(block.content)
411
+ rendered = render_inline_content_html(content)
412
+ self._observe_inline(rendered)
413
+ if not rendered.html:
414
+ return "", _wire_block_attributes(block)
415
+ anchor = _anchor_key(block.anchor)
416
+ if anchor and (anchor_id := self.anchor_targets.get(anchor)):
417
+ href = quote(anchor_id, safe="-._~")
418
+ return f'<a href="#{href}">{rendered.html}</a>', _wire_block_attributes(block)
419
+ return rendered.html, _wire_block_attributes(block)
420
+
421
+ def _render_image_block(self, block: ImageBlock) -> str:
422
+ """按原始子块顺序渲染图片主体及重复说明。"""
423
+ parts: list[str] = []
424
+ for child in block.content:
425
+ if isinstance(child, ImageBodyBlock):
426
+ body = self._render_image_body(block, child)
427
+ elif isinstance(child, ImageAnnotationBlock):
428
+ body = self._render_annotation(child)
429
+ else:
430
+ raise TypeError(f"Unsupported image child: {type(child).__name__}")
431
+ if body:
432
+ parts.append(body)
433
+ return f'<figure class="docvortex-figure docvortex-figure--image">{"".join(parts)}</figure>' if parts else ""
434
+
435
+ def _render_image_body(self, parent: ImageBlock, block: ImageBodyBlock) -> str:
436
+ """渲染图片,并把并存的识别内容放入原生 details。"""
437
+ if parent.sub_type == "flowchart":
438
+ mermaid_source = _extract_mermaid_flowchart_source(block.content)
439
+ if mermaid_source is not None:
440
+ rendered = self._render_flowchart_body(block, mermaid_source)
441
+ return _wrap_visual_body(rendered, block, "image")
442
+ source = self._safe_block_image_source(block)
443
+ content = self._render_embedded_content(block.content, linkify_text=parent.sub_type != "flowchart")
444
+ alt = _plain_content_text(block.content) or parent.sub_type or "image"
445
+ parts = [self._render_image(source, alt=alt, class_name="docvortex-image")] if source else []
446
+ if content.html:
447
+ if source:
448
+ parts.append(self._render_details(content.html, parent.sub_type or "image content"))
449
+ else:
450
+ parts.append(content.html)
451
+ return _wrap_visual_body("".join(parts), block, "image")
452
+
453
+ def _render_flowchart_body(self, block: ImageBodyBlock, mermaid_source: str) -> str:
454
+ """输出 Mermaid canvas,并保留 raster 与源码两级失败回退。"""
455
+ source = self._safe_block_image_source(block)
456
+ escaped_source = html.escape(_replace_html_controls(mermaid_source), quote=False)
457
+ fallback = self._render_image(source, alt="flowchart", class_name="docvortex-flowchart-fallback") if source else ""
458
+ fallback_class = " docvortex-flowchart--has-raster" if fallback else ""
459
+ details_open = "" if fallback else " open"
460
+ return (
461
+ f'<div class="docvortex-flowchart{fallback_class}" data-mermaid-state="pending">'
462
+ '<div class="docvortex-flowchart-canvas" role="img" aria-label="flowchart"></div>'
463
+ f"{fallback}</div>"
464
+ f'<details class="docvortex-details docvortex-flowchart-details"{details_open}>'
465
+ "<summary>flowchart source</summary>"
466
+ f'<pre class="docvortex-flowchart-source"><code>{escaped_source}</code></pre>'
467
+ "</details>"
468
+ )
469
+
470
+ def _render_table_block(self, block: TableBlock) -> str:
471
+ """按原始子块顺序渲染表格主体、标题与脚注。"""
472
+ parts: list[str] = []
473
+ for child in block.content:
474
+ if isinstance(child, TableBodyBlock):
475
+ body = self._render_table_body(child)
476
+ elif isinstance(child, TableAnnotationBlock):
477
+ body = self._render_annotation(child)
478
+ else:
479
+ raise TypeError(f"Unsupported table child: {type(child).__name__}")
480
+ if body:
481
+ parts.append(body)
482
+ return f'<figure class="docvortex-figure docvortex-figure--table">{"".join(parts)}</figure>' if parts else ""
483
+
484
+ def _render_table_body(self, block: TableBodyBlock) -> str:
485
+ """优先保留安全 HTML table,空间文本和整体图片依次回退。"""
486
+ normalized_content = _replace_html_controls(block.content)
487
+ content = normalized_content.strip()
488
+ if content:
489
+ source_soup = BeautifulSoup(content, "html.parser")
490
+ if source_soup.find("table") is not None:
491
+ rendered = self._render_embedded_content(content)
492
+ if rendered.html and _contains_usable_table(rendered.html):
493
+ return _wrap_visual_body(rendered.html, block, "table")
494
+ source = self._safe_block_image_source(block)
495
+ if source:
496
+ rendered_image = self._render_image(source, alt="table", class_name="docvortex-table-image")
497
+ return _wrap_visual_body(rendered_image, block, "table")
498
+ return _wrap_visual_body(_render_raw_fallback(content), block, "table")
499
+ rendered_text = f'<pre class="docvortex-table-text">{html.escape(normalized_content, quote=False)}</pre>'
500
+ return _wrap_visual_body(rendered_text, block, "table")
501
+ source = self._safe_block_image_source(block)
502
+ rendered_image = self._render_image(source, alt="table", class_name="docvortex-table-image") if source else ""
503
+ return _wrap_visual_body(rendered_image, block, "table")
504
+
505
+ def _render_chart_block(self, block: ChartBlock) -> str:
506
+ """按原始子块顺序渲染 chart 图片、结构内容及说明。"""
507
+ parts: list[str] = []
508
+ for child in block.content:
509
+ if isinstance(child, ChartBodyBlock):
510
+ body = self._render_chart_body(block, child)
511
+ elif isinstance(child, ChartAnnotationBlock):
512
+ body = self._render_annotation(child)
513
+ else:
514
+ raise TypeError(f"Unsupported chart child: {type(child).__name__}")
515
+ if body:
516
+ parts.append(body)
517
+ return f'<figure class="docvortex-figure docvortex-figure--chart">{"".join(parts)}</figure>' if parts else ""
518
+
519
+ def _render_chart_body(self, parent: ChartBlock, block: ChartBodyBlock) -> str:
520
+ """渲染 chart 图片,并将并存结构内容放入 details。"""
521
+ source = self._safe_block_image_source(block)
522
+ content = self._render_chart_content(block.content)
523
+ parts = (
524
+ [self._render_image(source, alt=parent.sub_type or "chart", class_name="docvortex-chart-image")] if source else []
525
+ )
526
+ if content.html:
527
+ if source:
528
+ parts.append(self._render_details(content.html, parent.sub_type or "chart content"))
529
+ else:
530
+ parts.append(content.html)
531
+ return _wrap_visual_body("".join(parts), block, "chart")
532
+
533
+ def _render_chart_content(self, content: str) -> HtmlInlineResult:
534
+ """按 HTML、严格 GFM 表格、普通行内内容的顺序渲染 chart content。"""
535
+ normalized = content.strip()
536
+ if not normalized:
537
+ return HtmlInlineResult("")
538
+ if is_supported_html_markup(normalized):
539
+ return self._render_embedded_content(normalized)
540
+ if looks_like_gfm_table(normalized):
541
+ rendered = render_gfm_table_html(normalized)
542
+ if rendered is None:
543
+ return HtmlInlineResult(_render_raw_fallback(content))
544
+ self._observe_inline(rendered)
545
+ return rendered
546
+ rendered = render_inline_content_html([TextSpan(type="text", content=content)])
547
+ self._observe_inline(rendered)
548
+ return rendered
549
+
550
+ def _render_code_block(self, block: CodeBlock) -> str:
551
+ """按原始子块顺序渲染代码或算法及其说明。"""
552
+ parts: list[str] = []
553
+ for child in block.content:
554
+ if isinstance(child, (CodeBodyBlock, AlgorithmBodyBlock)):
555
+ body = self._render_code_body(block, child)
556
+ elif isinstance(child, CodeAnnotationBlock):
557
+ body = self._render_annotation(child)
558
+ else:
559
+ raise TypeError(f"Unsupported code child: {type(child).__name__}")
560
+ if body:
561
+ parts.append(body)
562
+ return f'<figure class="docvortex-figure docvortex-figure--code">{"".join(parts)}</figure>' if parts else ""
563
+
564
+ def _render_code_body(self, parent: CodeBlock, block: CodeBodyBlock | AlgorithmBodyBlock) -> str:
565
+ """代码使用 Prism class,算法直接使用结构化 Span 保留空白。"""
566
+ if parent.sub_type == BlockType.CODE:
567
+ if not isinstance(block, CodeBodyBlock):
568
+ raise TypeError("code subtype requires CodeBodyBlock")
569
+ escaped = html.escape(_replace_html_controls(block.content), quote=False)
570
+ if not block.content:
571
+ return _wrap_visual_body('<pre class="docvortex-code"><code></code></pre>', block, "code")
572
+ language = _normalize_prism_language(parent.guess_lang)
573
+ class_attr = f' class="language-{language}"' if language else ""
574
+ pre_class = f"docvortex-code language-{language}" if language else "docvortex-code"
575
+ self.has_prism = bool(language) or self.has_prism
576
+ rendered = f'<pre class="{pre_class}"><code{class_attr}>{escaped}</code></pre>'
577
+ return _wrap_visual_body(rendered, block, "code")
578
+ if parent.sub_type == RAW_ALGORITHM:
579
+ if not isinstance(block, AlgorithmBodyBlock):
580
+ raise TypeError("algorithm subtype requires AlgorithmBodyBlock")
581
+ rendered = render_inline_spans_html(
582
+ block.content,
583
+ linkify_text=False,
584
+ separate_adjacent_math=True,
585
+ preserve_newlines=True,
586
+ )
587
+ self._observe_inline(rendered)
588
+ algorithm = f'<div class="docvortex-algorithm">{rendered.html}</div>' if rendered.html else ""
589
+ return _wrap_visual_body(algorithm, block, "code")
590
+ raise ValueError(f"Unsupported code subtype: {parent.sub_type}")
591
+
592
+ def _render_annotation(
593
+ self,
594
+ block: ImageAnnotationBlock | TableAnnotationBlock | ChartAnnotationBlock | CodeAnnotationBlock,
595
+ ) -> str:
596
+ """把视觉说明渲染为保持原类型的独立段落。"""
597
+ rendered = render_inline_content_html(block.content)
598
+ self._observe_inline(rendered)
599
+ if not rendered.html:
600
+ return ""
601
+ is_caption = str(block.type).endswith("caption")
602
+ role_class = "docvortex-caption" if is_caption else "docvortex-footnote"
603
+ type_class = html.escape(str(block.type).replace("_", "-"), quote=True)
604
+ attrs = [f'class="{role_class} {role_class}--{type_class}"', *_wire_block_attributes(block)]
605
+ return f"<p {' '.join(attrs)}>{rendered.html}</p>"
606
+
607
+ def _render_embedded_content(self, content: str, *, linkify_text: bool = True) -> HtmlInlineResult:
608
+ """安全处理 body 富 HTML;普通字符串始终按字面文本渲染。"""
609
+ normalized = content.strip()
610
+ if not normalized:
611
+ return HtmlInlineResult("")
612
+ if not is_supported_html_markup(normalized):
613
+ rendered = render_inline_spans_html([TextSpan(type="text", content=content)], linkify_text=linkify_text)
614
+ self._observe_inline(rendered)
615
+ return rendered
616
+ sanitized = sanitize_html_fragment(normalized, asset_base_url=self.asset_base_url)
617
+ rendered = self._replace_safe_equations(sanitized)
618
+ self._observe_inline(rendered)
619
+ return rendered
620
+
621
+ def _replace_safe_equations(self, markup: str) -> HtmlInlineResult:
622
+ """把 sanitizer 保留的 eq 标签替换为可信 MathJax carrier。"""
623
+ if not markup:
624
+ return HtmlInlineResult("")
625
+ soup = BeautifulSoup(markup, "html.parser")
626
+ has_math = False
627
+ for equation in list(soup.find_all("eq")):
628
+ rendered = render_math_html(equation.get_text(), display=False)
629
+ if not rendered.html:
630
+ equation.decompose()
631
+ continue
632
+ replacement = BeautifulSoup(rendered.html, "html.parser")
633
+ equation.replace_with(*list(replacement.contents))
634
+ has_math = True
635
+ return HtmlInlineResult(str(soup), has_math)
636
+
637
+ def _safe_block_image_source(self, block: EquationBlock | ImageBodyBlock | TableBodyBlock | ChartBodyBlock) -> str | None:
638
+ """解析 block 图片来源,并应用 HTML renderer 的 URL 安全策略。"""
639
+ if block.image_path:
640
+ return sanitize_image_source(block.image_path, asset_base_url=self.asset_base_url)
641
+ if block.image_base64:
642
+ return sanitize_image_source(block.image_base64)
643
+ if block.image_url:
644
+ return sanitize_image_source(block.image_url)
645
+ return None
646
+
647
+ def _render_image(self, source: str, *, alt: str, class_name: str) -> str:
648
+ """构造属性已转义的安全 img 元素。"""
649
+ return (
650
+ f'<img class="{class_name}" src="{html.escape(source, quote=True)}" '
651
+ f'alt="{html.escape(_replace_html_controls(alt), quote=True)}">'
652
+ )
653
+
654
+ def _render_details(self, content: str, summary: str) -> str:
655
+ """构造浏览器原生折叠详情,不引入额外脚本。"""
656
+ return (
657
+ '<details class="docvortex-details">'
658
+ f"<summary>{html.escape(_replace_html_controls(summary), quote=False)}</summary>{content}</details>"
659
+ )
660
+
661
+ def _observe_inline(self, rendered: HtmlInlineResult) -> None:
662
+ """累计当前文档是否需要加载 MathJax。"""
663
+ self.has_math = rendered.has_math or self.has_math
664
+
665
+ def _render_standalone(self, article: str) -> str:
666
+ """用固定 CSS 和按需 CDN 资源包装完整 HTML 文档。"""
667
+ title = html.escape(
668
+ _replace_html_controls(_resolve_document_title(self.middle_json, self.document_title)),
669
+ quote=False,
670
+ )
671
+ styles = _load_html_styles()
672
+ dependencies = []
673
+ if self.has_math:
674
+ dependencies.append(_mathjax_head())
675
+ if self.has_prism:
676
+ dependencies.append(_prism_head())
677
+ if self.has_mermaid:
678
+ dependencies.append(_mermaid_head())
679
+ dependency_html = "\n".join(dependencies)
680
+ return (
681
+ "<!doctype html>\n"
682
+ '<html lang="und">\n'
683
+ "<head>\n"
684
+ '<meta charset="utf-8">\n'
685
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">\n'
686
+ f"<title>{title}</title>\n"
687
+ f"<style>{styles}</style>\n"
688
+ f"{dependency_html}\n"
689
+ "</head>\n"
690
+ '<body class="docvortex-html-body">\n'
691
+ f"{article}\n"
692
+ "</body>\n"
693
+ "</html>"
694
+ )
695
+
696
+
697
+ def render_html(
698
+ middle_json: MiddleJson,
699
+ *,
700
+ mode: RenderMode = RenderMode.DEFAULT,
701
+ asset_base_url: str = "",
702
+ standalone: bool = True,
703
+ document_title: str | None = None,
704
+ ) -> str:
705
+ """把严格 MiddleJson 无副作用地渲染为 HTML 文档或单根 fragment。"""
706
+ if not isinstance(middle_json, MiddleJson):
707
+ raise TypeError("render_html expects a MiddleJson instance")
708
+ if not isinstance(mode, RenderMode):
709
+ raise TypeError("mode must be a RenderMode value")
710
+ if not isinstance(asset_base_url, str):
711
+ raise TypeError("asset_base_url must be a string")
712
+ if not isinstance(standalone, bool):
713
+ raise TypeError("standalone must be a bool")
714
+ if document_title is not None and not isinstance(document_title, str):
715
+ raise TypeError("document_title must be a string or None")
716
+ return _HtmlRenderer(
717
+ middle_json,
718
+ mode=mode,
719
+ asset_base_url=asset_base_url,
720
+ standalone=standalone,
721
+ document_title=document_title,
722
+ ).render()
723
+
724
+
725
+ def _classify_list(items: list[ListItem], add_reference_bullets: bool) -> tuple[str, str | None, str]:
726
+ """根据直属 marker 选择原生列表类型或显式 marker 模式。"""
727
+ if add_reference_bullets:
728
+ return "ul", None, "docvortex-list--reference"
729
+ if items and all(item.kind == "unordered" for item in items):
730
+ return "ul", None, "docvortex-list--unordered"
731
+ if items and all(item.kind == "ordered" for item in items):
732
+ styles = {item.ordered_style for item in items}
733
+ if len(styles) == 1:
734
+ style = next(iter(styles))
735
+ list_type = {
736
+ "lower-alpha": "a",
737
+ "upper-alpha": "A",
738
+ "lower-roman": "i",
739
+ "upper-roman": "I",
740
+ }.get(style or "")
741
+ return "ol", list_type, "docvortex-list--ordered"
742
+ if items and all(item.kind == "none" for item in items):
743
+ return "ul", None, "docvortex-list--unmarked"
744
+ return "ul", None, "docvortex-list--explicit"
745
+
746
+
747
+ def _list_item_content(
748
+ item: ListItem,
749
+ add_reference_bullets: bool,
750
+ *,
751
+ explicit_markers: bool,
752
+ ) -> tuple[list[InlineSpan], str | None]:
753
+ """决定一个列表项应剥离、保留还是显式显示源 marker。"""
754
+ if add_reference_bullets:
755
+ if item.kind == "unordered":
756
+ return item.body, None
757
+ prefix = f"{item.leading}{item.marker or ''}{item.separator}"
758
+ original = normalize_inline_spans([TextSpan(type="text", content=prefix), *item.body]) if prefix else item.body
759
+ return original, None
760
+ if explicit_markers:
761
+ return item.body, item.marker
762
+ if item.kind in {"unordered", "ordered"}:
763
+ return item.body, None
764
+ return item.body, item.marker
765
+
766
+
767
+ def _serialize_index_items(items: list[dict[str, object]]) -> str:
768
+ """把目录项及其已挂接的嵌套 ul 序列化为 li。"""
769
+ rendered: list[str] = []
770
+ for item in items:
771
+ nested = item["nested"]
772
+ assert isinstance(nested, list)
773
+ content = str(item["content"])
774
+ if not content and not nested:
775
+ continue
776
+ attrs = item["attrs"]
777
+ assert isinstance(attrs, list)
778
+ attrs_html = f" {' '.join(str(value) for value in attrs)}" if attrs else ""
779
+ rendered.append(f"<li{attrs_html}>{content}{''.join(str(value) for value in nested)}</li>")
780
+ return "".join(rendered)
781
+
782
+
783
+ def _wire_block_attributes(block: BlockBase) -> list[str]:
784
+ """把统一 block 身份和可往返元数据编码为版本化 HTML data 属性。"""
785
+ attrs = [f'data-block-type="{html.escape(str(block.type), quote=True)}"']
786
+ if block.index is not None:
787
+ attrs.append(f'data-block-index="{block.index}"')
788
+ sub_type = getattr(block, "sub_type", None)
789
+ if sub_type is not None:
790
+ attrs.append(f'data-block-sub-type="{html.escape(str(sub_type), quote=True)}"')
791
+ guess_lang = getattr(block, "guess_lang", None)
792
+ if isinstance(guess_lang, str) and guess_lang:
793
+ attrs.append(f'data-guess-lang="{html.escape(guess_lang, quote=True)}"')
794
+ anchor = getattr(block, "anchor", None)
795
+ if isinstance(anchor, str) and anchor:
796
+ attrs.append(f'data-anchor="{html.escape(_replace_html_controls(anchor), quote=True)}"')
797
+ level = getattr(block, "level", None)
798
+ if type(level) is int:
799
+ attrs.append(f'data-level="{level}"')
800
+ return attrs
801
+
802
+
803
+ def _wrap_visual_body(
804
+ content: str,
805
+ block: ImageBodyBlock | TableBodyBlock | ChartBodyBlock | CodeBodyBlock | AlgorithmBodyBlock,
806
+ kind: str,
807
+ ) -> str:
808
+ """为视觉主体添加不参与 CSS 类型推断的精确机器容器。"""
809
+ attrs = [f'class="{WIRE_VISUAL_BODY_CLASS} {WIRE_VISUAL_BODY_CLASS}--{kind}"', *_wire_block_attributes(block)]
810
+ return f"<div {' '.join(attrs)}>{content}</div>"
811
+
812
+
813
+ def _collect_document_anchor_ids(middle_json: MiddleJson) -> dict[str, str]:
814
+ """为正文、标题和页面脚注的首次非空 anchor 分配文档级唯一 HTML id。"""
815
+ anchor_ids: dict[str, str] = {}
816
+ used_ids: set[str] = set()
817
+ for page in middle_json.pages:
818
+ for block in page.blocks:
819
+ if isinstance(block, (TextBlock, TitleBlockBase)):
820
+ visible_content = inline_plain_text(block.content).strip()
821
+ elif isinstance(block, PageFootnoteBlock):
822
+ visible_content = inline_plain_text(block.content).strip()
823
+ else:
824
+ continue
825
+ anchor = _anchor_key(block.anchor)
826
+ if not anchor or not visible_content or anchor in anchor_ids:
827
+ continue
828
+ base_id = _html_anchor_base(anchor)
829
+ candidate = base_id
830
+ suffix = 2
831
+ while candidate in used_ids:
832
+ candidate = f"{base_id}-{suffix}"
833
+ suffix += 1
834
+ anchor_ids[anchor] = candidate
835
+ used_ids.add(candidate)
836
+ return anchor_ids
837
+
838
+
839
+ def _plain_content_text(content: str) -> str:
840
+ """从 body 内容提取安全 img alt 所需的可见纯文本。"""
841
+ if not content:
842
+ return ""
843
+ if is_supported_html_markup(content):
844
+ return BeautifulSoup(content, "html.parser").get_text(" ", strip=True)
845
+ return content.strip()
846
+
847
+
848
+ def _extract_mermaid_flowchart_source(content: str) -> str | None:
849
+ """提取受限 Mermaid flowchart fence,拒绝其他图类型和可改写配置的语法。"""
850
+ normalized = _replace_html_controls(content).replace("\r\n", "\n").replace("\r", "\n").strip()
851
+ if not normalized:
852
+ return None
853
+ lines = normalized.split("\n")
854
+ if len(lines) < 3:
855
+ return None
856
+ opening = re.fullmatch(r"(?P<fence>\x60{3,})[ \t]*mermaid[ \t]*", lines[0], re.IGNORECASE)
857
+ if opening is None:
858
+ return None
859
+ fence_length = len(opening.group("fence"))
860
+ if re.fullmatch(rf"\x60{{{fence_length},}}[ \t]*", lines[-1]) is None:
861
+ return None
862
+ source = "\n".join(lines[1:-1]).strip("\n")
863
+ if not source or len(source) > _MERMAID_MAX_TEXT_SIZE:
864
+ return None
865
+ if source.lstrip().startswith("---") or "%%{" in source:
866
+ return None
867
+ first_statement = next(
868
+ (line.strip() for line in source.splitlines() if line.strip() and not line.lstrip().startswith("%%")),
869
+ "",
870
+ )
871
+ if _MERMAID_FLOWCHART_HEADER_RE.match(first_statement) is None:
872
+ return None
873
+ return source
874
+
875
+
876
+ def _contains_usable_table(markup: str) -> bool:
877
+ """确认清洗后的 markup 至少保留一个含单元格的 table。"""
878
+ soup = BeautifulSoup(markup, "html.parser")
879
+ return any(table.find(("td", "th")) is not None for table in soup.find_all("table"))
880
+
881
+
882
+ def _render_raw_fallback(content: str) -> str:
883
+ """把无法安全结构化的原内容转义为可见 pre,避免静默丢失。"""
884
+ return f'<pre class="docvortex-raw-fallback">{html.escape(_replace_html_controls(content), quote=False)}</pre>'
885
+
886
+
887
+ def _normalize_prism_language(language: str | None) -> str | None:
888
+ """把模型语言名归一化为不可构造路径穿越的 Prism component 名。"""
889
+ normalized = (language or "").strip().lower()
890
+ normalized = _PRISM_LANGUAGE_ALIASES.get(normalized, normalized.replace("_", "-"))
891
+ if not normalized or normalized in _BLOCKED_PRISM_LANGUAGE_NAMES:
892
+ return None
893
+ return normalized if _SAFE_PRISM_LANGUAGE_RE.fullmatch(normalized) else None
894
+
895
+
896
+ def _resolve_document_title(middle_json: MiddleJson, explicit_title: str | None) -> str:
897
+ """按显式参数、首个正文文档标题、固定回退值解析 HTML title。"""
898
+ if explicit_title and explicit_title.strip():
899
+ return explicit_title.strip()
900
+ for page in middle_json.pages:
901
+ for block in page.blocks:
902
+ if isinstance(block, DocTitleBlock):
903
+ title = inline_plain_text(block.content).strip()
904
+ if title:
905
+ return title
906
+ return "DocVortex Document"
907
+
908
+
909
+ def _anchor_key(anchor: str | None) -> str:
910
+ """保留 producer anchor 身份,仅去除首尾空白供标题与目录精确匹配。"""
911
+ return (anchor or "").strip()
912
+
913
+
914
+ def _html_anchor_base(anchor: str) -> str:
915
+ """把原始 anchor 转成无空白的 HTML id 基值,碰撞由 document registry 处理。"""
916
+ return re.sub(r"\s+", "-", _replace_html_controls(anchor))
917
+
918
+
919
+ def _replace_html_controls(content: str) -> str:
920
+ """替换 HTML 不允许的 C0 控制字符与不可编码的孤立 surrogate。"""
921
+ return re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ud800-\udfff]", "\ufffd", content)
922
+
923
+
924
+ @lru_cache(maxsize=1)
925
+ def _load_html_styles() -> str:
926
+ """从包资源读取并缓存压缩后的独立 HTML 样式。"""
927
+ root = resources.files("docvortex").joinpath("resources", "html")
928
+ return root.joinpath(_STYLE_RESOURCE_NAME).read_text(encoding="utf-8")
929
+
930
+
931
+ def _mathjax_head() -> str:
932
+ """返回固定、收紧公式输入能力的 MathJax 4.1.2 head 片段。"""
933
+ return f"""<script>
934
+ window.MathJax = {{
935
+ loader: {{load: ['ui/safe']}},
936
+ tex: {{
937
+ inlineMath: [['\\\\(', '\\\\)']],
938
+ displayMath: [['\\\\[', '\\\\]']],
939
+ processEscapes: true,
940
+ processEnvironments: false,
941
+ processRefs: false,
942
+ packages: {{'[-]': ['require']}}
943
+ }},
944
+ options: {{
945
+ ignoreHtmlClass: 'docvortex-document',
946
+ processHtmlClass: 'docvortex-math',
947
+ enableMenu: false,
948
+ enableEnrichment: false,
949
+ safeOptions: {{allow: {{URLs: 'none', classes: 'none', cssIDs: 'none', styles: 'none'}}}}
950
+ }},
951
+ startup: {{
952
+ ready: function () {{
953
+ window.MathJax.startup.defaultReady();
954
+ var options = window.MathJax.startup.document.options;
955
+ options.enableMenu = false;
956
+ options.enableEnrichment = false;
957
+ }}
958
+ }}
959
+ }};
960
+ </script>
961
+ <script id="MathJax-script" defer src="{_MATHJAX_URL}"
962
+ integrity="{_MATHJAX_INTEGRITY}" crossorigin="anonymous" referrerpolicy="no-referrer"></script>"""
963
+
964
+
965
+ def _prism_head() -> str:
966
+ """返回固定 Prism core、Autoloader 和当前文档局部高亮脚本。"""
967
+ return f"""<script defer data-manual src="{_PRISM_CORE_URL}"
968
+ integrity="{_PRISM_CORE_INTEGRITY}" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
969
+ <script defer src="{_PRISM_AUTOLOADER_URL}"
970
+ integrity="{_PRISM_AUTOLOADER_INTEGRITY}" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
971
+ <script>
972
+ document.addEventListener('DOMContentLoaded', function () {{
973
+ if (!window.Prism || !Prism.plugins || !Prism.plugins.autoloader) return;
974
+ Prism.plugins.autoloader.languages_path = '{_PRISM_LANGUAGES_PATH}';
975
+ var root = document.querySelector('.docvortex-document');
976
+ if (root) Prism.highlightAllUnder(root);
977
+ }});
978
+ </script>"""
979
+
980
+
981
+ def _mermaid_head() -> str:
982
+ """返回固定 Mermaid 入口和逐图安全渲染、失败回退脚本。"""
983
+ return f"""<script defer src="{_MERMAID_URL}"
984
+ integrity="{_MERMAID_INTEGRITY}" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
985
+ <script>
986
+ function markDocVortexMermaidError(host) {{
987
+ host.dataset.mermaidState = 'error';
988
+ if (!host.classList.contains('docvortex-flowchart--has-raster')) {{
989
+ var details = host.nextElementSibling;
990
+ if (details && details.classList.contains('docvortex-flowchart-details')) details.open = true;
991
+ }}
992
+ }}
993
+ document.addEventListener('DOMContentLoaded', function () {{
994
+ var root = document.querySelector('.docvortex-document');
995
+ var hosts = root ? Array.from(root.querySelectorAll('.docvortex-flowchart')) : [];
996
+ if (!hosts.length) return;
997
+ if (!window.mermaid) {{
998
+ hosts.forEach(markDocVortexMermaidError);
999
+ return;
1000
+ }}
1001
+ try {{
1002
+ window.mermaid.initialize({{
1003
+ startOnLoad: false,
1004
+ securityLevel: 'strict',
1005
+ suppressErrorRendering: true,
1006
+ theme: 'neutral',
1007
+ maxTextSize: 50000,
1008
+ maxEdges: 500,
1009
+ flowchart: {{htmlLabels: false, useMaxWidth: true}}
1010
+ }});
1011
+ }} catch (_error) {{
1012
+ hosts.forEach(markDocVortexMermaidError);
1013
+ return;
1014
+ }}
1015
+ (async function () {{
1016
+ for (var index = 0; index < hosts.length; index += 1) {{
1017
+ var host = hosts[index];
1018
+ var details = host.nextElementSibling;
1019
+ var source = details && details.querySelector('.docvortex-flowchart-source code');
1020
+ var canvas = host.querySelector('.docvortex-flowchart-canvas');
1021
+ if (!source || !canvas) {{
1022
+ markDocVortexMermaidError(host);
1023
+ continue;
1024
+ }}
1025
+ var renderId = 'docvortex-mermaid-' + index;
1026
+ while (document.getElementById(renderId)) renderId += '-x';
1027
+ host.dataset.mermaidState = 'rendering';
1028
+ try {{
1029
+ var result = await window.mermaid.render(renderId, source.textContent);
1030
+ canvas.innerHTML = result.svg;
1031
+ host.dataset.mermaidState = 'rendered';
1032
+ }} catch (_error) {{
1033
+ markDocVortexMermaidError(host);
1034
+ }}
1035
+ }}
1036
+ }})();
1037
+ }});
1038
+ </script>"""
1039
+
1040
+
1041
+ __all__ = ["render_html"]