format_idf_target.py 9.9 KB

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