recommonmark_wrapper.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env python3
  2. # :Copyright: © 2020 Günter Milde.
  3. # :License: Released under the terms of the `2-Clause BSD license`_, in short:
  4. #
  5. # Copying and distribution of this file, with or without modification,
  6. # are permitted in any medium without royalty provided the copyright
  7. # notice and this notice are preserved.
  8. # This file is offered as-is, without any warranty.
  9. #
  10. # .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
  11. #
  12. # Revision: $Revision: 9043 $
  13. # Date: $Date: 2022-03-11 13:09:16 +0100 (Fr, 11. Mär 2022) $
  14. """
  15. A parser for CommonMark Markdown text using `recommonmark`__.
  16. __ https://pypi.org/project/recommonmark/
  17. .. important:: This module is provisional
  18. * The "recommonmark" package is unmaintained and deprecated.
  19. This wrapper module will be removed in a future Docutils version.
  20. * The API is not settled and may change with any minor Docutils version.
  21. """
  22. from docutils import nodes, Component
  23. try:
  24. from recommonmark.parser import CommonMarkParser
  25. except ImportError as err:
  26. raise ImportError(f'{err}.\n'
  27. 'Parsing "recommonmark" Markdown flavour requires the '
  28. 'package https://pypi.org/project/recommonmark which '
  29. 'in turn depends on https://pypi.org/project/sphinx.')
  30. try:
  31. from sphinx import addnodes
  32. # already cached in `sys.modules` if recommonmark >= 0.5.0
  33. except ImportError:
  34. # stub to prevent errors with recommonmark < 0.5.0
  35. class addnodes:
  36. pending_xref = nodes.pending
  37. # auxiliary function for `document.findall()`
  38. def is_literal(node):
  39. return isinstance(node, (nodes.literal, nodes.literal_block))
  40. class Parser(CommonMarkParser):
  41. """MarkDown parser based on recommonmark.
  42. This parser is provisional:
  43. the API is not settled and may change with any minor Docutils version.
  44. """
  45. supported = ('recommonmark', 'commonmark', 'markdown', 'md')
  46. """Formats this parser supports."""
  47. config_section = 'recommonmark parser'
  48. config_section_dependencies = ('parsers',)
  49. def get_transforms(self):
  50. return Component.get_transforms(self) # + [AutoStructify]
  51. def parse(self, inputstring, document):
  52. """Use the upstream parser and clean up afterwards.
  53. """
  54. # check for exorbitantly long lines
  55. for i, line in enumerate(inputstring.split('\n')):
  56. if len(line) > document.settings.line_length_limit:
  57. error = document.reporter.error(
  58. 'Line %d exceeds the line-length-limit.'%(i+1))
  59. document.append(error)
  60. return
  61. # pass to upstream parser
  62. try:
  63. CommonMarkParser.parse(self, inputstring, document)
  64. except Exception as err:
  65. if document.settings.traceback:
  66. raise err
  67. error = document.reporter.error('Parsing with "recommonmark" '
  68. 'returned the error:\n%s'%err)
  69. document.append(error)
  70. # Post-Processing
  71. # ---------------
  72. # merge adjoining Text nodes:
  73. for node in document.findall(nodes.TextElement):
  74. children = node.children
  75. i = 0
  76. while i+1 < len(children):
  77. if (isinstance(children[i], nodes.Text)
  78. and isinstance(children[i+1], nodes.Text)):
  79. children[i] = nodes.Text(children[i]+children.pop(i+1))
  80. children[i].parent = node
  81. else:
  82. i += 1
  83. # add "code" class argument to literal elements (inline and block)
  84. for node in document.findall(is_literal):
  85. if 'code' not in node['classes']:
  86. node['classes'].append('code')
  87. # move "language" argument to classes
  88. for node in document.findall(nodes.literal_block):
  89. if 'language' in node.attributes:
  90. node['classes'].append(node['language'])
  91. del node['language']
  92. # replace raw nodes if raw is not allowed
  93. if not document.settings.raw_enabled:
  94. for node in document.findall(nodes.raw):
  95. warning = document.reporter.warning('Raw content disabled.')
  96. node.parent.replace(node, warning)
  97. # drop pending_xref (Sphinx cross reference extension)
  98. for node in document.findall(addnodes.pending_xref):
  99. reference = node.children[0]
  100. if 'name' not in reference:
  101. reference['name'] = nodes.fully_normalize_name(
  102. reference.astext())
  103. node.parent.replace(node, reference)
  104. def visit_document(self, node):
  105. """Dummy function to prevent spurious warnings.
  106. cf. https://github.com/readthedocs/recommonmark/issues/177
  107. """
  108. pass
  109. # Overwrite parent method with version that
  110. # doesn't pass deprecated `rawsource` argument to nodes.Text:
  111. def visit_text(self, mdnode):
  112. self.current_node.append(nodes.Text(mdnode.literal))