Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an include directive that allows one filetype to be included in another #7739

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/usage/restructuredtext/directives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,21 @@ __ http://pygments.org/docs/lexers
.. versionchanged:: 2.1
Added the ``force`` option.


.. rst:directive:: .. docinclude:: filename

Includes another document in this one. Unlike :rst:dir:`include`, this
allows inclusion of documents that use different parsers to their parent
document, such as including a markdown file in an rst file.

.. rst:directive:option:: start-line
end-line
start-after
end-after
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these filtering options needed? In my experience of literalinclude directive, many filter options are requested. So I hesitate to add filtering options.

Copy link
Contributor Author

@eric-wieser eric-wieser May 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, see pygae/galgebra#413 for a useful application. It allows me to split a README.md into pieces, and include each one in a different rst document.

These filters are part of the built-in .. include, so it seems reasonable to keep them anyway.

encoding

Options treated the same way as the :rst:dir:`include` directive.

.. _glossary-directive:

Glossary
Expand Down
93 changes: 91 additions & 2 deletions sphinx/directives/other.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,25 @@
:license: BSD, see LICENSE for details.
"""

import os
import re
from typing import Any, Dict, List
from typing import cast

from docutils import nodes
from docutils import nodes, io
from docutils.nodes import Element, Node
from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.parsers.rst.directives.misc import Class
from docutils.parsers.rst.directives.misc import Include as BaseInclude
from docutils.utils import new_document, relative_path
from docutils.utils.error_reporting import SafeString, ErrorString

from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.domains.changeset import VersionChange # NOQA # for compatibility
from sphinx.locale import _
from sphinx.util import url_re, docname_join
from sphinx.util import url_re, docname_join, get_filetype
from sphinx.util.docutils import SphinxDirective
from sphinx.util.matching import Matcher, patfilter
from sphinx.util.nodes import explicit_title_re
Expand Down Expand Up @@ -361,6 +364,91 @@ def run(self) -> List[Node]:
return super().run()


class IncludeDocument(SphinxDirective):

""" Include one document in another. The language need not match """

required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {'start-line': int,
'end-line': int,
'start-after': directives.unchanged_required,
'end-before': directives.unchanged_required,
'encoding': directives.encoding}

standard_include_path = BaseInclude.standard_include_path

def run(self):
"""Include a file as part of the content of this reST file."""

# copied from docutils.parsers.rst.directives.misc.Include
if not self.state.document.settings.file_insertion_enabled:
raise self.warning('"%s" directive disabled.' % self.name)
source = self.state_machine.input_lines.source(
self.lineno - self.state_machine.input_offset - 1)
source_dir = os.path.dirname(os.path.abspath(source))
path = directives.path(self.arguments[0])
if path.startswith('<') and path.endswith('>'):
path = os.path.join(self.standard_include_path, path[1:-1])
path = os.path.normpath(os.path.join(source_dir, path))
path = relative_path(None, path)
encoding = self.options.get(
'encoding', self.state.document.settings.input_encoding)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.source_encoding is better for the default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is copied from the built-in ..include

e_handler = self.state.document.settings.input_encoding_error_handler
try:
self.state.document.settings.record_dependencies.add(path)
include_file = io.FileInput(source_path=path,
encoding=encoding,
error_handler=e_handler)
except UnicodeEncodeError:
raise self.severe(u'Problems with "%s" directive path:\n'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

u prefix is no longer needed, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed - it's here because this code is copied directly from docutils

'Cannot encode input file path "%s" '
'(wrong locale?).' %
(self.name, SafeString(path)))
except IOError as error:
raise self.severe(u'Problems with "%s" directive path:\n%s.' %
(self.name, ErrorString(error)))
startline = self.options.get('start-line', None)
endline = self.options.get('end-line', None)
try:
if startline or (endline is not None):
lines = include_file.readlines()
rawtext = ''.join(lines[startline:endline])
else:
rawtext = include_file.read()
except UnicodeError as error:
raise self.severe(u'Problem with "%s" directive:\n%s' %
(self.name, ErrorString(error)))
# start-after/end-before: no restrictions on newlines in match-text,
# and no restrictions on matching inside lines vs. line boundaries
after_text = self.options.get('start-after', None)
if after_text:
# skip content in rawtext before *and incl.* a matching text
after_index = rawtext.find(after_text)
if after_index < 0:
raise self.severe('Problem with "start-after" option of "%s" '
'directive:\nText not found.' % self.name)
rawtext = rawtext[after_index + len(after_text):]
before_text = self.options.get('end-before', None)
if before_text:
# skip content in rawtext after *and incl.* a matching text
before_index = rawtext.find(before_text)
if before_index < 0:
raise self.severe('Problem with "end-before" option of "%s" '
'directive:\nText not found.' % self.name)
rawtext = rawtext[:before_index]

# copied code ends
app = self.env.app
filetype = get_filetype(app.config.source_suffix, path)
Copy link
Contributor Author

@eric-wieser eric-wieser May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably makes sense to provide an option to overload this, for files with weird extensions like .inc or .md.inc etc, something like

        filetype = self.options.get('filetype', None)
        if filetype is None:
            filetype = get_filetype(app.config.source_suffix, path))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+0: But nobody does not know about filetype. So we have to add it to our document.

parser = app.registry.create_source_parser(app, filetype)

sub_document = new_document(path, self.state.document.settings)
parser.parse(rawtext, sub_document)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you generate new document here? It prevents to share local reference context (ex. footnotes, hyperref, etc.).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there another way to invoke the parser?

I figured it didn't matter, because if the included document is in a different file it's very unlikely to reuse footnotes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured it didn't matter, because if the included document is in a different file it's very unlikely to reuse footnotes.

The document object is large database for the doctree. So all references would be broken. Is it okay to conflict node_IDs between parent and included document? I guess :ref: role for included document may not work also.

Is there another way to invoke the parser?

How about parser.parse(rawtext, self.state.document) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does docutils allow a document to be parsed into multiple times? How does it know where the current insertion point is?

return sub_document.children


# Import old modules here for compatibility
from sphinx.domains.index import IndexDirective # NOQA

Expand All @@ -383,6 +471,7 @@ def setup(app: "Sphinx") -> Dict[str, Any]:
directives.register_directive('hlist', HList)
directives.register_directive('only', Only)
directives.register_directive('include', Include)
directives.register_directive('includedoc', IncludeDocument)
Copy link
Contributor Author

@eric-wieser eric-wieser May 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name is bad, would appreciate feedback on a better name

Copy link

@rpanderson rpanderson Jun 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative directives that better distinguish it from include might be: parse, includeother, includeparsed, etc.


# register the standard rst class directive under a different name
# only for backwards compatibility now
Expand Down