commit 23179b7ed08401574dfedca8c17dc13a0c4c9160 Author: nearology Date: Wed Jul 29 11:03:21 2026 +0330 init projects diff --git a/README.md b/README.md new file mode 100644 index 0000000..a18a808 --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +# mkdocs-schematic-footprint-viewer + +A Zensical/Python-Markdown extension that renders an interactive schematic and footprint viewer in Markdown. + +Use this shortcode anywhere in Markdown and pass the JSON file at the call site: + +```md +{% schematic-footprint json="files/VL53L0X/VL53L0X_merged.json" %} +``` + +## Installation + +```bash +pip install mkdocs-schematic-footprint-viewer +``` + +For local development: + +```bash +pip install -e ".[test]" +``` + +## Zensical Configuration + +Zensical's public plugin/module API is still evolving, so this package integrates through the stable Python-Markdown extension path that Zensical supports. + +Add the extension to `zensical.toml`: + +```toml +[project.markdown_extensions.schematic_footprint_viewer] +default_json = "merged.json" +``` + +Then use the shortcode in Markdown: + +```md +{% schematic-footprint json="files/VL53L0X/VL53L0X_merged.json" %} +``` + +Run Zensical: + +```bash +zensical serve +``` + +or: + +```bash +zensical build +``` + +## Shortcode Options + +The JSON path belongs in the Markdown shortcode: + +```md +{% schematic-footprint json="merged.json" %} +``` + +`src` is also accepted as an alias: + +```md +{% schematic-footprint src="files/MP1584/MP1584_merged.json" %} +``` + +You can override the box dimensions per shortcode: + +```md +{% schematic-footprint json="merged.json" width="24rem" height="20rem" %} +``` + +## Zensical Configuration With Options + +These settings define defaults for every viewer: + +```toml +[project.markdown_extensions.schematic_footprint_viewer] +default_json = "merged.json" +width = "15rem" +height = "13rem" +konva_url = "https://unpkg.com/konva@9/konva.min.js" +``` + +If you keep `konva.min.js` locally in your built site, point `konva_url` to that file: + +```toml +[project.markdown_extensions.schematic_footprint_viewer] +konva_url = "konva.min.js" +``` + +If your `zensical.toml` already has plugins, keep them separate. For example: + +```toml +[project] +plugins = [ + { search = {} }, + { badges = {} }, + { document-dates = { position = "top", type = "timeago", exclude = [ + "index.md", + "blog/*", + ] } }, +] + +[project.markdown_extensions.schematic_footprint_viewer] +default_json = "merged.json" +width = "15rem" +height = "13rem" +``` + +Do not add this package to `plugins` in `zensical.toml`; use `markdown_extensions` as shown above. + +## Output + +The shortcode is replaced during Markdown rendering with a scoped viewer containing two Konva canvases: one for PCB/footprint data and one for schematic data. The viewer fetches the JSON path passed in Markdown. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b3ec84d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mkdocs-schematic-footprint-viewer" +version = "0.1.1" +description = "Zensical/Python-Markdown extension that renders schematic footprint viewer shortcodes." +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } +authors = [ + { name = "Nearology" } +] +keywords = ["mkdocs", "zensical", "schematic", "footprint", "viewer"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Documentation", +] +dependencies = [ + "Markdown>=3.3.6", +] + +[project.optional-dependencies] +test = [ + "pytest>=7.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/schematic_footprint_viewer"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/src/schematic_footprint_viewer/__init__.py b/src/schematic_footprint_viewer/__init__.py new file mode 100644 index 0000000..e8946c1 --- /dev/null +++ b/src/schematic_footprint_viewer/__init__.py @@ -0,0 +1,15 @@ +"""Zensical/Python-Markdown extension for schematic footprint viewer shortcodes.""" + +from .extension import ( + SchematicFootprintViewerExtension, + ViewerOptions, + makeExtension, + render_viewer, +) + +__all__ = [ + "SchematicFootprintViewerExtension", + "ViewerOptions", + "makeExtension", + "render_viewer", +] diff --git a/src/schematic_footprint_viewer/__pycache__/__init__.cpython-312.pyc b/src/schematic_footprint_viewer/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1efc40a Binary files /dev/null and b/src/schematic_footprint_viewer/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/schematic_footprint_viewer/__pycache__/extension.cpython-312.pyc b/src/schematic_footprint_viewer/__pycache__/extension.cpython-312.pyc new file mode 100644 index 0000000..3d181cd Binary files /dev/null and b/src/schematic_footprint_viewer/__pycache__/extension.cpython-312.pyc differ diff --git a/src/schematic_footprint_viewer/extension.py b/src/schematic_footprint_viewer/extension.py new file mode 100644 index 0000000..09846d9 --- /dev/null +++ b/src/schematic_footprint_viewer/extension.py @@ -0,0 +1,801 @@ +"""Python-Markdown extension for schematic footprint viewer shortcodes.""" + +from __future__ import annotations + +import re +import shlex +from dataclasses import dataclass +from html import escape + +from markdown import Markdown +from markdown.extensions import Extension +from markdown.preprocessors import Preprocessor + + +SHORTCODE_RE = re.compile(r"{%\s*schematic-footprint(?P.*?)\s*%}") + + +@dataclass(frozen=True) +class ViewerOptions: + """Runtime settings for the generated viewer.""" + + json_path: str + width: str = "15rem" + height: str = "13rem" + konva_url: str = "https://unpkg.com/konva@9/konva.min.js" + + +def _parse_shortcode_attrs(raw_attrs: str, default_json: str) -> dict[str, str]: + attrs: dict[str, str] = {} + for token in shlex.split(raw_attrs.strip()): + if "=" in token: + key, value = token.split("=", 1) + attrs[key.strip()] = value.strip() + elif token: + attrs.setdefault("json", token) + + if "src" in attrs and "json" not in attrs: + attrs["json"] = attrs["src"] + attrs.setdefault("json", default_json) + return attrs + + +def _script_data(value: str) -> str: + return escape(value, quote=True) + + +def render_viewer(options: ViewerOptions) -> str: + """Render the schematic/footprint viewer shell.""" + + json_path = _script_data(options.json_path) + width = _script_data(options.width) + height = _script_data(options.height) + konva_url = _script_data(options.konva_url) + + return f""" +
+
+
+ PCB +
+ + 100% +
+
+
+
+
+
+
+
+ SCHEMATIC +
+ + 100% +
+
+
+
+
+
+
PARSING...
+
+
+ +""".strip() + + +VIEWER_CSS = r""" +.schematic-footprint-viewer, +.schematic-footprint-viewer * { + box-sizing: border-box; +} +.schematic-footprint-viewer { + --sfv-bg: #0d1117; + --sfv-panel: #161b22; + --sfv-border: #21262d; + --sfv-accent-pcb: #00e5a0; + --sfv-accent-sch: #4fc3f7; + --sfv-text: #e6edf3; + --sfv-dim: #8b949e; + position: relative; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: flex-start; + justify-content: center; + width: 100%; + padding: 0; + border-radius: 6px; + background: var(--sfv-bg); + color: var(--sfv-text); + font-family: "Share Tech Mono", "Courier New", monospace; +} +.schematic-footprint-viewer .sfv-viewer-box { + flex: 1 1 0; + width: var(--sfv-box-w); + height: var(--sfv-box-h); + max-width: calc(50% - 0.25rem); + min-width: 0; + display: flex; + flex-direction: column; + border: 1px solid var(--sfv-border); + border-radius: 6px; + overflow: hidden; + background: #090b0c; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.04), 0 8px 32px rgba(0, 0, 0, 0.5); + flex-shrink: 0; +} +.schematic-footprint-viewer .sfv-box-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 10px; + height: 2.1rem; + background: var(--sfv-panel); + border-bottom: 1px solid var(--sfv-border); + flex-shrink: 0; +} +.schematic-footprint-viewer .sfv-box-label { + font-size: 9px; + letter-spacing: 2.5px; + font-weight: 700; +} +.schematic-footprint-viewer .sfv-pcb-label { + color: var(--sfv-accent-pcb); +} +.schematic-footprint-viewer .sfv-sch-label { + color: var(--sfv-accent-sch); +} +.schematic-footprint-viewer .sfv-box-controls { + display: flex; + align-items: center; + gap: 6px; +} +.schematic-footprint-viewer .sfv-box-canvas-wrap { + flex: 1; + position: relative; + overflow: hidden; +} +.schematic-footprint-viewer .sfv-box-canvas-wrap > div { + width: 100%; + height: 100%; + position: relative; + z-index: 1; +} +.schematic-footprint-viewer .sfv-ctrl-btn { + background: transparent; + border: 1px solid var(--sfv-border); + color: var(--sfv-dim); + font-family: inherit; + font-size: 9px; + font-weight: 700; + letter-spacing: 1px; + padding: 2px 8px; + border-radius: 3px; + cursor: pointer; + line-height: 1.5; +} +.schematic-footprint-viewer .sfv-ctrl-btn:hover { + border-color: var(--sfv-accent-pcb); + color: var(--sfv-accent-pcb); +} +.schematic-footprint-viewer .sfv-zoom-pill { + font-size: 9px; + letter-spacing: 1.5px; + color: var(--sfv-accent-pcb); + min-width: 34px; + text-align: right; +} +.schematic-footprint-viewer .sfv-loading { + display: none; + position: absolute; + inset: 0; + z-index: 60; + background: rgba(13, 17, 23, 0.85); + align-items: center; + justify-content: center; +} +.schematic-footprint-viewer .sfv-loading.active { + display: flex; +} +.schematic-footprint-viewer .sfv-loading span { + font-size: 13px; + letter-spacing: 4px; + color: var(--sfv-accent-pcb); + animation: sfv-pulse 1s ease-in-out infinite; +} +@keyframes sfv-pulse { + 0%, 100% { opacity: 0.2; } + 50% { opacity: 1; } +} +.schematic-footprint-viewer .sfv-tooltip { + position: fixed; + pointer-events: none; + background: var(--sfv-panel); + border: 1px solid var(--sfv-accent-pcb); + color: var(--sfv-text); + font-size: 10px; + padding: 5px 10px; + border-radius: 3px; + white-space: pre; + display: none; + z-index: 100; + box-shadow: 0 4px 20px rgba(0, 229, 160, 0.15); + line-height: 1.6; +} +.schematic-footprint-viewer{ + --sfv-bg: #0d111700!important; +} +""" + + +class SchematicFootprintPreprocessor(Preprocessor): + """Replace schematic footprint shortcodes before Markdown parsing.""" + + def __init__( + self, + md: Markdown, + default_json: str, + width: str, + height: str, + konva_url: str, + ) -> None: + super().__init__(md) + self.default_json = default_json + self.width = width + self.height = height + self.konva_url = konva_url + + def run(self, lines: list[str]) -> list[str]: + markdown = "\n".join(lines) + + def replace(match: re.Match[str]) -> str: + attrs = _parse_shortcode_attrs( + match.group("attrs"), + default_json=self.default_json, + ) + return render_viewer( + ViewerOptions( + json_path=attrs["json"], + width=attrs.get("width", self.width), + height=attrs.get("height", self.height), + konva_url=attrs.get("konva", self.konva_url), + ), + ) + + rendered = SHORTCODE_RE.sub(replace, markdown) + return rendered.split("\n") + + +class SchematicFootprintViewerExtension(Extension): + """Python-Markdown extension used by Zensical.""" + + def __init__(self, **kwargs) -> None: + self.config = { + "default_json": ["merged.json", "Default JSON path when shortcode has no json/src attribute."], + "width": ["15rem", "Viewer box width."], + "height": ["13rem", "Viewer box height."], + "konva_url": ["https://unpkg.com/konva@9/konva.min.js", "Konva JavaScript URL."], + } + super().__init__(**kwargs) + + def extendMarkdown(self, md: Markdown) -> None: + md.preprocessors.register( + SchematicFootprintPreprocessor( + md, + default_json=str(self.getConfig("default_json")), + width=str(self.getConfig("width")), + height=str(self.getConfig("height")), + konva_url=str(self.getConfig("konva_url")), + ), + "schematic_footprint_viewer", + 35, + ) + + +def makeExtension(**kwargs) -> SchematicFootprintViewerExtension: + """Entry point used by Python-Markdown and Zensical.""" + + return SchematicFootprintViewerExtension(**kwargs) diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..ba47d81 --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,53 @@ +from markdown import Markdown + +from schematic_footprint_viewer import makeExtension +from schematic_footprint_viewer.extension import ViewerOptions, render_viewer + + +def test_render_viewer_outputs_two_canvas_boxes(): + html = render_viewer(ViewerOptions(json_path="files/demo.json")) + + assert 'class="schematic-footprint-viewer"' in html + assert 'data-sfv-json="files/demo.json"' in html + assert "data-sfv-pcb-canvas" in html + assert "data-sfv-sch-canvas" in html + assert "sfv-main" not in html + + +def test_extension_replaces_shortcode_with_json_attribute(): + md = Markdown(extensions=[makeExtension()]) + + rendered = md.convert( + 'Before\n\n{% schematic-footprint json="files/VL53L0X_merged.json" %}\n\nAfter', + ) + + assert "{% schematic-footprint" not in rendered + assert "Before" in rendered + assert "After" in rendered + assert 'data-sfv-json="files/VL53L0X_merged.json"' in rendered + + +def test_extension_accepts_src_alias_and_dimensions(): + md = Markdown(extensions=[makeExtension()]) + + rendered = md.convert( + '{% schematic-footprint src="merged.json" width="24rem" height="20rem" %}', + ) + + assert 'data-sfv-json="merged.json"' in rendered + assert "--sfv-box-w:24rem" in rendered + assert "--sfv-box-h:20rem" in rendered + + +def test_extension_uses_default_json_when_no_attribute_is_given(): + md = Markdown(extensions=[makeExtension(default_json="default.json")]) + rendered = md.convert("{% schematic-footprint %}") + + assert 'data-sfv-json="default.json"' in rendered + + +def test_extension_is_loadable_by_module_name(): + md = Markdown(extensions=["schematic_footprint_viewer"]) + rendered = md.convert('{% schematic-footprint json="module.json" %}') + + assert 'data-sfv-json="module.json"' in rendered