Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support dark mode #44

Merged
merged 13 commits into from
Sep 12, 2024
27 changes: 20 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,19 @@ plugins:
num_workers: 8
puml_keyword: puml
verify_ssl: true
auto_dark: false
```

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 `False`| Designates whether the plugin should also generate dark mode images |


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

Expand All @@ -63,6 +66,16 @@ 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 Support

The module supports dark mode, this can be enabled with the `auto_dark` option.

When this option is set the module will generate a second copy of the diagram in dark mode using the `/dsvg` server option.

In order to dynamically switch the image choice the module include a javascript file [dark.js](mkdocs_puml/static/dark.js).

> **Tip:** Try it with the `skinparam backgroundColor transparent` directive in your puml to see if it looks better :)

### 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
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")
102 changes: 80 additions & 22 deletions mkdocs_puml/plugin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import typing
import re
import uuid
import os
import shutil

from mkdocs.config.config_options import Type, Config
from mkdocs.plugins import BasePlugin
Expand Down Expand Up @@ -32,20 +34,25 @@ class PlantUMLPlugin(BasePlugin):
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
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=False)),
)

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 +62,7 @@ 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 instances are populated in this event.

Args:
config: Full mkdocs.yml config file. To access configs of PlantUMLPlugin only,
Expand All @@ -64,13 +71,21 @@ 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']
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_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}(.+?)```", flags=re.DOTALL)
self.puml_keyword = self.config["puml_keyword"]
self.regex = re.compile(rf"```{self.puml_keyword}(\n.+?)```", flags=re.DOTALL)
OnceUponALoop marked this conversation as resolved.
Show resolved Hide resolved
self.auto_dark = self.config["auto_dark"]
return config

def on_page_markdown(self, markdown: str, *args, **kwargs) -> str:
Expand All @@ -94,8 +109,7 @@ 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
Expand All @@ -111,10 +125,17 @@ def on_env(self, env, *args, **kwargs):
Returns:
Jinja environment
"""
resp = self.puml.translate(self.diagrams.values())

for key, svg in zip(self.diagrams.keys(), resp):
self.diagrams[key] = svg
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.items(), 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.items(), svgs):
self.diagrams[key] = (svg, None)
return env

def on_post_page(self, output: str, page, *args, **kwargs) -> str:
Expand All @@ -137,16 +158,53 @@ def on_post_page(self, output: str, page, *args, **kwargs) -> str:
# 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:
if hasattr(page, "html") and page.html is not None:
page.html = self._replace(v, page.html)

# Inject custom JavaScript only if PUML diagrams are present
script_tag = '<script src="assets/javascripts/puml/dark.js"></script>'
if script_tag not in output:
output = output.replace("</body>", f"{script_tag}</body>")

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="{self.div_class_name}" data-puml-theme="light">{light_svg}</div>'
f'<div class="{self.div_class_name}" data-puml-theme="dark" style="display:none;">{dark_svg}</div>'
)
else:
replacement = f'<div class="{self.div_class_name}">{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
static_dir = os.path.join(os.path.dirname(__file__), "static")
# Destination directory in the site output
dest_dir = os.path.join(config["site_dir"], "assets/javascripts/puml")

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

# Copy static files
for file_name in os.listdir(static_dir):
full_file_name = os.path.join(static_dir, file_name)
if os.path.isfile(full_file_name):
shutil.copy(full_file_name, dest_dir)
28 changes: 17 additions & 11 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 @@ -124,4 +130,4 @@ def _stylize_svg(self, svg: Element):
It can be used to add support of light / dark theme.
"""
svg.setAttribute('preserveAspectRatio', "xMidYMid meet")
svg.setAttribute('style', 'background: #ffffff')
svg.setAttribute("style", "background: var(--md-default-bg-color)")
Loading