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.
- mkdocs_likec4/__init__.py +0 -0
- mkdocs_likec4/generator.py +73 -0
- mkdocs_likec4/parser.py +80 -0
- mkdocs_likec4/plugin.py +122 -0
- mkdocs_likec4-1.0.0.dist-info/METADATA +48 -0
- mkdocs_likec4-1.0.0.dist-info/RECORD +9 -0
- mkdocs_likec4-1.0.0.dist-info/WHEEL +5 -0
- mkdocs_likec4-1.0.0.dist-info/entry_points.txt +2 -0
- mkdocs_likec4-1.0.0.dist-info/top_level.txt +1 -0
|
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
|
+
)
|
mkdocs_likec4/parser.py
ADDED
|
@@ -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
|
+
)
|
mkdocs_likec4/plugin.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
mkdocs_likec4
|