format_idf_target.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import re
  2. import os
  3. import os.path
  4. from docutils import io, nodes, statemachine, utils
  5. from docutils.utils.error_reporting import SafeString, ErrorString
  6. from docutils.parsers.rst import directives
  7. from sphinx.directives.other import Include as BaseInclude
  8. def setup(app):
  9. sub = StringSubstituter()
  10. # Config values not available when setup is called
  11. app.connect('config-inited', lambda _, config: sub.init_sub_strings(config))
  12. app.connect('source-read', sub.substitute_source_read_cb)
  13. # Override the default include directive to include formatting with idf_target
  14. # This is needed since there are no source-read events for includes
  15. app.add_directive('include', FormatedInclude, override=True)
  16. return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.2'}
  17. class StringSubstituter:
  18. """ Allows for string substitution of target related strings
  19. before any markup is parsed
  20. Supports the following replacements (examples shown is for target=esp32s2):
  21. {IDF_TARGET_NAME}, replaced with the current target name, e.g. ESP32-S2 Beta
  22. {IDF_TARGET_PATH_NAME}, replaced with the path name, e.g. esp32s2
  23. {IDF_TARGET_TOOLCHAIN_NAME}, replaced with the toolchain name, e.g. esp32s2
  24. {IDF_TARGET_CFG_PREFIX}, replaced with the prefix used for config parameters, e.g. ESP32S2
  25. {IDF_TARGET_TRM_EN_URL}, replaced with the url to the English technical reference manual
  26. {IDF_TARGET_TRM_CH_URL}, replaced with the url to the Chinese technical reference manual
  27. Also supports defines of local (single rst file) with the format:
  28. {IDF_TARGET_TX_PIN:default="IO3",esp32="IO4",esp32s2="IO5"}
  29. This will define a replacement of the tag {IDF_TARGET_TX_PIN} in the current rst-file, see e.g. uart.rst for example
  30. """
  31. TARGET_NAMES = {'esp32': 'ESP32', 'esp32s2': 'ESP32-S2'}
  32. TOOLCHAIN_NAMES = {'esp32': 'esp32', 'esp32s2': 'esp32s2'}
  33. CONFIG_PREFIX = {'esp32': 'ESP32', 'esp32s2': 'ESP32S2'}
  34. TRM_EN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf',
  35. 'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_en.pdf'}
  36. TRM_CN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_cn.pdf',
  37. 'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_cn.pdf'}
  38. RE_PATTERN = re.compile(r'^\s*{IDF_TARGET_(\w+?):(.+?)}', re.MULTILINE)
  39. def __init__(self):
  40. self.substitute_strings = {}
  41. self.local_sub_strings = {}
  42. def add_pair(self, tag, replace_value):
  43. self.substitute_strings[tag] = replace_value
  44. def init_sub_strings(self, config):
  45. self.target_name = config.idf_target
  46. self.add_pair("{IDF_TARGET_NAME}", self.TARGET_NAMES[config.idf_target])
  47. self.add_pair("{IDF_TARGET_PATH_NAME}", config.idf_target)
  48. self.add_pair("{IDF_TARGET_TOOLCHAIN_NAME}", self.TOOLCHAIN_NAMES[config.idf_target])
  49. self.add_pair("{IDF_TARGET_CFG_PREFIX}", self.CONFIG_PREFIX[config.idf_target])
  50. self.add_pair("{IDF_TARGET_TRM_EN_URL}", self.TRM_EN_URL[config.idf_target])
  51. self.add_pair("{IDF_TARGET_TRM_CN_URL}", self.TRM_CN_URL[config.idf_target])
  52. def add_local_subs(self, matches):
  53. for sub_def in matches:
  54. if len(sub_def) != 2:
  55. raise ValueError("IDF_TARGET_X substitution define invalid, val={}".format(sub_def))
  56. tag = "{" + "IDF_TARGET_{}".format(sub_def[0]) + "}"
  57. match_default = re.match(r'^\s*default(\s*)=(\s*)\"(.*?)\"', sub_def[1])
  58. if match_default is None:
  59. # There should always be a default value
  60. raise ValueError("No default value in IDF_TARGET_X substitution define, val={}".format(sub_def))
  61. match_target = re.match(r'^.*{}(\s*)=(\s*)\"(.*?)\"'.format(self.target_name), sub_def[1])
  62. if match_target is None:
  63. sub_value = match_default.groups()[2]
  64. else:
  65. sub_value = match_target.groups()[2]
  66. self.local_sub_strings[tag] = sub_value
  67. def substitute(self, content):
  68. # Add any new local tags that matches the reg.ex.
  69. sub_defs = re.findall(self.RE_PATTERN, content)
  70. if len(sub_defs) != 0:
  71. self.add_local_subs(sub_defs)
  72. # Remove the tag defines
  73. content = re.sub(self.RE_PATTERN,'', content)
  74. for key in self.local_sub_strings:
  75. content = content.replace(key, self.local_sub_strings[key])
  76. self.local_sub_strings = {}
  77. for key in self.substitute_strings:
  78. content = content.replace(key, self.substitute_strings[key])
  79. return content
  80. def substitute_source_read_cb(self, app, docname, source):
  81. source[0] = self.substitute(source[0])
  82. class FormatedInclude(BaseInclude):
  83. """
  84. Include and format content read from a separate source file.
  85. Code is based on the default include directive from docutils
  86. but extended to also format the content according to IDF target.
  87. """
  88. def run(self):
  89. # For code or literal include blocks we run the normal include
  90. if 'literal' in self.options or 'code' in self.options:
  91. return super(FormatedInclude, self).run()
  92. """Include a file as part of the content of this reST file."""
  93. if not self.state.document.settings.file_insertion_enabled:
  94. raise self.warning('"%s" directive disabled.' % self.name)
  95. source = self.state_machine.input_lines.source(
  96. self.lineno - self.state_machine.input_offset - 1)
  97. source_dir = os.path.dirname(os.path.abspath(source))
  98. rel_filename, filename = self.env.relfn2path(self.arguments[0])
  99. self.arguments[0] = filename
  100. self.env.note_included(filename)
  101. path = directives.path(self.arguments[0])
  102. if path.startswith('<') and path.endswith('>'):
  103. path = os.path.join(self.standard_include_path, path[1:-1])
  104. path = os.path.normpath(os.path.join(source_dir, path))
  105. path = utils.relative_path(None, path)
  106. path = nodes.reprunicode(path)
  107. encoding = self.options.get(
  108. 'encoding', self.state.document.settings.input_encoding)
  109. e_handler = self.state.document.settings.input_encoding_error_handler
  110. tab_width = self.options.get(
  111. 'tab-width', self.state.document.settings.tab_width)
  112. try:
  113. self.state.document.settings.record_dependencies.add(path)
  114. include_file = io.FileInput(source_path=path,
  115. encoding=encoding,
  116. error_handler=e_handler)
  117. except UnicodeEncodeError:
  118. raise self.severe(u'Problems with "%s" directive path:\n'
  119. 'Cannot encode input file path "%s" '
  120. '(wrong locale?).' %
  121. (self.name, SafeString(path)))
  122. except IOError as error:
  123. raise self.severe(u'Problems with "%s" directive path:\n%s.' %
  124. (self.name, ErrorString(error)))
  125. startline = self.options.get('start-line', None)
  126. endline = self.options.get('end-line', None)
  127. try:
  128. if startline or (endline is not None):
  129. lines = include_file.readlines()
  130. rawtext = ''.join(lines[startline:endline])
  131. else:
  132. rawtext = include_file.read()
  133. except UnicodeError as error:
  134. raise self.severe(u'Problem with "%s" directive:\n%s' %
  135. (self.name, ErrorString(error)))
  136. # Format input
  137. sub = StringSubstituter()
  138. config = self.state.document.settings.env.config
  139. sub.init_sub_strings(config)
  140. rawtext = sub.substitute(rawtext)
  141. # start-after/end-before: no restrictions on newlines in match-text,
  142. # and no restrictions on matching inside lines vs. line boundaries
  143. after_text = self.options.get('start-after', None)
  144. if after_text:
  145. # skip content in rawtext before *and incl.* a matching text
  146. after_index = rawtext.find(after_text)
  147. if after_index < 0:
  148. raise self.severe('Problem with "start-after" option of "%s" '
  149. 'directive:\nText not found.' % self.name)
  150. rawtext = rawtext[after_index + len(after_text):]
  151. before_text = self.options.get('end-before', None)
  152. if before_text:
  153. # skip content in rawtext after *and incl.* a matching text
  154. before_index = rawtext.find(before_text)
  155. if before_index < 0:
  156. raise self.severe('Problem with "end-before" option of "%s" '
  157. 'directive:\nText not found.' % self.name)
  158. rawtext = rawtext[:before_index]
  159. include_lines = statemachine.string2lines(rawtext, tab_width,
  160. convert_whitespace=True)
  161. self.state_machine.insert_input(include_lines, path)
  162. return []