blob: 8cd3798b1df303263669ba09d64fefb3874088e8 [file] [log] [blame]
# Copyright 2017 The Cobalt Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generates reference markdown documentation for Starboard modules.
The full process:
1. Preprocess the files to be Doxygen friendly (Doxygenation).
2. Run Doxygen to produce XML that describes each module.
3. Parse the XML and generate Markdown for each module.
"""
import contextlib
from io import StringIO
import logging
import os
import re
import sys
from xml.etree import ElementTree as ET
import doxygen
import environment
from markdown_writer import MarkdownWriter
_HEADER_PATTERN = r'.*\.h$'
_HEADER_RE = re.compile(_HEADER_PATTERN)
_HEADER_XML_PATTERN = r'.*h\.xml$'
_HEADER_XML_RE = re.compile(_HEADER_XML_PATTERN)
_SCRIPT_FILE = os.path.basename(__file__)
_SCRIPT_NAME, _ = os.path.splitext(_SCRIPT_FILE)
_OSS_STARBOARD_VERSIONS = [13, 14, 15]
def _strip(string_or_none):
if string_or_none:
return string_or_none.strip()
return ''
def _get_files(parent_path, regexp):
return sorted([
os.path.realpath(os.path.join(parent_path, x))
for x in os.listdir(parent_path)
if regexp.match(x)
])
def _find_filename(compounddef_element):
return _strip(compounddef_element.findtext('./compoundname'))
def _find_name(member_def):
return _strip(member_def.findtext('./name'))
def _has_innertext(element, query=''):
return bool(_find_innertext(element, query))
def _has_description(memberdef_element):
return (_has_innertext(memberdef_element, './briefdescription') or
_has_innertext(memberdef_element, './detaileddescription'))
def _find_innertext(element, query=''):
if element is None:
return ''
if query:
node = element.find(query)
if node is None:
return ''
else:
node = element
return _strip(''.join(x.strip() for x in node.itertext()))
def _find_memberdefs(compounddef_element, kind):
return compounddef_element.findall(f'./sectiondef/memberdef[@kind="{kind}"]')
def _find_documented_memberdefs(compounddef_element, kind):
memberdef_elements = _find_memberdefs(compounddef_element, kind)
if not memberdef_elements:
return {}
all_memberdefs = {_find_name(x): x for x in memberdef_elements}
def essential(k, v):
return _has_description(v) and not k.endswith('Private')
return {k: v for k, v in all_memberdefs.items() if essential(k, v)}
def _find_structs(compounddef_element, xml_path):
innerclass_elements = compounddef_element.findall('./innerclass')
if not innerclass_elements:
return {}
all_struct_refids = [x.get('refid') for x in innerclass_elements]
all_struct_elements = {}
for refid in all_struct_refids:
struct_xml_path = os.path.join(xml_path, refid + '.xml')
struct_xml = ET.parse(struct_xml_path)
struct_elements = struct_xml.findall(
'.//compounddef[@kind="struct"][compoundname]')
for struct_element in struct_elements:
struct_name = _find_filename(struct_element)
all_struct_elements[struct_name] = struct_element
def essential(k):
return not k.endswith('Private')
return {k: v for k, v in all_struct_elements.items() if essential(k)}
def _find_struct_names(compounddef_element, xml_path):
return set(_find_structs(compounddef_element, xml_path).keys())
def _find_documented_memberdef_names(compounddef_element, *args):
found = set([])
for kind in args:
found.update(_find_documented_memberdefs(compounddef_element, kind).keys())
return found
def _find_member_definition(memberdef_element):
type_element = memberdef_element.find('./type')
type_name = _find_innertext(type_element)
args_name = _strip(memberdef_element.findtext('./argsstring'))
member_name = _strip(memberdef_element.findtext('./name'))
# Doxygen does not handle structs of non-typedef'd function pointers
# gracefully. The 'type" and "argsstring' elements are used to temporarily
# store the information needed to be able to rebuild the full signature, e.g.:
#
# void (*glEnable)(SbGlEnum cap)
#
# type = 'void (*'
# name = 'glEnable'
# argsstring = ')(SbGlEnum cap)'
#
# When we identify these members using the end of 'type' and the beginning of
# 'argsstring' we return the full member signature instead.
if type_name.endswith('(*') and args_name.startswith(')('):
return type_name + member_name + args_name
return type_name + ' ' + member_name
def _node_to_markdown(out, node):
text = node.text.replace('|', '`') if node.text else ''
tail = node.tail.replace('|', '`') if node.tail else ''
if node.tag == 'ndash':
assert not _strip(text)
out.text('\u2013')
elif node.tag == 'mdash':
assert not _strip(text)
out.text('\u2013')
elif node.tag == 'para':
# Block tags should never be nested inside other blocks.
assert not _strip(tail)
out.paragraph()
elif node.tag == 'bold':
assert len(node) == 0
out.bold(text)
elif node.tag == 'computeroutput':
assert len(node) == 0
out.code(text)
elif node.tag == 'ulink':
url = node.get('url')
assert url
out.link(url)
elif node.tag == 'orderedlist':
# List tags should never have any text of their own.
assert not _strip(text)
# Block tags should never be nested inside other blocks.
assert not _strip(tail)
out.ordered_list()
elif node.tag == 'itemizedlist':
# List tags should never have any text of their own.
assert not _strip(text)
# Block tags should never be nested inside other blocks.
assert not _strip(tail)
out.unordered_list()
elif node.tag == 'listitem':
out.item()
elif node.tag == 'heading':
# Block tags should never be nested inside other blocks.
assert not _strip(tail)
try:
levels = int(node.get('level'))
except ValueError:
levels = 1
out.heading(levels=levels)
elif node.tag == 'verbatim':
# Verbatim tags can appear inside paragraphs.
assert len(node) == 0
# Don't replace pipes in verbatim text.
text = node.text if node.text else ''
out.code_block(text)
text = ''
else:
logging.warning('UNHANDLED: %s: %s', node.tag, text.replace('\n', '\\n'))
if text:
out.text(text)
for child in node:
_node_to_markdown(out, child)
if node.tag == 'para':
out.end_paragraph()
elif node.tag == 'ulink':
out.end_link()
elif node.tag in ['orderedlist', 'itemizedlist']:
out.end_list()
elif node.tag == 'heading':
out.end_heading()
out.pop_heading_level()
elif node.tag == 'listitem':
out.end_item()
if tail:
out.text(tail)
def _description_to_markdown(out, description_element):
if description_element is None:
return
for child in description_element:
_node_to_markdown(out, child)
def _emit_doc_header(out_io):
out_io.write('Project: /youtube/cobalt/_project.yaml\n')
out_io.write('Book: /youtube/cobalt/_book.yaml\n\n')
def _emit_description(out, memberdef_element):
_description_to_markdown(out, memberdef_element.find('./briefdescription'))
_description_to_markdown(out, memberdef_element.find('./detaileddescription'))
def _emit_macro(out, memberdef_element):
name = _find_name(memberdef_element)
assert name or _has_description(memberdef_element)
params = ''
param_defs = memberdef_element.findall('./param/defname')
if param_defs:
param_names = [_strip(x.text) for x in param_defs]
params = f'({", ".join(param_names)})'
logging.info('Macro: %s%s', name, params)
with out.auto_scoped_heading(name + params):
_emit_description(out, memberdef_element)
def _emit_enum(out, memberdef_element):
name = _find_name(memberdef_element)
assert name or _has_description(memberdef_element)
logging.info('Enum: %s', name)
with out.auto_scoped_heading(name):
_emit_description(out, memberdef_element)
with out.auto_scoped_heading('Values'):
with out.auto_unordered_list():
for enumvalue_element in memberdef_element.findall('./enumvalue'):
with out.auto_item():
out.code(_find_name(enumvalue_element))
_emit_description(out, enumvalue_element)
def _emit_typedef(out, memberdef_element):
name = _find_name(memberdef_element)
assert name or _has_description(memberdef_element)
with out.auto_scoped_heading(name):
_emit_description(out, memberdef_element)
definition = _strip(memberdef_element.findtext('./definition'))
if definition:
with out.auto_scoped_heading('Definition'):
out.code_block(definition)
def _emit_struct(out, compounddef_element):
name = _find_filename(compounddef_element)
assert name or _has_description(compounddef_element)
logging.info('Struct: %s', name)
with out.auto_scoped_heading(name):
_emit_description(out, compounddef_element)
memberdef_elements = _find_memberdefs(compounddef_element, 'variable')
if memberdef_elements:
with out.auto_scoped_heading('Members'):
with out.auto_unordered_list():
for memberdef_element in memberdef_elements:
with out.auto_item():
out.code(_find_member_definition(memberdef_element))
_emit_description(out, memberdef_element)
def _emit_variable(out, memberdef_element):
name = _find_name(memberdef_element)
assert name or _has_description(memberdef_element)
logging.info('Variable: %s', name)
with out.auto_scoped_heading(name):
_emit_description(out, memberdef_element)
def _emit_function(out, memberdef_element):
name = _find_name(memberdef_element)
assert name or _has_description(memberdef_element)
logging.info('Function: %s', name)
with out.auto_scoped_heading(name):
_emit_description(out, memberdef_element)
prototype = memberdef_element.findtext('./definition') + \
memberdef_element.findtext('./argsstring')
if prototype:
with out.auto_scoped_heading('Declaration'):
out.code_block(prototype)
def _emit_macros(out, compounddef_element):
member_dict = _find_documented_memberdefs(compounddef_element, 'define')
if not member_dict:
return False
with out.auto_scoped_heading('Macros'):
for name in sorted(member_dict.keys()):
_emit_macro(out, member_dict[name])
return True
def _emit_enums(out, compounddef_element):
member_dict = _find_documented_memberdefs(compounddef_element, 'enum')
if not member_dict:
return False
with out.auto_scoped_heading('Enums'):
for name in sorted(member_dict.keys()):
_emit_enum(out, member_dict[name])
return True
def _emit_typedefs(out, compounddef_element, xml_path):
member_dict = _find_documented_memberdefs(compounddef_element, 'typedef')
redundant_set = _find_documented_memberdef_names(compounddef_element,
'define', 'enum', 'function')
redundant_set |= _find_struct_names(compounddef_element, xml_path)
essential_set = set(member_dict.keys()) - redundant_set
if not essential_set:
return False
with out.auto_scoped_heading('Typedefs'):
for name in sorted(essential_set):
_emit_typedef(out, member_dict[name])
return True
def _emit_structs(out, compounddef_element, xml_path):
struct_dict = _find_structs(compounddef_element, xml_path)
if not struct_dict:
return False
with out.auto_scoped_heading('Structs'):
for name in sorted(struct_dict.keys()):
_emit_struct(out, struct_dict[name])
return True
def _emit_variables(out, compounddef_element):
member_dict = _find_documented_memberdefs(compounddef_element, 'variable')
if not member_dict:
return False
with out.auto_scoped_heading('Variables'):
for name in sorted(member_dict.keys()):
_emit_variable(out, member_dict[name])
return True
def _emit_functions(out, compounddef_element):
member_dict = _find_documented_memberdefs(compounddef_element, 'function')
if not member_dict:
return False
with out.auto_scoped_heading('Functions'):
for name in sorted(member_dict.keys()):
_emit_function(out, member_dict[name])
return True
def _emit_file(out_io, compounddef_element, xml_path):
header_filename = _find_filename(compounddef_element)
logging.info('File: %s', header_filename)
_emit_doc_header(out_io)
mdwriter = MarkdownWriter(out_io)
with mdwriter.auto_scoped_heading(
f'Starboard Module Reference: `{header_filename}`'):
_emit_description(mdwriter, compounddef_element)
# When an API is deprecated it will be removed via #ifdef. When this is the
# case, we will no longer have macros, enums, typedefs, structs, or
# functions and thus the API should not be included in the site.
has_content = _emit_macros(mdwriter, compounddef_element)
has_content = _emit_enums(mdwriter, compounddef_element) or has_content
has_content = _emit_typedefs(mdwriter, compounddef_element,
xml_path) or has_content
has_content = _emit_structs(mdwriter, compounddef_element,
xml_path) or has_content
has_content = _emit_variables(mdwriter, compounddef_element) or has_content
has_content = _emit_functions(mdwriter, compounddef_element) or has_content
return has_content
def generate(source_dir, output_dir):
if output_dir:
site_path = environment.get_site_dir(output_dir)
else:
site_path = environment.get_site_dir(source_dir)
doc_dir_path = os.path.join(site_path, 'docs', 'reference', 'starboard',
'modules')
environment.make_clean_dirs(doc_dir_path)
starboard_directory_path = environment.get_starboard_dir(source_dir)
starboard_files = _get_files(starboard_directory_path, _HEADER_RE)
with environment.mkdtemp(suffix='.' + _SCRIPT_NAME) as temp_directory_path:
logging.debug('Working directory: %s', temp_directory_path)
doxygenated_directory_path = os.path.join(temp_directory_path,
'doxygenated')
doxygenated_files = doxygen.doxygenate(starboard_files,
doxygenated_directory_path)
doxygen_directory_path = os.path.join(temp_directory_path, 'doxygen')
for sb_version in _OSS_STARBOARD_VERSIONS:
version_path = os.path.join(doxygen_directory_path, str(sb_version))
version_doc_dir_path = os.path.join(doc_dir_path, str(sb_version))
doxygen.doxygen(sb_version, doxygenated_files, [], version_path)
doxygen_xml_path = os.path.join(version_path, 'xml')
for header_xml_path in _get_files(doxygen_xml_path, _HEADER_XML_RE):
header_xml = ET.parse(header_xml_path)
for compounddef_element in header_xml.findall(
'.//compounddef[@kind="file"][compoundname]'):
environment.make_dirs(version_doc_dir_path)
header_filename = _find_filename(compounddef_element)
doc_filename = (
os.path.splitext(os.path.basename(header_filename))[0] + '.md')
with contextlib.closing(StringIO()) as doc_file:
if not _emit_file(doc_file, compounddef_element, doxygen_xml_path):
continue
doc_contents = doc_file.getvalue()
doc_file_path = os.path.join(version_doc_dir_path, doc_filename)
environment.write_file(doc_file_path, doc_contents)
# Make the latest Starboard documentation version the default version.
if sb_version == _OSS_STARBOARD_VERSIONS[-1]:
doc_file_path = os.path.join(doc_dir_path, doc_filename)
environment.write_file(doc_file_path, doc_contents)
return 0
def main(argv):
environment.setup_logging()
options = environment.parse_arguments(__doc__, argv)
environment.set_log_level(options.log_delta)
return generate(options.source, options.out)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))