# .. Copyright (C) 2012-2016 Bryan A. Jones.
#
# This file is part of CodeChat.
#
# CodeChat 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.
#
# CodeChat 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 CodeChat. If not, see .
#
# *************************************************************************
# CodeToRestSphinx.py - a Sphinx extension to translate source code to reST
# *************************************************************************
# This modules enables Sphinx to read in source files by converting the source
# code to reST before passing the file on to Sphinx. The overall design:
#
# 1. Monkeypatch_ Sphinx to include source files in the build, keeping the
# source file's extension intact. (Sphinx strips the extension of reST
# files).
# 2. When Sphinx `reads a source file `_, check to see if the
# file's extension is intact. If so, it's a source file; translate it to reST
# then pass it on to Sphinx.
#
# .. contents::
#
# Imports
# =======
# These are listed in the order prescribed by `PEP 8
# `_.
#
# Standard library
# ----------------
import os.path
from os import path
# For glob_to_lexer matching.
import fnmatch
# For saving Enki info.
import codecs
#
# Third-party imports
# -------------------
from sphinx.util import get_matching_files
import sphinx.environment
from sphinx.util.osutil import SEP
import pygments.util
#
# Local application imports
# -------------------------
from .CodeToRest import code_to_rest_string, get_lexer
from .CommentDelimiterInfo import SUPPORTED_GLOBS
from . import __version__
#
# source-read event
# =================
# The source-read_ event occurs when a source file is read. If it's code, this
# routine changes it into reST.
def _source_read(
# .. _app:
#
# The `Sphinx application object `_.
app,
# The name of the document that was read. It contains a path relative to the
# project directory and (typically) no extension.
docname,
# A list whose single element is the contents of the source file.
source):
# If the docname's extension doesn't change when asking for its full path,
# then it's source code. Normally, the docname of ``foo.rst`` is ``foo``;
# only for source code is the docname of ``foo.c`` also ``foo.c``. Look up
# the name and extension using `doc2path
# `_.
docname_ext = app.env.doc2path(docname, None)
if os.path.normpath(docname_ext) == os.path.normpath(docname):
# See if it's an extension we should process.
try:
base_docname = os.path.basename(docname)
# See if ``source_file`` matches any of the globs.
lexer = None
lfg = app.config.CodeChat_lexer_for_glob
for glob, lexer_alias in lfg.items():
if fnmatch.fnmatch(base_docname, glob):
# On a match, pass the specified lexer alias.
lexer = get_lexer(alias=lexer_alias)
break
# Do this after checking the CodeChat_lexer_for_glob list, since
# this will raise an exception on failure.
lexer = lexer or get_lexer(filename=base_docname, code=source[0])
app.info('Converted using the {} lexer.'.format(lexer.name))
source[0] = code_to_rest_string(source[0], lexer=lexer)
except (KeyError, pygments.util.ClassNotFound):
# We don't support this language.
pass
#
# Monkeypatch
# ===========
# Sphinx doesn't naturally look for source files. Simply adding all supported
# source file extensions to ``conf.py``'s `source_suffix `_
# doesn't work, since ``foo.c`` and ``foo.h`` will now both been seen as the
# docname ``foo``, making then indistinguishable.
#
# get_matching_docs patch
# -----------------------
# So, do a bit of monkeypatching: for source files, make their docname the same
# as the file name; for reST file, allow Sphinx to strip off the extension as
# before. The first patch accomplishes this. It comes from ``sphinx.util``, line
# 92 and following in Sphinx 1.3.1.
def _get_matching_docs(dirname, suffixes, exclude_matchers=()):
"""Get all file names (without suffixes) matching a suffix in a directory,
recursively.
Exclude files and dirs matching a pattern in *exclude_patterns*.
"""
suffixpatterns = ['*' + s for s in suffixes]
# The following line was added.
source_suffixpatterns = ( SUPPORTED_GLOBS |
set(_config.CodeChat_lexer_for_glob.keys()) )
for filename in get_matching_files(dirname, exclude_matchers):
for suffixpattern in suffixpatterns:
if fnmatch.fnmatch(filename, suffixpattern):
yield filename[:-len(suffixpattern)+1]
break
# The following code was added.
for source_suffixpattern in source_suffixpatterns:
if fnmatch.fnmatch(filename, source_suffixpattern):
yield filename
break
# Note that this is referenced in ``sphinx.environment`` by ``from sphinx.util
# import get_matching_docs``. So, `where to patch `_
# is in ``sphinx.environment`` instead of ``sphinx.util``.
sphinx.environment.get_matching_docs = _get_matching_docs
# doc2path patch
# --------------
# Next, the way docnames get transformed back to a full path needs to be fixed
# for source files. Specifically, a docname might be the source file, without
# adding an extension. This code comes from ``sphinx.environment`` of Sphinx
# 1.3.1. See also the official `doc2path `_
# Sphinx docs.
def _doc2path(self, docname, base=True, suffix=None):
"""Return the filename for the document name.
If *base* is True, return absolute path under self.srcdir.
If *base* is None, return relative path to self.srcdir.
If *base* is a path string, return absolute path under that.
If *suffix* is not None, add it instead of config.source_suffix.
"""
docname = docname.replace(SEP, path.sep)
if suffix is None:
for candidate_suffix in self.config.source_suffix:
if path.isfile(path.join(self.srcdir, docname) +
candidate_suffix):
suffix = candidate_suffix
break
else:
# Three lines of code added here -- check for the no-extenion case.
if path.isfile(path.join(self.srcdir, docname)):
suffix = ''
else:
# document does not exist
suffix = self.config.source_suffix[0]
if base is True:
return path.join(self.srcdir, docname) + suffix
elif base is None:
return docname + suffix
else:
return path.join(base, docname) + suffix
sphinx.environment.BuildEnvironment.doc2path = _doc2path
#
# Enki_ support
# =============
# `Enki `_, which hosts CodeChat, needs to know the
# HTML file extension. So, save it to a file for Enki_ to read. Note that this
# can't be done in `Extension setup`_, since the values in ``conf.py`` aren't
# loaded yet. See also global_config_. Instead, wait for the builder-inited_
# event, when the config_ settings are available.
def _builder_inited(
# See app_.
app):
try:
with codecs.open('sphinx-enki-info.txt', 'wb', 'utf-8') as f:
f.write(app.config.html_file_suffix)
except TypeError:
# If ``html_file_suffix`` is None (TypeError), Enki will assume
# ``.html``.
pass
#
# Extension setup
# ===============
# This routine defines the `entry point
# `_ called by Sphinx to initialize
# this extension.
def setup(
# See app_.
app):
# Ensure we're using at least Sphinx v1.3 using `require_sphinx
# `_.
app.require_sphinx('1.3')
# Use the `source-read `_
# event hook to transform source code to reST before Sphinx processes it.
app.connect('source-read', _source_read)
# Add the CodeChat.css style sheet using `add_stylesheet
# `_.
app.add_stylesheet('CodeChat.css')
# Add the CodeChat_lexer_for_glob config value. See `add_config_value
# `_.
app.add_config_value('CodeChat_lexer_for_glob', {}, 'html')
# Use the `builder-inited `_
# event to write out settings specified in ``conf.py``.
app.connect('builder-inited', _builder_inited)
# .. _global_config:
#
# An ugly hack: we need to get to the `Config `_
# object after ``conf.py``'s values have been loaded. They aren't loaded
# yet, so we store the ``config`` object to access it later when it is
# loaded.
global _config
_config = app.config
# Return `extension metadata `_.
return {'version' : __version__,
'parallel_read_safe' : True }