# Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors # This file is not part of GNU Emacs. # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # You should have received a copy of the GNU General Public License along with # this program. If not, see . import re from string import Template import requests from docutils import nodes from sphinx.roles import XRefRole from sphinx.util import ws_re, logging logger = logging.getLogger(__name__) # Regular expression object to parse the contents of an Info reference # role. INFO_RE = re.compile(r'\A\((?P[^)]+)\)(?P.+)\Z') class InfoNodeXRefRole(XRefRole): """A role to reference a node in an Info manual.""" innernodeclass = nodes.emphasis def process_link(self, env, refnode, has_explicit_title, title, target): """Process the link created by this role. Swap node and manual name, to more closely match the look of references in Texinfo. """ # Normalize whitespace in info node targets target = ws_re.sub(' ', target) refnode['has_explicit_title'] = has_explicit_title if not has_explicit_title: match = INFO_RE.match(target) if match: # Swap title and node to create a title like info does title = '{0}({1})'.format(match.group('node'), match.group('manual')) return title, target def node_encode(char): if char.isalnum(): return char elif char == ' ': return '-' else: return '_00{:02x}'.format(ord(char)) def expand_node_name(node): """Expand ``node`` for use in HTML. ``node`` is the name of a node as string. Return a pair ``(filename, anchor)``, where ``filename`` is the base-name of the corresponding file, sans extension, and ``anchor`` the HTML anchor. See http://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref-Node-Name-Expansion.html. """ if node == 'Top': return ('index', 'Top') else: normalized = ws_re.sub(' ', node.strip()) encoded = ''.join(node_encode(c) for c in normalized) prefix = 'g_t' if not node[0].isalpha() else '' return (encoded, prefix + encoded) class HTMLXRefDB(object): """Cross-reference database for Info manuals.""" #: URL of the htmlxref database of GNU Texinfo XREF_URL = 'http://ftpmirror.gnu.org/texinfo/htmlxref.cnf' #: Regular expression to parse entries from an xref DB XREF_RE = re.compile(r""" ^\s* (?: (?P[#].*) | (?P\w+)\s*=\s*(?P\S+) | (?P\w+)\s*(?Pnode|mono)\s*(?P\S+) ) \s*$""", re.VERBOSE) @classmethod def parse(cls, htmlxref): substitutions = {} manuals = {} for line in htmlxref.splitlines(): match = cls.XREF_RE.match(line) if match: if match.group('substname'): url = Template(match.group('substurl')).substitute( substitutions) substitutions[match.group('substname')] = url elif (match.group('manname') and match.group('mantype') == 'node'): url = Template(match.group('manurl')).substitute( substitutions) manuals[match.group('manname')] = url return cls(manuals) def __init__(self, entries): """Initialize the HTMLXrefDB object with the provided entries.""" self.entries = entries def resolve(self, manual, node): manual_url = self.entries.get(manual) if not manual_url: return None else: filename, anchor = expand_node_name(node) return manual_url + filename + '.html#' + anchor def update_htmlxref(app): if not isinstance(getattr(app.env, 'info_htmlxref', None), HTMLXRefDB): logger.info('fetching Texinfo htmlxref database from {0}... '.format( HTMLXRefDB.XREF_URL)) try: app.env.info_htmlxref = HTMLXRefDB.parse( requests.get(HTMLXRefDB.XREF_URL).text) except requests.exceptions.ConnectionError: logger.warning('Failed to load xref DB. ' 'Info references will not be resolved') app.env.info_htmlxref = None def resolve_info_references(app, _env, refnode, contnode): """Resolve Info references. Process all :class:`~sphinx.addnodes.pending_xref` nodes whose ``reftype`` is ``infonode``. Replace the pending reference with a :class:`~docutils.nodes.reference` node, which references the corresponding web URL, as stored in the database referred to by :data:`HTMLXREF_URL`. """ if refnode['reftype'] != 'infonode': return None target = ws_re.sub(' ', refnode['reftarget']) match = INFO_RE.match(target) if not match: logger.warning('Invalid info target: {0}'.format(target), location=(refnode.source, refnode.line)) return contnode manual = match.group('manual') node = match.group('node') xrefdb = app.env.info_htmlxref if xrefdb: uri = xrefdb.resolve(manual, node) if not uri: message = 'Cannot resolve info manual {0}'.format(manual) logger.warning(message, location=(refnode.source, refnode.line)) return contnode else: reference = nodes.reference('', '', internal=False, refuri=uri, reftitle=target) reference += contnode return reference else: # Without an xref DB we're unable to resolve any info references return None def setup(app): app.add_role('infonode', InfoNodeXRefRole()) app.connect(str('builder-inited'), update_htmlxref) app.connect(str('missing-reference'), resolve_info_references) return {'version': '0.1', 'parallel_read_safe': True}