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 support for workflow code cells #80

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
72 changes: 72 additions & 0 deletions src/_static/js/workflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
class WorkflowContainer {
static cellCounter = 0;

static escapeHtml(unsafe)
{
return unsafe
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

static createWorkflowCell(img, path) {
const wrap = document.createElement("div");
wrap.classList.add("highlight");

// workflow SVG renders in a visible pre element
const workflowCell = document.createElement("pre");
wrap.appendChild(workflowCell);
const imgParent = img.parentElement;
workflowCell.appendChild(img);
imgParent.remove();

// set button SVG from sphinx-copybutton
const id = "workflowcell" + WorkflowContainer.cellCounter++;
const clipboardButton = id =>
`<button class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
${iconCopy}
</button>`
workflowCell.insertAdjacentHTML('afterend', clipboardButton(id))

// fetch contents into an invisible pre for copying
fetch(path).then(req => req.text()).then(contents => {
const codeCell = document.createElement("pre");
codeCell.id = id;
codeCell.innerHTML = WorkflowContainer.escapeHtml(contents);
codeCell.hidden = true;
wrap.appendChild(codeCell);
});
return wrap;
}

static renderElement(element) {
element.classList.add("highlight-bonsai");
const img = element.querySelector("img");

// assumes all workflow references are hosted in _workflows/
const workflowPath = img.src
.replace("_images/", "_workflows/")
.replace(/\.[^.]+$/, ".bonsai");

const wrap = WorkflowContainer.createWorkflowCell(img, workflowPath);
const parent = element.parentElement;
parent.insertBefore(wrap, element);
element.appendChild(wrap);
}

static init() {
const observer = new MutationObserver(() => {
const theme = document.documentElement.getAttribute("data-bs-theme");
const root = document.querySelector(':root');
root.style.setProperty("color-scheme", theme);
}).observe(document.documentElement, { attributes: true, attributeFilter: ['data-bs-theme'] })
for (const element of document.getElementsByClassName("workflow")) {
WorkflowContainer.renderElement(element)
}
}
}

// reuse load function from sphinx-copybutton
runWhenDOMLoaded(WorkflowContainer.init);
5 changes: 5 additions & 0 deletions src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

sys.path.extend(
[
os.path.abspath("."),
os.path.abspath("../aeon_mecha/"),
os.path.abspath("../aeon_analysis/aeon_analysis"),
os.path.abspath("../aeon_acquisition"),
Expand Down Expand Up @@ -68,6 +69,7 @@ def get_current_release_tag():
"myst_nb",
"sphinx_design",
"sphinx_copybutton",
"convertworkflow"
]

# Configure the myst parser to enable cool markdown features
Expand Down Expand Up @@ -144,6 +146,9 @@ def get_current_release_tag():
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css",
"css/custom.css",
]
html_js_files = [
'js/workflow.js', # javascript for embedded workflows
]

# linkcheck will skip checking these URLs entirely
linkcheck_ignore = [
Expand Down
69 changes: 69 additions & 0 deletions src/convertworkflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import os
import sphinx
from pathlib import Path
from docutils import nodes

from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.osutil import ensuredir, copyfile

from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata

from sphinx.transforms.post_transforms.images import BaseImageConverter

logger = logging.getLogger(__name__)


class WorkflowImageConverter(BaseImageConverter):
"""An image converter to resolve workflow image references.
The converter will handle any image reference targeting a .bonsai
workflow file and look for the corresponding .svg file.
If the builder output is html it will also copy the source file
into the _workflows folder so it can be fetched by copy button scripts.
"""
default_priority = 300

def match(self, node: nodes.image) -> bool:
_, ext = os.path.splitext(node['uri'])
return '://' not in node['uri'] and ext == '.bonsai'

def handle(self, node: nodes.image) -> None:
try:
srcpath = Path(node['uri'])
abs_srcpath = self.app.srcdir / srcpath
abs_imgpath = abs_srcpath.with_suffix('.svg')
if not abs_imgpath.exists():
logger.warning(__('Could not find workflow image: %s [%s]'), node['uri'], abs_imgpath)
return

# copy workflow image to _images folder
destpath = os.path.join(self.imagedir, abs_imgpath.name)
ensuredir(self.imagedir)
copyfile(abs_imgpath, destpath)

# resolve image cross-references
if '*' in node['candidates']:
node['candidates']['*'] = destpath
node['uri'] = destpath
self.env.original_image_uri[destpath] = srcpath
self.env.images.add_file(self.env.docname, destpath)

# copy workflow file to _workflows folder when output is html
if self.app.builder.format == 'html':
abs_workflowdir = self.app.builder.outdir / '_workflows'
abs_workflowpath = abs_workflowdir / abs_srcpath.name
ensuredir(abs_workflowdir)
copyfile(abs_srcpath, abs_workflowpath)
except Exception as exc:
logger.warning(__('Could not fetch workflow image: %s [%s]'), node['uri'], exc)

def setup(app: Sphinx) -> ExtensionMetadata:
app.add_post_transform(WorkflowImageConverter)
return {
'version': sphinx.__display_version__,
'parallel_read_safe': True,
'parallel_write_safe': True,
}
Loading