From 23179b7ed08401574dfedca8c17dc13a0c4c9160 Mon Sep 17 00:00:00 2001 From: nearology Date: Wed, 29 Jul 2026 11:03:21 +0330 Subject: [PATCH] init projects --- README.md | 114 +++ pyproject.toml | 42 + src/schematic_footprint_viewer/__init__.py | 15 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 443 bytes .../__pycache__/extension.cpython-312.pyc | Bin 0 -> 32420 bytes src/schematic_footprint_viewer/extension.py | 801 ++++++++++++++++++ tests/test_plugin.py | 53 ++ 7 files changed, 1025 insertions(+) create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/schematic_footprint_viewer/__init__.py create mode 100644 src/schematic_footprint_viewer/__pycache__/__init__.cpython-312.pyc create mode 100644 src/schematic_footprint_viewer/__pycache__/extension.cpython-312.pyc create mode 100644 src/schematic_footprint_viewer/extension.py create mode 100644 tests/test_plugin.py 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 0000000000000000000000000000000000000000..1efc40a2f999f6f966b1ed96a074b2e835a029b5 GIT binary patch literal 443 zcmYjMu};G<5Vf781*)*Kg^doae*m!oQwvfh1}|0=o5nP@E5~gK8(+bWSo$q|03s_B z8=?bJCv4hO-0*aF@9Dj}*El`^uHIh6-6aIz!w*fNZL>OY%@bgN00tQ~3`wgBy5Ylsa8-&dE%0?^bFF%V)J&LOsD$5JPid9hYLk>%&Sh#v<|b{Y zMkt%i1z&JS<=WUxGhX&T-C3X;*2kkpe$l9J{>0~vX&rACS5xaE$~uPAGfM0I-C-thX(sd#d1yKCy0j=LU4oSx!Fq^uEjd;^HM-E_3NN&o;Iyz} zCVSp~&J8Y2hIb#*-G|$FOsP=9Qre$Z>%CK{*{tAaCU*Az-}1=8R{$Y=2cw@bf@lRC Od>=*~^w>5X$NmETGl}^C literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3d181cd14ca92913501695910d2e49e377f195f3 GIT binary patch literal 32420 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 Q_GbpZo*izEy~3#eUoqv>MF0Q* literal 0 HcmV?d00001 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