mkdocs-likec4 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,73 @@
1
+ import logging
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from .parser import LikeC4Parser
7
+
8
+ log = logging.getLogger(f"mkdocs.plugins.{__name__}")
9
+
10
+
11
+ class WebComponentGenerator:
12
+ """Generates LikeC4 web component JavaScript files."""
13
+
14
+ ASSETS_DIR = "assets/mkdocs_likec4"
15
+
16
+ @classmethod
17
+ def get_script_path(cls, project: Optional[str]) -> str:
18
+ """Get the site-relative path for a project's web component JS file."""
19
+ if project is None:
20
+ return f"{cls.ASSETS_DIR}/likec4_views.js"
21
+ return f"{cls.ASSETS_DIR}/likec4_views_{project}.js".lower()
22
+
23
+ @classmethod
24
+ def generate(
25
+ cls,
26
+ project_name: Optional[str],
27
+ project_dir: Optional[str],
28
+ build_dir: str,
29
+ site_dir: Path,
30
+ ) -> None:
31
+ """Generate web component JS file for a LikeC4 project."""
32
+ if project_name is not None and not LikeC4Parser.is_valid_identifier(
33
+ project_name
34
+ ):
35
+ log.error(
36
+ "mkdocs-likec4: Invalid project name '%s': must start with a letter "
37
+ "and contain only letters, numbers, hyphens, and underscores",
38
+ project_name,
39
+ )
40
+ return
41
+
42
+ site_dir.joinpath(cls.ASSETS_DIR).mkdir(parents=True, exist_ok=True)
43
+ dest_file = site_dir.joinpath(cls.get_script_path(project_name))
44
+
45
+ project_path = (
46
+ build_dir
47
+ if project_name is None
48
+ else str(Path(build_dir) / (project_dir or project_name))
49
+ )
50
+ log.info(
51
+ "mkdocs-likec4: Generating web component for %s from %s",
52
+ f"project '{project_name}'" if project_name else "default project",
53
+ project_path,
54
+ )
55
+
56
+ cmd = ["npx", "likec4", "codegen", "webcomponent"]
57
+ if project_name is not None:
58
+ cmd.extend(["--webcomponent-prefix", project_name.lower()])
59
+ cmd.extend([project_path, "-o", str(dest_file)])
60
+
61
+ try:
62
+ subprocess.run(cmd, check=True)
63
+ except subprocess.CalledProcessError as e:
64
+ log.error(
65
+ "mkdocs-likec4: Failed to generate web component for project '%s': %s",
66
+ project_name or "default",
67
+ e,
68
+ )
69
+ except FileNotFoundError:
70
+ log.error(
71
+ "mkdocs-likec4: 'npx' or 'likec4' command not found. "
72
+ "Ensure Node.js and likec4 are installed."
73
+ )
@@ -0,0 +1,80 @@
1
+ import logging
2
+ import re
3
+ from dataclasses import dataclass
4
+ from html import escape
5
+ from typing import Optional
6
+
7
+ log = logging.getLogger(f"mkdocs.plugins.{__name__}")
8
+
9
+ IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")
10
+
11
+
12
+ @dataclass
13
+ class ViewOptions:
14
+ """Options parsed from a likec4-view code block."""
15
+
16
+ view_id: str
17
+ browser: str = "true"
18
+ dynamic_variant: str = "diagram"
19
+ project: Optional[str] = None
20
+
21
+
22
+ class LikeC4Parser:
23
+ """Parser for likec4-view markdown code blocks."""
24
+
25
+ PATTERN = re.compile(
26
+ r"```likec4-view([^\r\n]*)\r?\n(.+?)\r?\n```",
27
+ flags=re.DOTALL,
28
+ )
29
+ OPT_BROWSER = re.compile(r"\bbrowser=(true|false)\b")
30
+ OPT_VARIANT = re.compile(r"\bdynamic-variant=(diagram|sequence)\b")
31
+ OPT_PROJECT = re.compile(r"\bproject=([^\s]+)\b")
32
+
33
+ @classmethod
34
+ def is_valid_identifier(cls, value: str) -> bool:
35
+ """Validate that an identifier contains only safe characters."""
36
+ return bool(IDENTIFIER_PATTERN.match(value))
37
+
38
+ @classmethod
39
+ def parse_options(cls, options_text: str, view_id: str) -> ViewOptions:
40
+ """
41
+ Parse options from the opening fence line of a likec4-view block.
42
+
43
+ Options can appear in any order and are all optional with sensible defaults.
44
+ """
45
+ opts = ViewOptions(view_id=view_id)
46
+
47
+ if m := cls.OPT_BROWSER.search(options_text):
48
+ opts.browser = m.group(1)
49
+
50
+ if m := cls.OPT_VARIANT.search(options_text):
51
+ opts.dynamic_variant = m.group(1)
52
+
53
+ if m := cls.OPT_PROJECT.search(options_text):
54
+ opts.project = m.group(1)
55
+
56
+ return opts
57
+
58
+ @classmethod
59
+ def to_html(cls, opts: ViewOptions) -> str:
60
+ if not cls.is_valid_identifier(opts.view_id):
61
+ log.warning(
62
+ "mkdocs-likec4: Invalid view ID '%s': contains unsafe characters",
63
+ opts.view_id,
64
+ )
65
+
66
+ if opts.project and not cls.is_valid_identifier(opts.project):
67
+ log.warning(
68
+ "mkdocs-likec4: Invalid project name '%s': using 'likec4-view' tag",
69
+ opts.project,
70
+ )
71
+
72
+ tag = (
73
+ f"{opts.project.lower()}-view"
74
+ if opts.project and cls.is_valid_identifier(opts.project)
75
+ else "likec4-view"
76
+ )
77
+ return (
78
+ f'<{tag} view-id="{escape(opts.view_id, quote=True)}" '
79
+ f'browser="{opts.browser}" dynamic-variant="{opts.dynamic_variant}"></{tag}>'
80
+ )
@@ -0,0 +1,122 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import pyjson5
6
+ from mkdocs.plugins import BasePlugin
7
+ from mkdocs.utils import get_relative_url
8
+
9
+ from .generator import WebComponentGenerator
10
+ from .parser import LikeC4Parser
11
+
12
+ log = logging.getLogger(f"mkdocs.plugins.{__name__}")
13
+
14
+
15
+ class LikeC4Plugin(BasePlugin):
16
+ """MkDocs plugin for embedding LikeC4 architecture diagrams."""
17
+
18
+ def __init__(self):
19
+ self.docs_dir = None
20
+ self.page_projects = {}
21
+ self.project_map = {}
22
+
23
+ def _discover_projects(self, docs_dir: Path):
24
+ """Discover LikeC4 projects by scanning for likec4.config.json files."""
25
+ if not docs_dir.exists():
26
+ log.warning("mkdocs-likec4: docs_dir does not exist: %s", docs_dir)
27
+ return
28
+
29
+ for config_file in docs_dir.rglob("likec4.config.json"):
30
+ try:
31
+ with config_file.open("r") as f:
32
+ config_data = pyjson5.load(f)
33
+ if project_name := config_data.get("name"):
34
+ project_dir = str(config_file.parent.relative_to(docs_dir))
35
+ self.project_map[project_name] = project_dir
36
+ log.info(
37
+ "mkdocs-likec4: Discovered project '%s' at %s",
38
+ project_name,
39
+ project_dir,
40
+ )
41
+ except (pyjson5.Json5Exception, OSError) as e:
42
+ log.warning("mkdocs-likec4: Failed to read %s: %s", config_file, e)
43
+
44
+ if not self.project_map:
45
+ self.project_map[None] = "."
46
+ log.info(
47
+ "mkdocs-likec4: No projects discovered, using default root project"
48
+ )
49
+
50
+ def _find_nearest_project(self, page_path: Path, docs_dir: Path) -> Optional[str]:
51
+ """Find the nearest LikeC4 project by traversing upward from the page."""
52
+ current = page_path.parent
53
+ while current >= docs_dir:
54
+ relative_str = str(current.relative_to(docs_dir))
55
+ for project_name, project_dir in self.project_map.items():
56
+ if project_dir == relative_str:
57
+ return project_name
58
+ if current == docs_dir:
59
+ break
60
+ current = current.parent
61
+ return None
62
+
63
+ def on_config(self, config):
64
+ self.docs_dir = Path(config["docs_dir"])
65
+ self._discover_projects(self.docs_dir)
66
+ return config
67
+
68
+ def on_page_markdown(self, markdown: str, page, **kwargs) -> str:
69
+ """Parse likec4-view code blocks and replace with web component HTML."""
70
+ page_file = page.file.src_uri
71
+ projects_on_page = set()
72
+ page_path = self.docs_dir / page.file.src_path
73
+
74
+ def replacer(match):
75
+ options_text = (match.group(1) or "").strip()
76
+ view_id = match.group(2).strip()
77
+ opts = LikeC4Parser.parse_options(options_text, view_id)
78
+
79
+ if opts.project is None:
80
+ opts.project = self._find_nearest_project(page_path, self.docs_dir)
81
+ if opts.project:
82
+ log.debug(
83
+ "mkdocs-likec4: Auto-detected project '%s' for %s",
84
+ opts.project,
85
+ page_file,
86
+ )
87
+
88
+ projects_on_page.add(opts.project)
89
+ return LikeC4Parser.to_html(opts)
90
+
91
+ markdown = LikeC4Parser.PATTERN.sub(replacer, markdown)
92
+ if projects_on_page:
93
+ self.page_projects[page_file] = projects_on_page
94
+ return markdown
95
+
96
+ def on_page_content(self, html, page, **kwargs):
97
+ """Inject project-specific JavaScript only on pages that use likec4-view."""
98
+ page_file = page.file.src_uri
99
+ if page_file not in self.page_projects:
100
+ return html
101
+
102
+ scripts = [
103
+ f'<script src="{get_relative_url(WebComponentGenerator.get_script_path(p), page.url)}"></script>'
104
+ for p in self.page_projects[page_file]
105
+ ]
106
+ return "\n".join(scripts) + "\n" + html
107
+
108
+ def on_post_build(self, config):
109
+ """Generate web component JS files for all projects used across the site."""
110
+ site_dir = Path(config["site_dir"])
111
+ all_projects = {p for projects in self.page_projects.values() for p in projects}
112
+
113
+ for project in all_projects:
114
+ if project in self.project_map:
115
+ WebComponentGenerator.generate(
116
+ project, self.project_map[project], str(self.docs_dir), site_dir
117
+ )
118
+ else:
119
+ log.warning(
120
+ "mkdocs-likec4: Skipping generation for undiscovered project: %s",
121
+ project,
122
+ )
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-likec4
3
+ Version: 1.0.0
4
+ Summary: LikeC4 for MkDocs
5
+ Author-email: Jonas Häusler <jonas.haeusler@doubleslash.de>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mkdocs>=1.6
9
+ Requires-Dist: pyjson5>=2.0
10
+
11
+ # mkdocs-likec4
12
+
13
+ MkDocs plugin for embedding [LikeC4](https://likec4.dev/) architecture diagrams.
14
+
15
+ ## Quick Start
16
+
17
+ 1. Ensure `likec4` and `graphviz` are available on the build system.
18
+ 2. Install the `mkdocs-likec4` plugin via `pip`:
19
+ ```shell
20
+ pip install mkdocs-likec4
21
+ ```
22
+ 3.Add the plugin to your `mkdocs.yml`:
23
+ ```yaml
24
+ plugins:
25
+ - mkdocs-likec4
26
+ ```
27
+ 4. Start embedding views in your markdown:
28
+
29
+ ````markdown
30
+ ```likec4-view
31
+ <your-view-id>
32
+ ```
33
+ ````
34
+
35
+ ## Documentation
36
+
37
+ For complete documentation with available options and examples, please read the **[Documentation](https://doubleslashde.github.io/mkdocs-likec4/)**.
38
+
39
+ ## Development
40
+
41
+ ### Registry setup
42
+
43
+ Run `./local-preview` in your terminal to build and run a MkDocs server with the plugin installed,
44
+ serving on <http://127.0.0.1:8000/>.
45
+
46
+ ## Releasing
47
+
48
+ Trigger the manual `release` workflow via GitHub Actions.
@@ -0,0 +1,9 @@
1
+ mkdocs_likec4/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mkdocs_likec4/generator.py,sha256=eQRJA6Y0v-OEN27qrhvuUnECVSclsJuNcOXFTyT7HWg,2476
3
+ mkdocs_likec4/parser.py,sha256=oXSWBZYjlPS_COGbY4Gk00M_C_dmsMiCxGpTWnUsAso,2487
4
+ mkdocs_likec4/plugin.py,sha256=j3QR0UNlhmQlp2bgxJKgJzHc9ixQH5XvWDSuan_TOGc,4702
5
+ mkdocs_likec4-1.0.0.dist-info/METADATA,sha256=9bpXNv5FCrcJUPQj58KrCExgsRLvHsu9OWT3A1p_FFU,1149
6
+ mkdocs_likec4-1.0.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
7
+ mkdocs_likec4-1.0.0.dist-info/entry_points.txt,sha256=yTY55XeHbJmJzDPLuQ7QsC5VA0_yu0G45swAWa3tNA8,60
8
+ mkdocs_likec4-1.0.0.dist-info/top_level.txt,sha256=-ogze9a_5heAOroarc-uU6RpCdJzYuZnToH0gVek58k,14
9
+ mkdocs_likec4-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ likec4 = mkdocs_likec4.plugin:LikeC4Plugin
@@ -0,0 +1 @@
1
+ mkdocs_likec4