mkdocs-semiliterate/mkdocs_semiliterate/plugin.py
Glen Whitney 5a1f9e044a
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
refactor: Adjust to latest commit of simple plugin
Now reiterates significantly less code from the simple plugin. Also, adjusted
  the default for extract_standard_markdown when copy_standard_markdown is true.
2021-01-13 21:36:29 -08:00

209 lines
9.3 KiB
Python

""" md
## Usage
Once this plugin is [installed](../drone_install.md), in your `mkdocs.yml`
file just replace the plugin name `simple` with `semiliterate`. It accepts all
of the same parameters, so `mkdocs` will still work as before, and
you will have immediate access to all of the following extensions. (Note that
this documentation assumes a familiarity with the
[usage](https://athackst.github.io/mkdocs-simple-plugin/mkdocs_simple_plugin/plugin/)
of the `simple` plugin.)
"""
from mkdocs import utils
from mkdocs.config import config_options
from mkdocs_simple_plugin.plugin import SimplePlugin, StreamExtract
import re
import yaml
class StreamInclusion(StreamExtract):
""" md An extension of the StreamExtract class which adds
### Inclusion syntax
While extracting content from a file (because it matches one of the
`semiliterate` patterns, rather than just one of the `include_extensions`),
an unescaped expression of the form
`{! FILENAME YAML !}`
(which may span multiple lines) will trigger file inclusion. The FILENAME may
be a bare word, in which case it cannot contain whitespace, or it may be
enclosed in single or double quotes. Note that FILENAME is interpreted relative
to the directory in which the file containing the `{! .. !}` expression
resides. The YAML is interpreted exactly as the extraction options to a
`semiliterate` item as
[documented](https://athackst.github.io/mkdocs-simple-plugin/mkdocs_simple_plugin/plugin/index.html#plugin_usage)
for the `simple` extension. The text extracted from FILENAME
is interpolated at the current location in the file currently being written.
Recursive inclusion is supported.
Note that the `{! ... !}` directive must be in lines that semiliterate would
normally copy. That is, it does not examine lines before the `start` regexp
is encountered, or after the `terminate` regexp, or between instances of
`stop` and `start`. It also doesn't check any text written from lines that
match these special expressions. Moreover, on such normally-transcribed lines,
it's the text **after** the application of any semiliterate `replace`ments that
is checked for `{! ... !}`.
"""
def __init__(self, input_stream, output_stream, **kwargs):
""" md
### Adjusted semiliterate options
The `start` regular-expression parameter to a `semiliterate` file-inclusion
pattern is now optional. If omitted, it means that extraction begins immediately
with the first line of a file; in this case, `pause` and `terminate` retain
their usual meanings, although there is not currently any way to resume from a
`pause` when `start` is not specified. This adjustment to `semiliterate`
parameters makes it easier to extract "front-matter" style documentation from
files. It also means that a plain `{! file.md !}` directive will simply
incorporate the full contents of `file.md`.
"""
start_hot = False
if 'start' not in kwargs:
kwargs['start'] = 'dummy'
start_hot = True
super().__init__(input_stream, output_stream, **kwargs)
if start_hot:
self.extracting = True
self.start = False
include_open = re.compile(r'''(?<![`\\])(\{\!\s*)([\s'"])''')
include_quoted_file = re.compile(
r'''(['"])(?P<fn>.*?)\1\s+(?P<yml>[\s\S]*?)\s?\!\}''')
include_bare_file = re.compile(r'\s(?P<fn>.*?)\s+(?P<yml>[\s\S]*?)\s?\!\}')
def extract_line(self, line):
"""Copy line to the output stream, applying all specified replacements
and handling inclusion syntax.
"""
line = self.replace_line(line)
include_match = StreamInclusion.include_open.search(line)
if not include_match:
self.transcribe(line)
return
# OK, we have found (the start of) an inclusion and must process it
preamble = line[:include_match.start()]
remainder = line[include_match.end(1):]
body_pattern = StreamInclusion.include_quoted_file
if include_match[2].isspace():
body_pattern = StreamInclusion.include_bare_file
body_match = body_pattern.search(remainder)
if not body_match:
for extra_line in self.input_stream:
remainder += self.replace_line(extra_line)
body_match = body_pattern.search(remainder)
if body_match:
break
if not body_match:
errmsg = "semiliterate: End of file while scanning for `!}`"
utils.log.error(errmsg)
raise EOFError(errmsg)
include_path = self.include_root + '/' + body_match['fn']
new_root = re.match(r'(.*)/', include_path)[1]
try:
include_parameters = yaml.safe_load(body_match['yml'])
except Exception as err:
newmsg = (f"While attempting to include '{include_path}', could"
+ f" not parse yaml '{body_match['yml']}'.")
if hasattr(err, 'message'):
raise SyntaxError(
f"{newmsg} YAML parser reports: {err.message}")
raise SyntaxError(f"{newmsg} Caught exception: {str(err)}")
if not include_parameters:
include_parameters = {}
with open(include_path) as include_file:
self.transcribe(preamble)
inclusion = StreamInclusion(
include_file, self.output_stream, include_root=new_root,
**include_parameters)
if inclusion.extract():
self.wrote_something = True
self.transcribe(remainder[body_match.end():])
class SemiliteratePlugin(SimplePlugin):
r""" md An extension of the mkdocs-simple-plugin
In addition, block-comment markdown `/** md` ... `**/` is by
default scanned for in all files with an extension, as it's valid in so many
disparate languages.
### Additional plugin parameters
`semiliterate` adds a couple of new plugin parameters to further tailor its
behavior as compared to `simple`. They are described in this section, with
default values in parentheses at the beginning of each entry.
{! plugin.py ---
start: '[*]altered_config_scheme'
terminate: '^\s*\)'
replace:
- ["\\('(.*)',\\s*$", '\1\n']
- ['config_options.Type.*?default=([^\)]*)', ': (\1)']
- '^\s*#(.*\s*)$'
!}
"""
super_sdict = dict(SimplePlugin.config_scheme)
super_semi_dflt = super_sdict['semiliterate'].default
semi_dflt = [b if r'\*' not in b['stop'] else dict(b, pattern=r'\.')
for b in super_semi_dflt]
altered_config_scheme = dict(
super_sdict,
semiliterate=config_options.Type(list, default=semi_dflt)).items()
config_scheme = (
# Note documentation of each new parameter **follows** the parameter.
*altered_config_scheme,
('copy_standard_markdown',
config_options.Type(bool, default=False)),
# Whether to add MkDocs' list of standard Markdown extensions to the
# `include_extensions` parameter so that Markdown files will be
# directly copied to the docsite. Note that the `simple` behavior
# corresponds to a _true_ value for `copy_standard_markdown`, but
# `semiliterate` still incorporates all standard Markdown files
# because of the following `extract_standard_markdown` parameter.
('extract_standard_markdown',
config_options.Type(dict, default={})),
# If the `enable` key of this dict parameter is true
# (it defaults to the opposite of `copy_standard_markdown`),
# it adds a semiliterate block causing extraction (and hence
# include-directive processing) from all standard Markdown files
# (as defined by MkDocs). The remaining keys of this parameter are
# included as parameters of that semiliterate block. Thus, the
# default values of the parameters arrange for Markdown files to be
# copied "as-is", except possibly for embedded inclusions.
# On the other hand, setting this parameter to `{enable: false}`
# (which is also the default when `copy_standard_markdown` is true)
# will prevent automatic extraction from standard Markdown files.
('report_docs_build',
config_options.Type(bool, default=False))
# If true, the name of the temporary directory to which generated docs
# files are copied/extracted will be written to standard output
# (even if the `-v` verbose option to mkdocs is not specified).
)
def build_docs(self):
if self.config['report_docs_build']:
utils.log.info(
f"semiliterate: generating docs in {self.build_docs_dir}")
dflt_enable = False
if not self.config['copy_standard_markdown']:
self.include_extensions = self.config['include_extensions']
dflt_enable = True
if self.config['extract_standard_markdown'].get('enable', dflt_enable):
ext_pat = '|'.join(re.escape(s) for s in utils.markdown_extensions)
self.semiliterate.append(dict(
pattern=re.compile(f"^(.*(?:{ext_pat}))$"),
destination=r'\1',
**self.config['extract_standard_markdown']))
return super().build_docs()
def try_extraction(self, original_file, root, new_file, **kwargs):
extraction = StreamInclusion(
original_file, new_file, include_root=root, **kwargs)
return extraction.extract()