pluto_hdl_adi/docs/extensions/adi_hdl_parser.py

934 lines
25 KiB
Python

###############################################################################
## Copyright (C) 2022-2023 Analog Devices, Inc. All rights reserved.
### SPDX short identifier: ADIBSD
###############################################################################
import os.path
from docutils import nodes
from docutils.statemachine import ViewList
from docutils.parsers.rst import Directive, directives
from sphinx.util.nodes import nested_parse_with_titles
from sphinx.util import logging
from lxml import etree
from adi_hdl_static import hdl_strings
from uuid import uuid4
from hashlib import sha1
import contextlib
logger = logging.getLogger(__name__)
dft_hide_collapsible_content = True
class node_base(nodes.Element, nodes.General):
"""
Adapted from
https://github.com/pradyunsg/sphinx-inline-tabs
https://github.com/dgarcia360/sphinx-collapse
"""
@staticmethod
def visit(translator, node):
attributes = node.attributes.copy()
attributes.pop("ids")
attributes.pop("classes")
attributes.pop("names")
attributes.pop("dupnames")
attributes.pop("backrefs")
text = translator.starttag(node, node.tagname, **attributes)
translator.body.append(text.strip())
@staticmethod
def depart(translator, node):
if node.endtag:
translator.body.append(f"</{node.tagname}>")
@staticmethod
def default(translator, node):
pass
class node_div(node_base):
tagname = 'div'
endtag = 'true'
class node_input(node_base):
tagname = 'input'
endtag = 'false'
class node_label(node_base):
tagname = 'label'
endtag = 'true'
class node_icon(node_base):
tagname = 'div'
endtag = 'false'
def dot_fix(string):
if (string.rfind('.') != len(string)-1):
return string + '.'
else:
return string
def pretty_dep(string):
if string is None:
return ''
return string.replace("'MODELPARAM_VALUE.",'').replace("'",'')
def sanitized_bool(string):
string_ = string.strip().lower()
if string_ in ['1', 'true', 'yes', 'y']:
return True
elif string_ in ['0', 'false', 'no', 'n']:
return False
else:
logger.warning(f"Got malformed bool value {string}.")
return False
class directive_base(Directive):
has_content = True
add_index = True
current_doc = ''
final_argument_whitespace = True
@staticmethod
def get_descriptions(content):
items = {}
key = ''
for line in content:
if line.startswith('* -'):
key = line[line.find('* -')+3:].split()[0]
items[key] = []
else:
items[key].append(line)
for key in items:
items[key] = ' '.join(items[key]).replace('-', '', 1).strip()
return items
def column_entry(self, row, text, node_type, classes=[]):
entry = nodes.entry(classes=classes)
if node_type == 'literal':
entry += nodes.literal(text=text)
elif node_type == 'paragraph':
entry += nodes.paragraph(text=text)
elif node_type == 'reST':
rst = ViewList()
rst.append(text, f"virtual_{str(uuid4())}", 0)
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, rst, node)
entry += node
else:
return
row += entry
def column_entries(self, rows, items):
row = nodes.row()
for item in items:
if len(item) == 3:
self.column_entry(row, item[0], item[1], classes=item[2])
else:
self.column_entry(row, item[0], item[1])
rows.append(row)
def generic_table(self, description):
tgroup = nodes.tgroup(cols=2)
for _ in range(2):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table()
table += tgroup
self.table_header(tgroup, ["Name", "Description"])
rows = []
for key in description:
row = nodes.row()
entry = nodes.entry()
entry += nodes.literal(text="{:s}".format(key))
row += entry
entry = nodes.entry()
rst = ViewList()
rst.append(description[key], f"virtual_{str(uuid4())}", 0)
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, rst, node)
entry += node
row += entry
rows.append(row)
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
return table
@staticmethod
def table_header(tgroup, columns):
thead = nodes.thead()
tgroup += thead
row = nodes.row()
for header_name in columns:
entry = nodes.entry()
entry += nodes.paragraph(text=header_name)
row += entry
thead.append(row)
def collapsible(self, section, text=""):
env = self.state.document.settings.env
_id = sha1(text.encode('utf-8')).hexdigest()
container = nodes.container(
"",
is_div=True,
classes=['collapsible']
)
checked = {"checked": ''} if not env.config.hide_collapsible_content else {}
input_ = node_input(
type="checkbox",
**checked,
ids=[_id],
name=_id,
classes=['collapsible_input']
)
label = node_label(
**{"for": _id}
)
icon = node_icon(
classes=['icon']
)
content = nodes.container(
"",
is_div=True,
classes=['collapsible_content']
)
label += nodes.paragraph(text=text)
label += icon
container += input_
container += label
container += content
section += container
return (content, label)
class directive_collapsible(directive_base):
option_spec = {'path': directives.unchanged}
required_arguments = 1
optional_arguments = 0
def run(self):
self.assert_has_content()
env = self.state.document.settings.env
self.current_doc = env.doc2path(env.docname)
node = node_div()
content, _ = self.collapsible(node, self.arguments[0].strip())
self.state.nested_parse(self.content, self.content_offset, content)
return [ node ]
class directive_interfaces(directive_base):
option_spec = {'path': directives.unchanged}
required_arguments = 0
optional_arguments = 0
def tables(self, subnode, content, component):
description = self.get_descriptions(content)
if component is None:
return self.generic_table(description)
bs = component['bus_interface']
for tag in bs:
section = nodes.section(
ids=[f"bus-interface-{tag}"]
)
title = nodes.title(text=tag)
section += title
if bs[tag]['dependency'] is not None:
section += [nodes.inline(text="Enabled if "),
nodes.literal(text=pretty_dep(bs[tag]['dependency'])),
nodes.inline(text=".")]
if tag in description:
rst = ViewList()
rst.append(description[tag], f"virtual_{str(uuid4())}", 0)
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, rst, node)
section += node
content, _ = self.collapsible(section, f"Ports of {tag} bus.")
tgroup = nodes.tgroup(cols=3)
for _ in range(3):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table()
table += tgroup
self.table_header(tgroup, ["Physical Port", "Logical Port", "Direction"])
rows = []
pm = bs[tag]['port_map']
for key in pm:
self.column_entries(rows, [
[key, 'literal'],
[pm[key]['logical_port'], 'literal'],
[pm[key]['direction'], 'paragraph'],
])
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
content += table
subnode += section
section = nodes.section(ids=[f"ports"])
title = nodes.title(text="Ports")
section += title
content, _ = self.collapsible(section, f"Ports table.")
tgroup = nodes.tgroup(cols=4)
for _ in range(4):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table()
table += tgroup
self.table_header(tgroup, ["Physical Port", "Direction", "Dependency", "Description"])
rows = []
pr = component['ports']
dm = component['bus_domain']
for key in pr:
row = nodes.row()
self.column_entry(row, key, 'literal')
self.column_entry(row, pr[key]['direction'], 'paragraph')
self.column_entry(row, pretty_dep(pr[key]['dependency']), 'paragraph')
if 'clk' in key or 'clock' in key:
domain = 'clock domain'
elif 'reset':
domain = 'reset signal'
else:
domain = 'domain'
if key in dm:
bus = 'Buses' if len(dm[key]) > 1 else 'Bus'
plr = 'are' if len(dm[key]) > 1 else 'is'
in_domain = f"{bus} ``{'``, ``'.join(dm[key])}`` {plr} synchronous to this {domain}."
else:
in_domain = ""
if key in description:
self.column_entry(row, " ".join([description[key], in_domain]), 'reST', classes=['description'])
else:
self.column_entry(row, in_domain, 'reST', classes=['description'])
rows.append(row)
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
content += table
subnode += section
for tag in description:
if tag not in bs and tag not in pr:
logger.warning(f"Signal {tag} defined in the directive does not exist in the source code!")
return subnode
def run(self):
env = self.state.document.settings.env
self.current_doc = env.doc2path(env.docname)
node = node_div()
if 'path' in self.options:
lib_name = self.options['path']
else:
lib_name = env.docname.replace('/index', '')
if lib_name in env.component:
self.tables(node, self.content, env.component[lib_name])
else:
self.tables(node, self.content, None)
return [ node ]
class directive_regmap(directive_base):
option_spec = {'name': directives.unchanged, 'no-type-info': directives.unchanged}
required_arguments = 0
optional_arguments = 0
def tables(self, subnode, obj):
section = nodes.section(ids=[f"register-map-{obj['title']}"])
title = nodes.title(text=f"{obj['title']} ({obj['title']})")
section += title
content, _ = self.collapsible(section, f"Register map table.")
tgroup = nodes.tgroup(cols=7)
for _ in range(7):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table(classes=['regmap'])
table += tgroup
self.table_header(tgroup, ["DWORD", "BYTE", "BITS", "Name", "Type", "Default", "Description"])
rows = []
for reg in obj['regmap']:
self.column_entries(rows, [
[reg['address'][0], 'literal'],
[reg['address'][1], 'literal'],
['', 'literal'],
[reg['name'], 'literal'],
['', 'literal'],
['', 'literal'],
[reg['description'], 'reST', ['description']],
])
for field in reg['fields']:
self.column_entries(rows, [
['', 'literal'],
['', 'literal'],
[f"[{field['bits']}]", 'literal'],
[field['name'], 'literal'],
[field['rw'], 'paragraph'],
[field['default'], 'paragraph', ['default']],
[field['description'], 'reST', ['description']],
])
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
content += table
subnode += section
if 'no-type-info' in self.options:
return subnode
tgroup = nodes.tgroup(cols=3)
for _ in range(3):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table()
table += tgroup
self.table_header(tgroup, ["Access Type", "Name", "Description"])
rows = []
for at in obj['access_type']:
self.column_entries(rows, [
[at, 'paragraph'],
[hdl_strings.access_type[at]['name'], 'paragraph'],
[hdl_strings.access_type[at]['description'], 'paragraph']
])
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
section += table
return subnode
def run(self):
env = self.state.document.settings.env
self.current_doc = env.doc2path(env.docname)
if os.getcwd() not in self.current_doc:
raise Exception(f"Inconsistent paths, {os.getcwd()} not in {self.current_doc}")
owner = self.current_doc[len(os.getcwd())+1:-4]
node = node_div()
if 'name' in self.options:
lib_name = self.options['name']
else:
logger.warning("hdl-regmap directive without name option, skipped!")
return [ node ]
subnode = nodes.section(ids=["hdl-regmap"])
# Have to search all because it is allowed to have more than one regmap per file...
file = None
for f in env.regmaps:
if lib_name in env.regmaps[f]['subregmap']:
file = f
break
if file is None:
logger.warning(f"Title tool {lib_name} not-found in any regmap file, skipped!")
return [ node ]
if owner not in env.regmaps[f]['owners']:
env.regmaps[f]['owners'].append(owner)
self.tables(subnode, env.regmaps[f]['subregmap'][lib_name])
node += subnode
return [ node ]
class directive_parameters(directive_base):
option_spec = {'path': directives.unchanged}
required_arguments = 0
optional_arguments = 0
def tables(self, content, parameter):
description = self.get_descriptions(content)
if parameter is None:
return self.generic_table(description)
tgroup = nodes.tgroup(cols=5)
for _ in range(5):
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table = nodes.table()
table += tgroup
self.table_header(tgroup, ["Name", "Description", "Data Type", "Default Value", "Choices/Range"])
rows = []
for key in parameter:
row = nodes.row()
self.column_entry(row, "{:s}".format(key), 'literal')
if key in description:
self.column_entry(row, description[key], 'reST', classes=['description'])
else:
self.column_entry(row, dot_fix(parameter[key]['description']), 'paragraph', classes=['description'])
for tag, ty in zip(['type', 'default'], ['paragraph', 'literal']):
if parameter[key][tag] is not None:
self.column_entry(row, parameter[key][tag], ty, classes=[tag])
else:
logger.warning(f"Got empty {tag} at parameter {key}!")
self.column_entry(row, "", 'paragraph')
crange = []
for tag in ['choices', 'range']:
if parameter[key][tag] is not None:
crange.append(parameter[key][tag])
crange = '. '.join(crange)
self.column_entry(row, crange, 'paragraph')
rows.append(row)
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
for tag in description:
if tag not in parameter:
logger.warning(f"{tag} defined in the directive does not exist in the source code!")
return table
def run(self):
env = self.state.document.settings.env
self.current_doc = env.doc2path(env.docname)
node = node_div()
if 'path' in self.options:
lib_name = self.options['path']
else:
lib_name = env.docname.replace('/index', '')
subnode = nodes.section(ids=["hdl-parameters"])
if lib_name in env.component:
subnode += self.tables(self.content, env.component[lib_name]['parameters'])
else:
subnode += self.tables(self.content, None)
node += subnode
return [ node ]
def parse_hdl_component(path, ctime):
component = {
'bus_interface':{},
'bus_domain':{},
'ports': {},
'parameters': {},
'ctime': ctime
}
def get_namespaces(item):
nsmap = item.nsmap
for i in ['spirit', 'xilinx', 'xsi']:
if i not in nsmap:
raise Exception(f"Required namespace {i} not in file!")
return (nsmap['spirit'], nsmap['xilinx'], nsmap['xsi'])
def get(item, local_name):
items = get_all(item, local_name)
if len(items) == 0:
return None
else:
return items[0]
def get_all(item, local_name):
template = "/*[local-name()='%s']"
if not isinstance(local_name, str):
raise Exception("Got wrong type, only Strings are allowed")
local_name = local_name.split('/')
return item.xpath('.' + ''.join([template % ln for ln in local_name]))
def sattrib(item, attr):
nonlocal spirit
return item.get(f"{{{spirit}}}{attr}")
def xattrib(item, attr):
nonlocal xilinx
return item.get(f"{{{xilinx}}}{attr}")
def stag(item):
nonlocal spirit
return item.tag.replace(f"{{{spirit}}}",'')
def xtag(item):
nonlocal xilinx
return item.tag.replace(f"{{{xilinx}}}",'')
def clean_dependency(string):
return string[string.find("'"): string.rfind(')')].replace(')','')
def get_dependency(item, type_=None):
if type_ is None:
type_ = stag(item)
dependency = get(item, f"vendorExtensions/{type_}Info/enablement/isEnabled")
if dependency is None:
return None
else:
return clean_dependency(xattrib(dependency, 'dependency'))
def get_range(item):
min_ = sattrib(item, 'minimum')
max_ = sattrib(item, 'maximum')
if max_ == None or min_ == None:
return None
else:
return f"From {min_} to {max_}."
def get_choice_type(name):
return name[name.find('_')+1:name.rfind('_')]
def format_default_value(value, fmt):
if fmt == "bitString" and value[0:2].lower() == "0x":
return f"'h{value[2:].upper()}"
if fmt == "bitString" and value[0] == '"' and value[-1] == '"':
return f"'b{value[1:-1]}"
if fmt == "bool":
return value.title()
return value
root = etree.parse(path).getroot()
spirit, xilinx, _ = get_namespaces(root)
vendor = get(root, 'vendor').text
name = get(root, 'name').text
bs = component['bus_interface']
dm = component['bus_domain']
for bus_interface in get_all(root, 'busInterfaces/busInterface'):
bus_name = get(bus_interface, 'name').text
if '_signal_clock' in bus_name:
signal_name = get(get(bus_interface, 'portMaps/portMap'), 'physicalPort/name').text
if signal_name not in dm:
dm[signal_name] = []
dm[signal_name].append(bus_name[0:bus_name.find('_signal_clock')])
continue
if '_signal_reset' in bus_name:
signal_name = get(get(bus_interface, 'portMaps/portMap'), 'physicalPort/name').text
if signal_name not in dm:
dm[signal_name] = []
dm[signal_name].append(bus_name[0:bus_name.find('_signal_reset')])
continue
bs[bus_name] = {
'name': sattrib(get(bus_interface, 'busType'), 'name'),
'dependency': get_dependency(bus_interface, 'busInterface'),
'port_map': {}
}
pm = bs[bus_name]['port_map']
for port_map in get_all(bus_interface, 'portMaps/portMap'):
pm[get(port_map, 'physicalPort/name').text] = {
'logical_port': get(port_map, 'logicalPort/name').text,
'direction': ''
}
lport = component['ports']
for port in get_all(root, 'model/ports/port'):
port_name = get(port, 'name').text
port_direction = get(port, 'wire/direction').text
found = False
for bus in bs:
if port_name in bs[bus]['port_map']:
found = True
bs[bus]['port_map'][port_name]['direction'] = port_direction
break;
if found == False:
lport[port_name] = {
'direction': port_direction,
'dependency': get_dependency(port, 'port')
}
pr = component['parameters']
for parameter in get_all(root, 'parameters/parameter'):
param_description = get(parameter, 'displayName')
if param_description is not None:
param_name = get(parameter, 'name').text
param_value = get(parameter, 'value')
param_format = sattrib(param_value, 'format')
pr[param_name] = {
'description': param_description.text,
'default': format_default_value(param_value.text, param_format),
'type': param_format,
'_choice_ref': sattrib(param_value, 'choiceRef'),
'choices': None,
'range': get_range(param_value)
}
for parameter in get_all(root, 'model/modelParameters/modelParameter'):
param_name = get(parameter, 'name').text
param_type = sattrib(parameter, 'dataType')
if param_type == "std_logic_vector":
param_type = "logic vector"
if param_type is not None and param_name in pr:
if pr[param_name]['type'] is None:
pr[param_name]['type'] = param_type.capitalize()
else:
param_format = pr[param_name]['type']
pr[param_name]['type'] = param_format[0].upper()+param_format[1:]
for choice in get_all(root, 'choices/choice'):
name = get(choice, 'name').text
for key in pr:
if pr[key]['_choice_ref'] == name:
type_ = get_choice_type(name)
values = get_all(choice, 'enumeration')
string = []
if type_ == 'pairs':
for v in values:
string.append(f"{sattrib(v, 'text')} ({v.text})")
elif type_ == 'list':
for v in values:
string.append(v.text)
else:
break
pr[key]['choices'] = ', '.join(string)
break
return component
def manage_hdl_components(env, docnames, libraries):
if not hasattr(env, 'component'):
env.component = {}
cp = env.component
for lib in list(cp):
f = f"../{lib}/component.xml"
if not os.path.isfile(f):
del cp[lib]
for lib, doc in libraries:
f = f"../{lib}/component.xml"
if not os.path.isfile(f):
continue
ctime = os.path.getctime(f)
if lib in cp and cp[lib]['ctime'] >= ctime:
pass
else:
cp[lib] = parse_hdl_component(f, ctime)
docnames.append(doc)
# From https://github.com/tfcollins/vger/blob/main/vger/hdl_reg_map.py
def parse_hdl_regmap(reg, ctime):
regmap = {
'subregmap': {},
'owners':[],
'ctime': ctime
}
with open(f"regmap/adi_regmap_{reg}.txt", "r") as f:
data = f.readlines()
data = [d.replace("\n", "") for d in data]
while "TITLE" in data:
# Get title
tit = data.index("TITLE")
title = str(data[tit + 1].strip())
title_tool = str(data[tit + 2].strip())
data = data[tit + 2 :]
if 'ENDTITLE' in [title_tool, title]:
logger.warning(f"Malformed title fields at file regmap/adi_regmap_{reg}.txt, skipped!")
continue
regmap['subregmap'][title_tool] = {
'title': title,
'regmap': [],
'access_type': []
}
# Get registers
access_type = []
while "REG" in data:
regi = data.index("REG")
rfi = data.index("ENDREG")
if not regi:
break
reg_addr = data[regi + 1].strip()
reg_name = data[regi + 2].strip()
reg_desc = [data[fi].strip() for fi in range(regi + 3, rfi)]
reg_desc = " ".join(reg_desc)
with contextlib.suppress(ValueError):
if tet := data.index("TITLE"):
if regi > tet:
# into next regmap
break
data = data[regi + 1 :]
# Get fields
fields = []
while "FIELD" in data:
fi = data.index("FIELD")
efi = data.index("ENDFIELD")
if not fi:
break
with contextlib.suppress(ValueError):
if rege := data.index("REG"):
if fi > rege:
# into next register
break
field_loc = data[fi + 1].strip()
field_loc = field_loc.split(" ")
field_bits = field_loc[0].replace("[", "").replace("]", "")
field_default = field_loc[1] if len(field_loc) > 1 else "NA"
field_name = data[fi + 2].strip()
field_rw = data[fi + 3].strip()
if field_rw == 'R':
field_rw = 'RO'
elif field_rw == 'W':
field_rw = 'WO'
if '-V' in field_rw:
if 'V' not in access_type:
access_type.append('V')
field_rw_ = field_rw.replace('-V','')
if field_rw_ not in access_type:
if field_rw_ not in hdl_strings.access_type:
logger.warning(f"Malformed access type {field_rw} for register {field_name}, file regmap/adi_regmap_{reg}.txt.")
else:
access_type.append(field_rw)
field_desc = [data[fi].strip() for fi in range(fi + 4, efi)]
field_desc = " ".join(field_desc)
fields.append(
{
"name": field_name,
"bits": field_bits,
"default": field_default,
"rw": field_rw,
"description": field_desc,
}
)
data = data[fi + 1 :]
try:
if '+' in reg_addr:
reg_addr_ = reg_addr.split('+')
reg_addr_[0] = int(reg_addr_[0], 16)
reg_addr_[1] = int(reg_addr_[1].replace('*n',''), 16)
reg_addr_dword = f"{hex(reg_addr_[0])} + {hex(reg_addr_[1])}*n"
reg_addr_byte = f"{hex(reg_addr_[0]<<2)} + {hex(reg_addr_[1]<<2)}*n"
else:
reg_addr_ = int(reg_addr, 16)
reg_addr_dword = f"{hex(reg_addr_)}"
reg_addr_byte = f"{hex(reg_addr_<<2)}"
except:
logger.warning(f"Got malformed register address {reg_addr} for register {reg_name}, file regmap/adi_regmap_{reg}.txt.")
reg_addr_dword = ""
reg_addr_byte = ""
regmap['subregmap'][title_tool]['regmap'].append(
{
'name': reg_name,
'address': [reg_addr_dword, reg_addr_byte],
'description': reg_desc,
'fields': fields
}
)
regmap['subregmap'][title_tool]['access_type'] = access_type
return regmap
def manage_hdl_regmaps(env, docnames):
if not hasattr(env, 'regmaps'):
env.regmaps = {}
rm = env.regmaps
for lib in list(rm):
f = f"regmap/adi_regmap_{lib}.txt"
if not os.path.isfile(f):
del rm[lib]
# Inconsistent naming convention, need to parse all in directory.
files = []
for (dirpath, dirnames, filenames) in os.walk("regmap"):
files.extend(filenames)
break
regmaps = [f.replace('adi_regmap_','').replace('.txt','') for f in files]
for reg_name in regmaps:
ctime = os.path.getctime(f"regmap/adi_regmap_{reg_name}.txt")
if reg_name in rm and rm[reg_name]['ctime'] < ctime:
for o in rm[reg_name]['owners']:
if o not in docnames:
docnames.append(o)
if reg_name in rm and rm[reg_name]['ctime'] >= ctime:
pass
else:
rm[reg_name] = parse_hdl_regmap(reg_name, ctime)
def manage_hdl_artifacts(app, env, docnames):
libraries = [[k.replace('/index',''), k] for k in env.found_docs if k.find('library/') == 0]
manage_hdl_components(env, docnames, libraries)
manage_hdl_regmaps(env, docnames)
def setup(app):
app.add_directive('collapsible', directive_collapsible)
app.add_directive('hdl-parameters', directive_parameters)
app.add_directive('hdl-interfaces', directive_interfaces)
app.add_directive('hdl-regmap', directive_regmap)
for node in [node_div, node_input, node_label, node_icon]:
app.add_node(node,
html =(node.visit, node.depart),
latex=(node.visit, node.depart),
text =(node.visit, node.depart))
app.connect('env-before-read-docs', manage_hdl_artifacts)
app.add_config_value('hide_collapsible_content', dft_hide_collapsible_content, 'env')
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}