From ac1da96a7b2969c36b86daaa5b281e5e7608d673 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 17 Jun 2018 11:41:20 +0200 Subject: [PATCH] doxygen: don't crash on anchors with IDs that don't match the compound. --- doxygen/dox2html5.py | 62 +++++++++++-------- .../Doxyfile | 14 +++++ .../File.h | 20 ++++++ .../namespaceFoo.html | 49 +++++++++++++++ doxygen/test/test_contents.py | 8 +++ 5 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 doxygen/test/contents_anchor_in_both_group_and_namespace/Doxyfile create mode 100644 doxygen/test/contents_anchor_in_both_group_and_namespace/File.h create mode 100644 doxygen/test/contents_anchor_in_both_group_and_namespace/namespaceFoo.html diff --git a/doxygen/dox2html5.py b/doxygen/dox2html5.py index 0a758f46..7d06c56f 100755 --- a/doxygen/dox2html5.py +++ b/doxygen/dox2html5.py @@ -355,7 +355,8 @@ class State: self.images: List[str] = [] self.current = '' self.current_prefix = [] - self.current_compound = None + self.current_compound_url = None + self.current_definition_url_base = None self.parsing_toplevel_desc = False def slugify(text: str) -> str: @@ -405,22 +406,30 @@ def parse_ref(state: State, element: ET.Element) -> str: return '{}'.format(url, class_, add_wbr(parse_inline_desc(state, element).strip())) -def parse_id(element: ET.Element) -> Tuple[str, str]: +def parse_id(element: ET.Element) -> Tuple[str, str, str]: + # Returns URL base (usually saved to state.current_definition_url_base and + # used by extract_id_hash() later), base URL (with file extension), and the + # actual ID id = element.attrib['id'] i = id.rindex('_1') - base_url = id[:i] + '.html' - return base_url, id[i+2:] + return id[:i], id[:i] + '.html', id[i+2:] def extract_id_hash(state: State, element: ET.Element) -> str: # Can't use parse_id() here as sections with _1 in it have it verbatim - # unescaped and mess up with the rindex(). OTOH, can't use this approach in + # unescaped and mess up with rindex(). OTOH, can't use this approach in # parse_id() because for example enums can be present in both file and # namespace documentation, having the base_url either the file one or the # namespace one, depending on what's documented better. Ugh. See the # contents_section_underscore_one test for a verification. + # + # Can't use current compount URL base here, as definitions can have + # different URL base (again an enum being present in both file and + # namespace documentation). The state.current_definition_url_base usually + # comes from parse_id()[0]. See the + # contents_anchor_in_both_group_and_namespace test for a verification. id = element.attrib['id'] - assert id.startswith(state.current_compound.url_base) - return id[len(state.current_compound.url_base)+2:] + assert id.startswith(state.current_definition_url_base) + return id[len(state.current_definition_url_base)+2:] def fix_type_spacing(type: str) -> str: return type.replace('< ', '<').replace(' >', '>').replace(' &', '&').replace(' *', '*') @@ -1450,7 +1459,7 @@ def parse_enum(state: State, element: ET.Element): assert element.tag == 'memberdef' and element.attrib['kind'] == 'enum' enum = Empty() - enum.base_url, enum.id = parse_id(element) + state.current_definition_url_base, enum.base_url, enum.id = parse_id(element) enum.type = parse_type(state, element.find('type')) enum.name = element.find('name').text if enum.name.startswith('@'): enum.name = '(anonymous)' @@ -1468,8 +1477,7 @@ def parse_enum(state: State, element: ET.Element): value = Empty() # The base_url might be different than state.current_compound.url, but # should be the same as enum.base_url - value_base_url, value.id = parse_id(enumvalue) - assert value_base_url == enum.base_url + value.id = extract_id_hash(state, enumvalue) value.name = enumvalue.find('name').text # There can be an implicit initializer for enum value value.initializer = html.escape(enumvalue.findtext('initializer', '')) @@ -1478,7 +1486,7 @@ def parse_enum(state: State, element: ET.Element): value.description, value_search_keywords, value.is_deprecated = parse_enum_value_desc(state, enumvalue) if value.description: enum.has_value_details = True - if enum.base_url == state.current_compound.url and not state.doxyfile['M_SEARCH_DISABLED']: + if enum.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.ENUM_VALUE|(ResultFlag.DEPRECATED if value.is_deprecated else ResultFlag(0)) result.url = enum.base_url + '#' + value.id @@ -1490,9 +1498,9 @@ def parse_enum(state: State, element: ET.Element): state.search += [result] enum.values += [value] - enum.has_details = enum.base_url == state.current_compound.url and (enum.description or enum.has_value_details) + enum.has_details = enum.base_url == state.current_compound_url and (enum.description or enum.has_value_details) if enum.brief or enum.has_details or enum.has_value_details: - if enum.base_url == state.current_compound.url and not state.doxyfile['M_SEARCH_DISABLED']: + if enum.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.ENUM|(ResultFlag.DEPRECATED if enum.is_deprecated else ResultFlag(0)) result.url = enum.base_url + '#' + enum.id @@ -1544,7 +1552,7 @@ def parse_typedef(state: State, element: ET.Element): assert element.tag == 'memberdef' and element.attrib['kind'] == 'typedef' typedef = Empty() - typedef.base_url, typedef.id = parse_id(element) + state.current_definition_url_base, typedef.base_url, typedef.id = parse_id(element) typedef.is_using = element.findtext('definition', '').startswith('using') typedef.type = parse_type(state, element.find('type')) typedef.args = parse_type(state, element.find('argsstring')) @@ -1554,10 +1562,10 @@ def parse_typedef(state: State, element: ET.Element): typedef.is_protected = element.attrib['prot'] == 'protected' typedef.has_template_details, typedef.templates = parse_template_params(state, element.find('templateparamlist'), templates) - typedef.has_details = typedef.base_url == state.current_compound.url and (typedef.description or typedef.has_template_details) + typedef.has_details = typedef.base_url == state.current_compound_url and (typedef.description or typedef.has_template_details) if typedef.brief or typedef.has_details: # Avoid duplicates in search - if typedef.base_url == state.current_compound.url and not state.doxyfile['M_SEARCH_DISABLED']: + if typedef.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.TYPEDEF|(ResultFlag.DEPRECATED if typedef.is_deprecated else ResultFlag(0)) result.url = typedef.base_url + '#' + typedef.id @@ -1572,7 +1580,7 @@ def parse_func(state: State, element: ET.Element): assert element.tag == 'memberdef' and element.attrib['kind'] == 'function' func = Empty() - func.base_url, func.id = parse_id(element) + state.current_definition_url_base, func.base_url, func.id = parse_id(element) func.type = parse_type(state, element.find('type')) func.name = fix_type_spacing(html.escape(element.find('name').text)) func.brief = parse_desc(state, element.find('briefdescription')) @@ -1659,10 +1667,10 @@ def parse_func(state: State, element: ET.Element): # Some param description got unused if params: logging.warning("{}: function parameter description doesn't match parameter names: {}".format(state.current, repr(params))) - func.has_details = func.base_url == state.current_compound.url and (func.description or func.has_template_details or func.has_param_details or func.return_value or func.return_values or func.exceptions) + func.has_details = func.base_url == state.current_compound_url and (func.description or func.has_template_details or func.has_param_details or func.return_value or func.return_values or func.exceptions) if func.brief or func.has_details: # Avoid duplicates in search - if func.base_url == state.current_compound.url and not state.doxyfile['M_SEARCH_DISABLED']: + if func.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.FUNC|(ResultFlag.DEPRECATED if func.is_deprecated else ResultFlag(0))|(ResultFlag.DELETED if func.is_deleted else ResultFlag(0)) result.url = func.base_url + '#' + func.id @@ -1679,7 +1687,7 @@ def parse_var(state: State, element: ET.Element): assert element.tag == 'memberdef' and element.attrib['kind'] == 'variable' var = Empty() - var.base_url, var.id = parse_id(element) + state.current_definition_url_base, var.base_url, var.id = parse_id(element) var.type = parse_type(state, element.find('type')) if var.type.startswith('constexpr'): var.type = var.type[10:] @@ -1693,10 +1701,10 @@ def parse_var(state: State, element: ET.Element): var.brief = parse_desc(state, element.find('briefdescription')) var.description, search_keywords, var.is_deprecated = parse_var_desc(state, element) - var.has_details = var.base_url == state.current_compound.url and var.description + var.has_details = var.base_url == state.current_compound_url and var.description if var.brief or var.has_details: # Avoid duplicates in search - if var.base_url == state.current_compound.url and not state.doxyfile['M_SEARCH_DISABLED']: + if var.base_url == state.current_compound_url and not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.VAR|(ResultFlag.DEPRECATED if var.is_deprecated else ResultFlag(0)) result.url = var.base_url + '#' + var.id @@ -1737,7 +1745,7 @@ def parse_define(state: State, element: ET.Element): if not state.doxyfile['M_SEARCH_DISABLED']: result = Empty() result.flags = ResultFlag.DEFINE|(ResultFlag.DEPRECATED if define.is_deprecated else ResultFlag(0)) - result.url = state.current_compound.url + '#' + define.id + result.url = state.current_compound_url + '#' + define.id result.prefix = [] result.name = define.name result.keywords = search_keywords @@ -2039,8 +2047,12 @@ def parse_xml(state: State, xml: str): # for pages because that doesn't reflect CASE_SENSE_NAMES. THANKS DOXYGEN. compound.url_base = ('index' if compound.id == 'indexpage' else compound.id) compound.url = compound.url_base + '.html' - # Save current compound URL for search data building and ID extraction - state.current_compound = compound + # Save current compound URL for search data building and ID extraction, + # save current URL prefix for extract_id_hash() (these are the same for + # top-level desc, but usually not for definitions, as an enum can be both + # in file docs and namespace docs, for example) + state.current_compound_url = compound.url + state.current_definition_url_base = compound.url_base compound.has_template_details = False compound.templates = None compound.brief = parse_desc(state, compounddef.find('briefdescription')) diff --git a/doxygen/test/contents_anchor_in_both_group_and_namespace/Doxyfile b/doxygen/test/contents_anchor_in_both_group_and_namespace/Doxyfile new file mode 100644 index 00000000..5945b1ee --- /dev/null +++ b/doxygen/test/contents_anchor_in_both_group_and_namespace/Doxyfile @@ -0,0 +1,14 @@ +INPUT = File.h +QUIET = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_PROGRAMLISTING = NO + +M_PAGE_FINE_PRINT = +M_THEME_COLOR = +M_FAVICON = +M_LINKS_NAVBAR1 = +M_LINKS_NAVBAR2 = +M_SEARCH_DISABLED = YES + diff --git a/doxygen/test/contents_anchor_in_both_group_and_namespace/File.h b/doxygen/test/contents_anchor_in_both_group_and_namespace/File.h new file mode 100644 index 00000000..bb33bf3d --- /dev/null +++ b/doxygen/test/contents_anchor_in_both_group_and_namespace/File.h @@ -0,0 +1,20 @@ +/** @defgroup fizzbuzz A group +@{ */ + +/*@}*/ + +/** @brief Foo */ +namespace Foo { + +/** +@brief Bar + +@ingroup fizzbuzz + +@anchor this_anchor + +@link this_anchor More details @endlink +*/ +void bar(); + +} diff --git a/doxygen/test/contents_anchor_in_both_group_and_namespace/namespaceFoo.html b/doxygen/test/contents_anchor_in_both_group_and_namespace/namespaceFoo.html new file mode 100644 index 00000000..46257090 --- /dev/null +++ b/doxygen/test/contents_anchor_in_both_group_and_namespace/namespaceFoo.html @@ -0,0 +1,49 @@ + + + + + Foo namespace | My Project + + + + + +
+
+
+
+
+

Foo namespace

+

Foo.

+
+

Contents

+ +
+
+

Functions

+
+
+ void bar() +
+
Bar.
+
+
+
+
+
+
+ + diff --git a/doxygen/test/test_contents.py b/doxygen/test/test_contents.py index c916e8d1..8a25a66f 100644 --- a/doxygen/test/test_contents.py +++ b/doxygen/test/test_contents.py @@ -300,3 +300,11 @@ class SectionInFunction(IntegrationTestCase): def test(self): self.run_dox2html5(wildcard='File_8h.xml') self.assertEqual(*self.actual_expected_contents('File_8h.html')) + +class AnchorInBothGroupAndNamespace(IntegrationTestCase): + def __init__(self, *args, **kwargs): + super().__init__(__file__, 'anchor_in_both_group_and_namespace', *args, **kwargs) + + def test(self): + self.run_dox2html5(wildcard='namespaceFoo.xml') + self.assertEqual(*self.actual_expected_contents('namespaceFoo.html')) -- 2.30.2