# 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}