docs: multi target include dir support

Closes IDF-1371

Added an include directive that allows for the content to be formatted
according to idf target
This commit is contained in:
Marius Vikhammer 2020-02-17 16:59:50 +08:00
parent 8e1442f0e7
commit dfc95bc78b
3 changed files with 137 additions and 27 deletions

View file

@ -157,9 +157,10 @@ Other Extensions
This will define a replacement of the tag {\IDF_TARGET_TX_PIN} in the current rst-file.
The extension also overrides the default ``.. include::`` directive in order to format any included content using the same rules.
These replacements cannot be used inside markup that rely on alignment of characters, e.g. tables.
These replacement can't be used in a file which is `::include`-ed from another file. *This includes any English document where the ``zh_CN`` translation includes then ``en`` translation*.
Related Documents
-----------------

View file

@ -303,8 +303,6 @@ This extension also supports markup for defining a local (for a single .rst-file
{\IDF_TARGET_TX_PIN:default="IO3",esp32="IO4",esp32s2beta="IO5"} will define a substitution for the tag {\IDF_TARGET_TX_PIN}, which would be replaced by the text IO5 if sphinx was called with the tag esp32s2beta.
.. note:: Due to limitations in Sphinx processing, these substitutions are not applied to any document that is included via the ``.. include::` directive. In these cases it's necessary to use the ``only`` blocks and write per-target sections instead. Unfortunately this includes any document which is not yet translated, as the ``zh_CN`` version will include the ``en`` version.
Put it all together
-------------------

View file

@ -1,14 +1,24 @@
import re
import os
import os.path
from docutils import io, nodes, statemachine, utils
from docutils.utils.error_reporting import SafeString, ErrorString
from docutils.parsers.rst import directives
from sphinx.directives.other import Include as BaseInclude
TARGET_NAMES = {'esp32': 'ESP32', 'esp32s2': 'ESP32-S2'}
TOOLCHAIN_NAMES = {'esp32': 'esp', 'esp32s2': 'esp32s2'}
CONFIG_PREFIX = {'esp32': 'ESP32', 'esp32s2': 'ESP32S2'}
TRM_EN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf',
'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_en.pdf'}
def setup(app):
sub = StringSubstituter()
TRM_CN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_cn.pdf',
'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_cn.pdf'}
# Config values not available when setup is called
app.connect('config-inited', lambda _, config: sub.init_sub_strings(config))
app.connect('source-read', sub.substitute_source_read_cb)
# Override the default include directive to include formatting with idf_target
# This is needed since there are no source-read events for includes
app.add_directive('include', FormatedInclude, override=True)
return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.2'}
class StringSubstituter:
@ -29,6 +39,15 @@ class StringSubstituter:
This will define a replacement of the tag {IDF_TARGET_TX_PIN} in the current rst-file, see e.g. uart.rst for example
"""
TARGET_NAMES = {'esp32': 'ESP32', 'esp32s2': 'ESP32-S2'}
TOOLCHAIN_NAMES = {'esp32': 'esp', 'esp32s2': 'esp32s2'}
CONFIG_PREFIX = {'esp32': 'ESP32', 'esp32s2': 'ESP32S2'}
TRM_EN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf',
'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_en.pdf'}
TRM_CN_URL = {'esp32': 'https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_cn.pdf',
'esp32s2': 'https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_cn.pdf'}
RE_PATTERN = re.compile(r'^\s*{IDF_TARGET_(\w+?):(.+?)}', re.MULTILINE)
def __init__(self):
@ -38,15 +57,15 @@ class StringSubstituter:
def add_pair(self, tag, replace_value):
self.substitute_strings[tag] = replace_value
def init_sub_strings(self, app, config):
def init_sub_strings(self, config):
self.target_name = config.idf_target
self.add_pair("{IDF_TARGET_NAME}", TARGET_NAMES[config.idf_target])
self.add_pair("{IDF_TARGET_NAME}", self.TARGET_NAMES[config.idf_target])
self.add_pair("{IDF_TARGET_PATH_NAME}", config.idf_target)
self.add_pair("{IDF_TARGET_TOOLCHAIN_NAME}", TOOLCHAIN_NAMES[config.idf_target])
self.add_pair("{IDF_TARGET_CFG_PREFIX}", CONFIG_PREFIX[config.idf_target])
self.add_pair("{IDF_TARGET_TRM_EN_URL}", TRM_EN_URL[config.idf_target])
self.add_pair("{IDF_TARGET_TRM_CN_URL}", TRM_CN_URL[config.idf_target])
self.add_pair("{IDF_TARGET_TOOLCHAIN_NAME}", self.TOOLCHAIN_NAMES[config.idf_target])
self.add_pair("{IDF_TARGET_CFG_PREFIX}", self.CONFIG_PREFIX[config.idf_target])
self.add_pair("{IDF_TARGET_TRM_EN_URL}", self.TRM_EN_URL[config.idf_target])
self.add_pair("{IDF_TARGET_TRM_CN_URL}", self.TRM_CN_URL[config.idf_target])
def add_local_subs(self, matches):
@ -71,30 +90,122 @@ class StringSubstituter:
self.local_sub_strings[tag] = sub_value
def substitute(self, app, docname, source):
def substitute(self, content):
# Add any new local tags that matches the reg.ex.
sub_defs = re.findall(self.RE_PATTERN, source[0])
sub_defs = re.findall(self.RE_PATTERN, content)
if len(sub_defs) != 0:
self.add_local_subs(sub_defs)
# Remove the tag defines
source[0] = re.sub(self.RE_PATTERN,'', source[0])
content = re.sub(self.RE_PATTERN,'', content)
for key in self.local_sub_strings:
source[0] = source[0].replace(key, self.local_sub_strings[key])
content = content.replace(key, self.local_sub_strings[key])
self.local_sub_strings = {}
for key in self.substitute_strings:
source[0] = source[0].replace(key, self.substitute_strings[key])
content = content.replace(key, self.substitute_strings[key])
return content
def substitute_source_read_cb(self, app, docname, source):
source[0] = self.substitute(source[0])
def setup(app):
sub = StringSubstituter()
class FormatedInclude(BaseInclude):
# Config values not available when setup is called
app.connect('config-inited', sub.init_sub_strings)
app.connect('source-read', sub.substitute)
"""
Include and format content read from a separate source file.
return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.1'}
Code is based on the default include directive from docutils
but extended to also format the content according to IDF target.
"""
def run(self):
# For code or literal include blocks we run the normal include
if 'literal' in self.options or 'code' in self.options:
return super(FormatedInclude, self).run()
"""Include a file as part of the content of this reST file."""
if not self.state.document.settings.file_insertion_enabled:
raise self.warning('"%s" directive disabled.' % self.name)
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
source_dir = os.path.dirname(os.path.abspath(source))
rel_filename, filename = self.env.relfn2path(self.arguments[0])
self.arguments[0] = filename
self.env.note_included(filename)
path = directives.path(self.arguments[0])
if path.startswith('<') and path.endswith('>'):
path = os.path.join(self.standard_include_path, path[1:-1])
path = os.path.normpath(os.path.join(source_dir, path))
path = utils.relative_path(None, path)
path = nodes.reprunicode(path)
encoding = self.options.get(
'encoding', self.state.document.settings.input_encoding)
e_handler = self.state.document.settings.input_encoding_error_handler
tab_width = self.options.get(
'tab-width', self.state.document.settings.tab_width)
try:
self.state.document.settings.record_dependencies.add(path)
include_file = io.FileInput(source_path=path,
encoding=encoding,
error_handler=e_handler)
except UnicodeEncodeError:
raise self.severe(u'Problems with "%s" directive path:\n'
'Cannot encode input file path "%s" '
'(wrong locale?).' %
(self.name, SafeString(path)))
except IOError as error:
raise self.severe(u'Problems with "%s" directive path:\n%s.' %
(self.name, ErrorString(error)))
startline = self.options.get('start-line', None)
endline = self.options.get('end-line', None)
try:
if startline or (endline is not None):
lines = include_file.readlines()
rawtext = ''.join(lines[startline:endline])
else:
rawtext = include_file.read()
except UnicodeError as error:
raise self.severe(u'Problem with "%s" directive:\n%s' %
(self.name, ErrorString(error)))
# Format input
sub = StringSubstituter()
config = self.state.document.settings.env.config
sub.init_sub_strings(config)
rawtext = sub.substitute(rawtext)
# start-after/end-before: no restrictions on newlines in match-text,
# and no restrictions on matching inside lines vs. line boundaries
after_text = self.options.get('start-after', None)
if after_text:
# skip content in rawtext before *and incl.* a matching text
after_index = rawtext.find(after_text)
if after_index < 0:
raise self.severe('Problem with "start-after" option of "%s" '
'directive:\nText not found.' % self.name)
rawtext = rawtext[after_index + len(after_text):]
before_text = self.options.get('end-before', None)
if before_text:
# skip content in rawtext after *and incl.* a matching text
before_index = rawtext.find(before_text)
if before_index < 0:
raise self.severe('Problem with "end-before" option of "%s" '
'directive:\nText not found.' % self.name)
rawtext = rawtext[:before_index]
include_lines = statemachine.string2lines(rawtext, tab_width,
convert_whitespace=True)
self.state_machine.insert_input(include_lines, path)
return []