Creating plugins

Live preview + developing plugins

Live preview will also watch the implementation of your Python plugin implementations. 🙂

Life is too short for slow dev/test loops.

Using Zig

Some of the built-in plugins also have Zig implementations, but this is not yet supported for custom plugins.

Using Python

Note: your Python will be executed via a new Python interpreter that is included within the main WASM blob.

Lifecycle Hooks

onDocParsed(doc: Doc, c: Context)

afterAllDocsRendered(c: Context)

Doc

doc.pid → str # "path in docset"
doc.docset → Docset
doc.page_pathname → str
doc.page_url → str
doc.root → Node
doc.md → str

Docset

docset.docWithPid(…) → Doc
docset.fileWithPid(…) → bytes

Context

c.addInlineCSS(…)
c.addInlineJS(…)
c.addHeadTag(…)
c.setPageLang(…)

c.addFileToZip(pizo=, file_bytes=)

c.assetFromExternalSubproc(…) → AssetRef
c.assetFromExternalRequest(…) → AssetRef
c.assetFromExternalRender(…) → AssetRef
# ^ These return immediately, and the
# process happens in the background

c.escapeHTML(…)
c.finalHtmlBytesForPagePathname(…)
# ^ (only during `afterAllDocsRendered`)

c.log(…)
c.warn(…)
c.error(…)
# ^ TODO: make a diagram like in
# "The Birth and Death of JavaScript",
# showing how many layers of abstraction
# these logs need to bubble up through
# to appear on your live preview page

AssetRef

asset.asImageNode() → ImageNode

Node

from markdownmesh import NodeType as T

node.isa(T.CodeBlock) → bool
node.text_attr → str
node.as_flattened_text → str

node.dict → dict
node.json → str

node.parent
node.children
node.siblings_after
node.siblings_before
node.preceding_sibling_or_none

node.delete()
node.replaceWithNodes(nodes)
node.setHtmlAttribute(k, v)

node.findNodes(…) → Sequence[Node]
node.findImageNodes(…) → Sequence[ImageNode]
node.findCodeBlockNodes(…) → Sequence[CodeBlockNode]
…

CodeBlockNode extends Node

.code # This is the same as .text_attr

.info_string # str, possibly empty
.lang # First word, if non-empty, else None
.meta # Remaining text, if any, else None

Examples

Example 1

The main function of an early draft of import-markdown:

def onDocParsed(doc: Doc, c: Context):

    # We're looking for an Image element
    for imgnode in doc.root.findImageNodes():

        # ...whose `.src` is a Markdown file
        src_pathname = pathname_of(imgnode.src)
        src_fragment_slug = parse_fragment_slug_or_none(imgnode.src)
        if not src_pathname.endswith(".md"):
            continue

        # ...which is preceded
        before = imgnode.preceding_sibling_or_none
        if before is None:
            continue

        # ...by the Text "!"
        if before.isa(T.Text) and before.text_attr == "!":
            # TODO: handle more generally, including Text(text="... !")

            # ...then it's an import
            import_parent = imgnode.parent
            # ...which might be followed by a Text node that defines a filter
            filter_function = None
            filter_node = None
            siblings_after = imgnode.siblings_after
            if len(siblings_after) > 0:
                s = siblings_after[0]
                if s.isa(T.Text) and first_word_of(s.text_attr) in FILTERS_BY_INVOCATION:
                    filter_function = FILTERS_BY_INVOCATION[first_word_of(s.text_attr)]
                    filter_node = s
            import_parent_had_no_other_children = bool(len(import_parent.children) == (2 if filter_function is None else 3))

            # So now we'll import the Markdown doc that was linked to
            doc_being_imported = doc.docset.docWithPid(src_pathname, relative_to=doc,
                                                       temp_go_beyond_edition=True)
            new_nodes = doc_being_imported.root.children

            # ...and apply the fragment slug, if any,
            if src_fragment_slug is not None:
                new_nodes = extract_section_nodes_from_nodes(new_nodes, src_fragment_slug)

            # ...and apply the specified filter, if any,
            if filter_function:
                new_nodes = filter_function(new_nodes, c)
                # TODO: postprocess the result of this based on:
                # - new_nodes type: Block, blocks, inline, or inlines?
                # - imgnode.parent type: Paragraph, TableCell, ...?

            # ...and replace the `!![](…):…` nodes with the new nodes
            before.delete()
            if filter_node is not None:
                filter_node.delete()
            imgnode.replaceWithNodes(new_nodes)

            # ...and if this import was the sole contents of a Paragraph node, then drop that paragraph wrapper
            if import_parent_had_no_other_children and import_parent.isa(T.Paragraph):
                import_parent.replaceWithNodes(import_parent.children)