MyST is able to invoke plugins written in different languages through standard IO protocols, for example, in Python. Executable MyST plugins are treated as a black box , whereby MyST only sees the data it passes to the plugin, and the response from the plugin itself.
Defining a new directive ¶ There are many different ways to create an executable plugin. Here we’ll use Python to implement an unsplash-py
directive, but any programming language that can read and write from stdin, stdout, and stderr, and define a command line interface would work.
First, we’ll declare the plugin specification that allows MyST to discover the directives, transforms, and/or roles that the plugin implements. This specification looks very similar to the definition of a JavaScript plugin , except the implementation logic (e.g. the directive run
method) is not defined.
#!/usr/bin/env python3
import argparse
import json
import sys
plugin = {
"name": "Unsplash Images",
"directives": [
{
"name": "unsplash-py",
"doc": "An example directive for showing a nice random image at a custom size.",
"alias": ["random-pic-py"],
"arg": {
"type": "string",
"doc": "The kinds of images to search for, e.g., `fruit`",
},
"options": {
"size": {
"type": "string",
"doc": "Size of the image, for example, `500x200`.",
},
},
}
],
}
def declare_result(content):
"""Declare result as JSON to stdout
:param content: content to declare as the result
"""
# Format result and write to stdout
json.dump(content, sys.stdout, indent=2)
# Successfully exit
raise SystemExit(0)
def run_directive(name, data):
"""Execute a directive with the given name and data
:param name: name of the directive to run
:param data: data of the directive to run
"""
assert name == "unsplash-py"
query = data["arg"]
size = data["options"].get("size", "500x200")
url = f"https://source.unsplash.com/random/{size}/?{query}"
# Insert an image of a landscape
img = {
"type": "image",
"url": url,
}
return [img]
if __name__ == "__main__":
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--role")
group.add_argument("--directive")
group.add_argument("--transform")
args = parser.parse_args()
if args.directive:
data = json.load(sys.stdin)
declare_result(run_directive(args.directive, data))
elif args.transform:
raise NotImplementedError
elif args.role:
raise NotImplementedError
else:
declare_result(plugin)
a plugin to add an unsplash-py
directive that includes a beautiful, random picture based on a query string.
this file should be executable, e.g.
and should be referenced from your myst.yml
under the projects.plugins
:
project:
plugins:
- type: executable
path: unsplash.py
then start or build your document using myst start
or myst build
, and you will see that the plugin is loaded.
myst start
...
🔌 unsplash images (unsplash.py) loaded: 1 directive
...
you can now use the directive, for example:
:::{unsplash-py} misty,mountains
:::
If you change the source code you will have to stop and re-start the server to see the results.
The types are defined in myst-common
(npm , github ) with the DirectiveSpec
and RoleSpec
being the main types to implement.
Directives can be used to extend MyST with rich structured content. However, sometimes we want to modify the existing behavior of MyST . One of the ways to do this is by writing a custom transform. In this section, we’ll implement a transform that replaces bold text with special bold text .
First, let’s define the transform
#!/usr/bin/env python3
import argparse
import json
import sys
plugin = {
"name": "Strong to emphasis",
"transforms": [
{
"name": "transform-typography",
"doc": "An example transform that rewrites bold text as text with emphasis.",
"stage": "document"
}
],
}
def find_all_by_type(node: dict, type_: str):
"""Simple node visitor that matches a particular node type
:param parent: starting node
:param type_: type of the node to search for
"""
if node["type"] == type_:
yield node
if "children" not in node:
return
for next_node in node["children"]:
yield from find_all_by_type(next_node, type_)
def declare_result(content):
"""Declare result as JSON to stdout
:param content: content to declare as the result
"""
# Format result and write to stdout
json.dump(content, sys.stdout, indent=2)
# Successfully exit
raise SystemExit(0)
def run_transform(name, data):
"""Execute a transform with the given name and data
:param name: name of the transform to run
:param data: AST of the document upon which the transform is being run
"""
assert name == "transform-typography"
for strong_node in find_all_by_type(data, "strong"):
child_text_nodes = find_all_by_type(strong_node, "text")
child_text = "".join([node['value'] for node in child_text_nodes])
# Only transform nodes whose text reads "special bold text (python)"
if child_text == "special bold text (python)":
strong_node["type"] = "span"
strong_node["style"] = {
"background": "-webkit-linear-gradient(20deg, #09009f, #E743D9)",
"-webkit-background-clip": "text",
"-webkit-text-fill-color": "transparent",
};
return data
if __name__ == "__main__":
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("--role")
group.add_argument("--directive")
group.add_argument("--transform")
args = parser.parse_args()
if args.directive:
raise NotImplementedError
elif args.transform:
data = json.load(sys.stdin)
declare_result(run_transform(args.transform, data))
elif args.role:
raise NotImplementedError
else:
declare_result(plugin)
A plugin to add a transform that replaces strong nodes with emphasis nodes.
this code should be referenced from your myst.yml
under the projects.plugins
:
project:
plugins:
- type: executable
path: markup.mjs
then start or build your document using myst start
or myst build
, and you will see that the plugin is loaded.
myst start
...
🔌 Strong to emphasis (markup.py) loaded: 1 directive
...
you can now use the directive, for example:
I am **special bold text (python)**, whilst I am **normal bold text**
I am special bold text (python) , whilst I am normal bold text