Skip to content

Commit

Permalink
Merge pull request #47 from MikhailKravets/1.4.0
Browse files Browse the repository at this point in the history
1.4.0
  • Loading branch information
MikhailKravets authored Oct 2, 2024
2 parents 1574fb5 + 1c01868 commit e7968de
Show file tree
Hide file tree
Showing 14 changed files with 738 additions and 513 deletions.
8 changes: 4 additions & 4 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pytest = "*"
pytest-cov = "*"
flake8 = "*"
pre-commit = "*"
importlib-metadata = {version = "*", markers = "python_version < '3.10'"}
zipp = {version = "*", markers = "python_version < '3.10'"}
exceptiongroup = {version = "*", markers = "python_version < '3.11'"}
tomli = {version = "*", markers = "python_version < '3.11'"}
importlib-metadata = {version = ">=8.5", markers = "python_version < '3.10'"}
zipp = {version = ">=3.20", markers = "python_version < '3.10'"}
exceptiongroup = {version = ">=1.2", markers = "python_version < '3.11'"}
tomli = {version = ">=2.0", markers = "python_version < '3.11'"}

[requires]
python_version = "3.12"
862 changes: 450 additions & 412 deletions Pipfile.lock

Large diffs are not rendered by default.

27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,18 @@ plugins:
num_workers: 8
puml_keyword: puml
verify_ssl: true
auto_dark: true
```

Where

| Parmeter | Type | Descripton |
|----------|----------------------|----------------------------------------------------------------|
| `puml_url` | `str`. Required | URL to the plantuml service |
| `num_workers` | `int`. Default `8` | Max amount of concurrent workers that request plantuml service |
| `puml_keyword` | `str`. Default `puml` | The keyword for PlantUML code fence, i.e. \```puml \``` |
| `verify_ssl` | `bool`. Default `True` | Designates whether `requests` should verify SSL or not |
| Parameter | Type | Description |
|----------------|------------------------|-----------------------------------------------------------------------------|
| `puml_url` | `str`. Required | URL to the PlantUML service |
| `num_workers` | `int`. Default `8` | Max amount of concurrent workers that request the PlantUML service |
| `puml_keyword` | `str`. Default `puml` | The keyword for PlantUML code fence, i.e. \```puml \``` |
| `verify_ssl` | `bool`. Default `True` | Designates whether `requests` should verify SSL or not |
| `auto_dark` | `bool`. Default `True` | Enables `slate` diagrams generation |

Now, you can put your puml diagrams into your `.md` documentation. For example,

Expand All @@ -63,6 +65,17 @@ Bob -> Alice : hello
At the build step `mkdocs` sends requests to `puml_url` and substitutes your
diagram with the `svg` images from the responses.

### Dark Mode

Since version `1.4.0` this plugin integrates with [mkdocs-material](https://squidfunk.github.io/mkdocs-material/)
to display diagrams based on the current theme.
However, it comes at a cost: the plugin needs to generate two copies of each
diagram — one for the light theme and another for the dark one.
During the generation of dark-themed diagrams, the plugin uses a `/dsvg` path when requesting PlantUML server.

ℹ️ You can use `skinparam backgroundColor transparent` directive inside your puml code which
can enhance the appearance of your diagrams when the dark theme is enabled.

### Run PlantUML service with Docker

It is possible to run [plantuml/plantuml-server](https://hub.docker.com/r/plantuml/plantuml-server)
Expand Down Expand Up @@ -112,7 +125,7 @@ Jon -> Sansa : hello
@enduml
"""
puml = PlantUML(puml_url, num_worker=2)
puml = PlantUML(puml_url, num_workers=2)
svg_for_diag1, svg_for_diag2 = puml.translate([diagram1, diagram2])
```

Expand Down
2 changes: 1 addition & 1 deletion mkdocs_puml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Package that brings PlantUML to MkDocs"""

__version__ = "1.3.1"
__version__ = "1.4.0"
11 changes: 4 additions & 7 deletions mkdocs_puml/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
import string
from zlib import compress

__all__ = ('encode',)
__all__ = ("encode",)

_B64_CHARS = f"{string.ascii_uppercase}{string.ascii_lowercase}{string.digits}+/"
_PUML_CHARS = f"{string.digits}{string.ascii_uppercase}{string.ascii_lowercase}-_"

_TRANSLATE_MAP = bytes.maketrans(
_B64_CHARS.encode('utf-8'),
_PUML_CHARS.encode('utf-8')
)
_TRANSLATE_MAP = bytes.maketrans(_B64_CHARS.encode("utf-8"), _PUML_CHARS.encode("utf-8"))


def encode(content: str) -> str:
Expand All @@ -27,7 +24,7 @@ def encode(content: str) -> str:
Encoded string that can be used in plantUML service
to build diagram images
"""
content = compress(content.encode('utf-8'))[2:-4] # 0:2 - header, -4: - checksum
content = compress(content.encode("utf-8"))[2:-4] # 0:2 - header, -4: - checksum
content = base64.b64encode(content)
content = content.translate(_TRANSLATE_MAP)
return content.decode('utf-8')
return content.decode("utf-8")
117 changes: 85 additions & 32 deletions mkdocs_puml/plugin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from pathlib import Path
import typing
import re
import uuid
import os
import shutil

from mkdocs.config.config_options import Type, Config
from mkdocs.config.base import Config
from mkdocs.config.config_options import Type
from mkdocs.plugins import BasePlugin

from mkdocs_puml.puml import PlantUML
Expand All @@ -20,8 +24,6 @@ class PlantUMLPlugin(BasePlugin):
num_workers: 10
Attributes:
div_class_name (str): the class that will be set to resulting <div> tag
containing the diagram
pre_class_name (str): the class that will be set to intermediate <pre> tag
containing uuid code
config_scheme (str): config scheme to set by user in mkdocs.yml file
Expand All @@ -31,21 +33,25 @@ class PlantUMLPlugin(BasePlugin):
puml (PlantUML): PlantUML instance that requests PlantUML service
diagrams (dict): Dictionary containing the diagrams (puml and later svg) and their keys
puml_keyword (str): keyword used to find PlantUML blocks within Markdown files
verify_ssl (bool): Designates whether the ``requests`` should verify SSL certiticate
verify_ssl (bool): Designates whether the ``requests`` should verify SSL certificate
auto_dark (bool): Designates whether the plugin should automatically generate dark mode images.
"""
div_class_name = "puml"

pre_class_name = "diagram-uuid"

config_scheme = (
('puml_url', Type(str, required=True)),
('num_workers', Type(int, default=8)),
('puml_keyword', Type(str, default='puml')),
('verify_ssl', Type(bool, default=True))
("puml_url", Type(str, required=True)),
("num_workers", Type(int, default=8)),
("puml_keyword", Type(str, default="puml")),
("verify_ssl", Type(bool, default=True)),
("auto_dark", Type(bool, default=True)),
)

def __init__(self):
self.regex: typing.Optional[typing.Any] = None
self.uuid_regex = re.compile(rf'<pre class="{self.pre_class_name}">(.+?)</pre>', flags=re.DOTALL)
self.uuid_regex = re.compile(
rf'<pre class="{self.pre_class_name}">(.+?)</pre>', flags=re.DOTALL
)

self.puml: typing.Optional[PlantUML] = None
self.diagrams = {
Expand All @@ -55,7 +61,8 @@ def __init__(self):
def on_config(self, config: Config) -> Config:
"""Event that is fired by mkdocs when configs are created.
self.puml instance is populated in this event.
self.puml_light, self.puml_dark instances are populated in this event.
Also, `puml.css` that enable dark / light mode styles is added to `extra_css`.
Args:
config: Full mkdocs.yml config file. To access configs of PlantUMLPlugin only,
Expand All @@ -64,13 +71,22 @@ def on_config(self, config: Config) -> Config:
Returns:
Full config of the mkdocs
"""
self.puml = PlantUML(
self.config['puml_url'],
num_workers=self.config['num_workers'],
verify_ssl=self.config['verify_ssl']
config['extra_css'].append("assets/stylesheets/puml.css")
self.puml_light = PlantUML(
self.config["puml_url"],
num_workers=self.config["num_workers"],
verify_ssl=self.config["verify_ssl"],
output_format="svg",
)
self.puml_keyword = self.config['puml_keyword']
self.regex = re.compile(rf"```{self.puml_keyword}(.+?)```", flags=re.DOTALL)
self.puml_dark = PlantUML(
self.config["puml_url"],
num_workers=self.config["num_workers"],
verify_ssl=self.config["verify_ssl"],
output_format="dsvg",
)
self.puml_keyword = self.config["puml_keyword"]
self.regex = re.compile(rf"```{self.puml_keyword}(\n.+?)```", flags=re.DOTALL)
self.auto_dark = self.config["auto_dark"]
return config

def on_page_markdown(self, markdown: str, *args, **kwargs) -> str:
Expand All @@ -94,33 +110,42 @@ def on_page_markdown(self, markdown: str, *args, **kwargs) -> str:
id_ = str(uuid.uuid4())
self.diagrams[id_] = v
markdown = markdown.replace(
f"```{self.puml_keyword}{v}```",
f'<pre class="{self.pre_class_name}">{id_}</pre>'
f"```{self.puml_keyword}{v}```", f'<pre class="{self.pre_class_name}">{id_}</pre>'
)

return markdown

def on_env(self, env, *args, **kwargs):
"""The event is fired when jinja environment is configured.
Such as it is fired once when all .md pages are processed,
we can use it to request PlantUML service to convert our
we can use it to request PlantUML service and convert the
diagrams.
Args:
env: jinja environment
Returns:
Jinja environment
"""
resp = self.puml.translate(self.diagrams.values())

for key, svg in zip(self.diagrams.keys(), resp):
self.diagrams[key] = svg
# TODO: self.diagrams changes its structure throughout a program run:
# 1. Initially it's {key: "value"}
# 2. After translate, it's {key: ("light_svg", "dark_svg" | None)}
# 3. Align to a single format
diagram_contents = [diagram for diagram in self.diagrams.values()]

if self.auto_dark:
light_svgs = self.puml_light.translate(diagram_contents)
dark_svgs = self.puml_dark.translate(diagram_contents)
for key, light_svg, dark_svg in zip(self.diagrams, light_svgs, dark_svgs):
self.diagrams[key] = (light_svg, dark_svg)
else:
svgs = self.puml_light.translate(diagram_contents)
for key, svg in zip(self.diagrams, svgs):
self.diagrams[key] = (svg, None)
return env

def on_post_page(self, output: str, page, *args, **kwargs) -> str:
"""The event is fired after HTML page is rendered.
Here, we substitute <pre> tags with uuid codes of diagrams
with the corresponding SVG images.
Here, we substitute <pre> tags with the corresponding SVG images.
Args:
output: rendered HTML in str format
Expand All @@ -132,21 +157,49 @@ def on_post_page(self, output: str, page, *args, **kwargs) -> str:
schemes = self.uuid_regex.findall(output)
for v in schemes:
output = self._replace(v, output)
page.content = self._replace(v, page.content)
page.content = output

# MkDocs >=1.4 doesn't have html attribute.
# This is required for integration with mkdocs-print-page plugin.
# TODO: Remove the support of older versions in future releases
if hasattr(page, 'html') and page.html is not None:
page.html = self._replace(v, page.html)
if hasattr(page, "html") and page.html is not None:
page.html = output

return output

def _replace(self, key: str, content: str) -> str:
"""Replace a UUID key with a real diagram in a
content
"""
return content.replace(
f'<pre class="{self.pre_class_name}">{key}</pre>',
f'<div class="{self.div_class_name}">{self.diagrams[key]}</div>'
light_svg, dark_svg = self.diagrams[key]
if dark_svg:
replacement = (
f'<div class="puml light">{light_svg}</div>'
f'<div class="puml dark">{dark_svg}</div>'
)
else:
replacement = f'<div class="puml light">{light_svg}</div>'
return content.replace(f'<pre class="{self.pre_class_name}">{key}</pre>', replacement)

def on_post_build(self, config):
"""
Event triggered after the build process is complete.
This method is responsible for copying static files from the plugin's
`static` directory to the specified `assets/javascripts/puml` directory
in the site output. This ensures that the necessary JavaScript files
are available in the final site.
Args:
config (dict): The MkDocs configuration object.
"""
# Path to the static directory in the plugin
puml_css = Path(__file__).parent.joinpath("static/puml.css")
# Destination directory in the site output
dest_dir = Path(config["site_dir"]).joinpath("assets/stylesheets/")

if not dest_dir.exists():
os.makedirs(dest_dir)

shutil.copy(puml_css, dest_dir)
30 changes: 18 additions & 12 deletions mkdocs_puml/puml.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,26 @@ class PlantUML:
base_url (str): Base URL to the PUML service
num_workers (int): The size of pool to run requests in
verify_ssl (bool): Designates whether the ``requests`` should verify SSL certiticate
output_format (str): The output format for the diagrams (e.g., "svg" or "dsvg")
Examples:
Use this class as::
puml = PlantUML("https://www.plantuml.com")
svg = puml.translate([diagram])[0]
"""
_format = 'svg'

_html_comment_regex = re.compile(r"<!--.*?-->", flags=re.DOTALL)

def __init__(self, base_url: str, num_workers: int = 5, verify_ssl: bool = True):
self.base_url = base_url if base_url.endswith('/') else f"{base_url}/"
def __init__(
self,
base_url: str,
num_workers: int = 5,
verify_ssl: bool = True,
output_format: str = "svg",
):
self.base_url = base_url if base_url.endswith("/") else f"{base_url}/"
self.base_url = f"{self.base_url}{output_format}/"

if num_workers <= 0:
raise ValueError("`num_workers` argument should be bigger than 0.")
Expand All @@ -46,8 +54,9 @@ def translate(self, diagrams: typing.Iterable[str]) -> typing.List[str]:
Args:
diagrams (list): string representation of PUML diagram
Returns:
SVG image of built diagram
SVG image of built diagram
"""
encoded = [self.preprocess(v) for v in diagrams]

Expand Down Expand Up @@ -98,13 +107,10 @@ def request(self, encoded_diagram: str) -> str:
Returns:
SVG representation of the diagram
"""
resp = requests.get(
urljoin(self.base_url, f"{self._format}/{encoded_diagram}"),
verify=self.verify_ssl
)
resp = requests.get(urljoin(self.base_url, encoded_diagram), verify=self.verify_ssl)

# Use 'ignore' to strip non-utf chars
return resp.content.decode('utf-8', errors='ignore')
return resp.content.decode("utf-8", errors="ignore")

def _clean_comments(self, content: str) -> str:
return self._html_comment_regex.sub("", content)
Expand All @@ -114,7 +120,7 @@ def _convert_to_dom(self, content: str) -> Element:
for future modifications
"""
dom = parseString(content) # nosec
svg = dom.getElementsByTagName('svg')[0]
svg = dom.getElementsByTagName("svg")[0]
return svg

def _stylize_svg(self, svg: Element):
Expand All @@ -123,5 +129,5 @@ def _stylize_svg(self, svg: Element):
Notes:
It can be used to add support of light / dark theme.
"""
svg.setAttribute('preserveAspectRatio', "true")
svg.setAttribute('style', 'background: #ffffff')
svg.setAttribute('preserveAspectRatio', "xMidYMid meet")
svg.setAttribute("style", "background: var(--md-default-bg-color)")
13 changes: 13 additions & 0 deletions mkdocs_puml/static/puml.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
body[data-md-color-scheme="default"] .puml.light {
display: block;
}
body[data-md-color-scheme="slate"] .puml.light {
display: none;
}

body[data-md-color-scheme="default"] .puml.dark {
display: none;
}
body[data-md-color-scheme="slate"] .puml.dark {
display: block;
}
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ Home = "https://github.com/MikhailKravets/mkdocs_puml"

[project.entry-points."mkdocs.plugins"]
plantuml = "mkdocs_puml.plugin:PlantUMLPlugin"

[tool.flit.sdist]
# Specify the files to include in the source distribution
include = ["mkdocs_puml/static/puml.css"]
Loading

0 comments on commit e7968de

Please sign in to comment.