Skip to content

Commit

Permalink
✨ NEW: Add sub-ref role and word-counting (#367)
Browse files Browse the repository at this point in the history
This commit adds the markdown-it-py `wordcount_plugin`
(executablebooks/mdit-py-plugins#20) to the Markdown parser,
and exposes it via the `sub-ref` role
(which also exposes sphinx's `today` substitution).
For example, you can now add to a document:

```markdown
{sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
```
  • Loading branch information
chrisjsewell authored May 4, 2021
1 parent fb8c8fd commit 4954f5c
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ repos:
additional_dependencies:
- sphinx~=3.3
- markdown-it-py>=1.0.0,<2.0.0
- mdit-py-plugins~=0.2.4
- mdit-py-plugins~=0.2.8
files: >
(?x)^(
myst_parser/.*py|
Expand Down
15 changes: 5 additions & 10 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,14 @@
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))

from myst_parser import __version__

# -- Project information -----------------------------------------------------

project = "MyST Parser"
copyright = "2020, Executable Book Project"
author = "Executable Book Project"
version = __version__

master_doc = "index"
language = "en"
Expand Down Expand Up @@ -58,6 +50,9 @@
html_theme_options = {
"github_url": "https://github.com/executablebooks/MyST-Parser",
"repository_url": "https://github.com/executablebooks/MyST-Parser",
"use_edit_page_button": True,
"repository_branch": "master",
"path_to_docs": "docs",
}

# Add any paths that contain custom static files (such as style sheets) here,
Expand Down
2 changes: 2 additions & 0 deletions docs/examples/wealth_dynamics_md.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Wealth Distribution Dynamics in MyST

> {sub-ref}`today` | {sub-ref}`wordcount-minutes` min read
```{note}
You can {download}`Download the source file for this page <./wealth_dynamics_md.md>`
```
Expand Down
3 changes: 3 additions & 0 deletions docs/using/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ To do so, use the keywords beginning `myst_`.
* - `myst_footnote_transition`
- `True`
- Place a transition before any footnotes.
* - `myst_words_per_minute`
- `200`
- Reading speed used to calculate `` {sub-ref}`wordcount-minutes` ``
`````

List of extensions:
Expand Down
10 changes: 6 additions & 4 deletions docs/using/syntax-optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,14 @@ Substitution references are assessed as [Jinja2 expressions](http://jinja.pallet
Therefore you can do things like:

```md
{{ env.docname | upper }}
{{ "a" + "b" }}
- version: {{ env.config.version }}
- docname: {{ env.docname | upper }}
- {{ "a" + "b" }}
```

{{ env.docname | upper }}
{{ "a" + "b" }}
- version: {{ env.config.version }}
- docname: {{ env.docname | upper }}
- {{ "a" + "b" }}

You can also change the delimiter if necessary, for example setting in the `conf.py`:

Expand Down
25 changes: 25 additions & 0 deletions docs/using/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# The MyST Syntax Guide

> {sub-ref}`today` | {sub-ref}`wordcount-minutes` min read
As a base, MyST adheres to the [CommonMark specification](https://spec.commonmark.org/).
For this, it uses the [markdown-it-py](https://github.com/executablebooks/markdown-it-py) parser,
which is a well-structured markdown parser for Python that is CommonMark-compliant
Expand Down Expand Up @@ -604,6 +606,29 @@ For example, following the `ref` example above, if you pass a string like this:

How roles parse this content depends on the author that created the role.

(syntax/roles/special)=

### Special roles

```{versionadded} 0.14.0
The `sub-ref` role and word counting.
```

For all MyST documents, the date and word-count are made available by substitution definitions,
which can be accessed *via* the `sub-ref` role.

For example:

```markdown
> {sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
```

> {sub-ref}`today` | {sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
`today` is replaced by either the date on which the document is parsed, with the format set by [`today_fmt`](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-today_fmt), or the `today` variable if set in the configuration file.

The reading speed is computed using the `myst_words_per_minute` configuration (see the [Sphinx configuration options](intro/config-options)).

(extra-markdown-syntax)=
## Extra markdown syntax

Expand Down
3 changes: 2 additions & 1 deletion myst_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ def setup_sphinx(app: "Sphinx"):
"""Initialize all settings and transforms in Sphinx."""
# we do this separately to setup,
# so that it can be called by external packages like myst_nb
from myst_parser.directives import FigureMarkdown
from myst_parser.directives import FigureMarkdown, SubstitutionReferenceRole
from myst_parser.main import MdParserConfig
from myst_parser.mathjax import override_mathjax
from myst_parser.myst_refs import MystReferenceResolver

app.add_role("sub-ref", SubstitutionReferenceRole())
app.add_directive("figure-md", FigureMarkdown)

app.add_post_transform(MystReferenceResolver)
Expand Down
16 changes: 15 additions & 1 deletion myst_parser/directives.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""MyST specific directives"""
from typing import List
from typing import List, Tuple

from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.directives import SphinxDirective
from sphinx.util.docutils import SphinxRole


def align(argument):
Expand All @@ -17,6 +18,19 @@ def figwidth_value(argument):
return directives.length_or_percentage_or_unitless(argument, "px")


class SubstitutionReferenceRole(SphinxRole):
"""Implement substitution references as a role.
Note, in ``docutils/parsers/rst/roles.py`` this is left unimplemented.
"""

def run(self) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
subref_node = nodes.substitution_reference(self.rawtext, self.text)
self.set_source_info(subref_node, self.lineno) # type: ignore[arg-type]
subref_node["refname"] = nodes.fully_normalize_name(self.text)
return [subref_node], []


class FigureMarkdown(SphinxDirective):
"""Directive for creating a figure with Markdown compatible syntax.
Expand Down
33 changes: 32 additions & 1 deletion myst_parser/docutils_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,39 @@ def render(
else:
self.render_footnote_reference(foot_ref_tokens[0])

self.add_document_wordcount()

return self.document

def add_document_wordcount(self) -> None:
"""Add the wordcount, generated by the ``mdit_py_plugins.wordcount_plugin``."""

wordcount_metadata = self.md_env.get("wordcount", {})
if not wordcount_metadata:
return

# save the wordcount to the sphinx BuildEnvironment metadata
try:
sphinx_env = self.document.settings.env
except AttributeError:
pass # if not sphinx renderer
else:
meta = sphinx_env.metadata.setdefault(sphinx_env.docname, {})
meta["wordcount"] = wordcount_metadata

# now add the wordcount as substitution definitions,
# so we can reference them in the document
for key in ("words", "minutes"):
value = wordcount_metadata.get(key, None)
if value is None:
continue
substitution_node = nodes.substitution_definition(
str(value), nodes.Text(str(value))
)
substitution_node.source = self.document["source"]
substitution_node["names"].append(f"wordcount-{key}")
self.document.note_substitution_def(substitution_node, f"wordcount-{key}")

def nested_render_text(self, text: str, lineno: int) -> None:
"""Render unparsed text."""

Expand Down Expand Up @@ -796,7 +827,7 @@ def render_myst_target(self, token: SyntaxTreeNode) -> None:
self.current_node.append(target)

def render_myst_line_comment(self, token: SyntaxTreeNode) -> None:
self.current_node.append(nodes.comment(token.content, token.content))
self.current_node.append(nodes.comment(token.content, token.content.strip()))

def render_myst_role(self, token: SyntaxTreeNode) -> None:
name = token.meta["name"]
Expand Down
8 changes: 7 additions & 1 deletion myst_parser/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from mdit_py_plugins.myst_blocks import myst_block_plugin
from mdit_py_plugins.myst_role import myst_role_plugin
from mdit_py_plugins.substitution import substitution_plugin
from mdit_py_plugins.wordcount import wordcount_plugin

from . import __version__ # noqa: F401

Expand Down Expand Up @@ -91,6 +92,8 @@ def check_extensions(self, attribute, value):

sub_delimiters: Tuple[str, str] = attr.ib(default=("{", "}"))

words_per_minute: int = attr.ib(default=200, validator=instance_of(int))

@sub_delimiters.validator
def check_sub_delimiters(self, attribute, value):
if (not isinstance(value, (tuple, list))) or len(value) != 2:
Expand Down Expand Up @@ -123,7 +126,9 @@ def default_parser(config: MdParserConfig) -> MarkdownIt:
raise ValueError("unknown renderer type: {0}".format(config.renderer))

if config.commonmark_only:
md = MarkdownIt("commonmark", renderer_cls=renderer_cls)
md = MarkdownIt("commonmark", renderer_cls=renderer_cls).use(
wordcount_plugin, per_minute=config.words_per_minute
)
md.options.update({"commonmark_only": True})
return md

Expand All @@ -134,6 +139,7 @@ def default_parser(config: MdParserConfig) -> MarkdownIt:
.use(myst_block_plugin)
.use(myst_role_plugin)
.use(footnote_plugin)
.use(wordcount_plugin, per_minute=config.words_per_minute)
.disable("footnote_inline")
# disable this for now, because it need a new implementation in the renderer
.disable("footnote_tail")
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ install_requires =
docutils>=0.15,<0.18
jinja2 # required for substitutions, but let sphinx choose version
markdown-it-py>=1.0.0,<2.0.0
mdit-py-plugins~=0.2.5
mdit-py-plugins~=0.2.8
pyyaml
sphinx>=2.1,<4
python_requires = >=3.6
Expand Down
4 changes: 4 additions & 0 deletions tests/test_sphinx/sourcedirs/basic/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ this is a second paragraph
def func(a, b=1):
print(a)
```

Special substitution references:

{sub-ref}`wordcount-words` words | {sub-ref}`wordcount-minutes` min read
1 change: 1 addition & 0 deletions tests/test_sphinx/test_sphinx_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_basic(
"date": "2/12/1985",
"copyright": "MIT",
"other": "Something else",
"wordcount": {"minutes": 0, "words": 53},
}


Expand Down
6 changes: 6 additions & 0 deletions tests/test_sphinx/test_sphinx_builds/test_basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ <h1>
</pre>
</div>
</div>
<p>
Special substitution references:
</p>
<p>
53 words | 0 min read
</p>
</section>
</div>
</div>
Expand Down
7 changes: 7 additions & 0 deletions tests/test_sphinx/test_sphinx_builds/test_basic.resolved.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,10 @@
<literal_block language="default" linenos="False" xml:space="preserve">
def func(a, b=1):
print(a)
<paragraph>
Special substitution references:
<paragraph>
53
words |
0
min read
7 changes: 7 additions & 0 deletions tests/test_sphinx/test_sphinx_builds/test_basic.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,10 @@
<literal_block language="default" xml:space="preserve">
def func(a, b=1):
print(a)
<paragraph>
Special substitution references:
<paragraph>
53
words |
0
min read

0 comments on commit 4954f5c

Please sign in to comment.