Repository: MikhailKravets/mkdocs_puml

Issue:
Dynamically show light/dark mode SVGs
**Is your feature request related to a problem? Please describe.**

SVG files generated by plantuml.com using the default URL do not look good when embedded in documentation sites that use a dark theme. 

A (relatively) simple fix for that would be to add an option for specifying the file-type path, i.e. letting the user specify `dsvg` instead of `svg` as a plugin option, for modifying the part of the URL (in the case of plantuml.com) that follows `/plantuml/`.

That fix does not take into consideration sites (like those with typical MkDocs+Material [configs](https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/?h=dark#color-palette-toggle)) that allow the user to change the theme.

**Describe the solution you'd like**

It would be wonderful if the plugin could fetch both the regular and the dark mode SVGs and then dynamically display the correct one depending on which theme is active.

**Describe alternatives you've considered**

A plugin option for simply specifying the entire path parameter, as described above.

Hints from issue comments:
@MikhailKravets: Hi @ari-noumena! That's a great idea. I think about light / dark theme implementation too but I didn't come up with the good solution yet. 

We need to add themed styles to the `svg` generated by PlangUML (this will be ideal and correct solution). This way we'll be able to use material design theme toggle to change theme of our diagrams. There is already a method [_stylize_svg:115](https://github.com/MikhailKravets/mkdocs_puml/blob/ea75769dae7146fb9f4aefc741dc80f23f4d64a9/mkdocs_puml/puml.py#L115) that's designed to post-process svg styles. The problem is that we need to define all the rules by which PlangUML generates svg and use them to add dark theme to the svg element before placing it in the documentation page.

This way we don't even need any additional option.

@MikhailKravets: The plugin now supports the dark mode when used with `mkdocs-material`.

Solution patch:
diff --git a/README.md b/README.md
index cb40d6a..bb5f5cf 100644
--- a/README.md
+++ b/README.md
@@ -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,
 
@@ -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)
@@ -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])
 ```
 
diff --git a/mkdocs_puml/encoder.py b/mkdocs_puml/encoder.py
index 07faa2a..1a476b4 100644
--- a/mkdocs_puml/encoder.py
+++ b/mkdocs_puml/encoder.py
@@ -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:
@@ -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")
diff --git a/mkdocs_puml/plugin.py b/mkdocs_puml/plugin.py
index f6a086a..5ed465c 100644
--- a/mkdocs_puml/plugin.py
+++ b/mkdocs_puml/plugin.py
@@ -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
@@ -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 = {
@@ -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,
@@ -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)
+        self.auto_dark = self.config["auto_dark"]
         return config
 
     def on_page_markdown(self, markdown: str, *args, **kwargs) -> str:
@@ -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
@@ -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:
@@ -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)
diff --git a/mkdocs_puml/puml.py b/mkdocs_puml/puml.py
index 186209c..0ec9467 100644
--- a/mkdocs_puml/puml.py
+++ b/mkdocs_puml/puml.py
@@ -18,6 +18,7 @@ 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::
@@ -25,11 +26,18 @@ class PlantUML:
             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.")
@@ -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]
 
@@ -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)
@@ -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):
@@ -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)")
diff --git a/mkdocs_puml/static/dark.js b/mkdocs_puml/static/dark.js
new file mode 100644
index 0000000..1bd9426
--- /dev/null
+++ b/mkdocs_puml/static/dark.js
@@ -0,0 +1,142 @@
+/**
+ * mkdocs-puml plugin - Auto Dark Mode Script
+ * ==========================================
+ *
+ * This script is designed to work with the mkdocs-puml plugin and the
+ * mkdocs-material theme to automatically switch between light and dark themes.
+ *
+ * Dependencies:
+ * -------------
+ * - The script requires the mkdocs-puml plugin to be installed and configured
+ *   in the MkDocs project.
+ * - The script is intended to be used with the mkdocs-material theme.
+ *
+ * Description:
+ * ------------
+ * The script listens for changes in the system preference for dark mode and
+ * updates the theme accordingly. It also listens for changes in the MkDocs
+ * theme preference and updates the theme based on the user's selection.
+ * The script toggles the visibility of PlantUML divs based on the theme to ensure
+ * that the correct image is displayed.
+ *
+ */
+
+// Set this flag to true for debugging output, false to suppress logs
+const DEBUG = false;
+
+/**
+ * Helper function for controlled logging.
+ * @param {string} message - The message to log.
+ */
+function log(message) {
+  if (DEBUG) {
+    console.log(message);
+  }
+}
+
+/**
+ * Retrieves the current theme state based on system preferences.
+ * @returns {string} - The current theme state ('dark' or 'light').
+ */
+function getThemeState() {
+  const systemPreference = window.matchMedia("(prefers-color-scheme: dark)")
+    .matches
+    ? "dark"
+    : "light";
+  log(`🌐 System Preference: ${systemPreference}`);
+  return systemPreference;
+}
+
+/**
+ * Retrieves the current theme state based on the data attribute on the body.
+ * @returns {string} - The current theme state ('dark', 'light', or 'auto').
+ */
+function getMkdocsThemeState() {
+  const colorScheme = document.body.getAttribute("data-md-color-media");
+
+  let themeState;
+  switch (colorScheme) {
+    case "(prefers-color-scheme: dark)":
+      themeState = "dark";
+      break;
+    case "(prefers-color-scheme: light)":
+      themeState = "light";
+      break;
+    case "(prefers-color-scheme)":
+      themeState = "auto";
+      break;
+    default:
+      themeState = getThemeState();
+      break;
+  }
+
+  log(`📋 Mkdocs Preference: ${themeState}`);
+  return themeState;
+}
+
+/**
+ * Updates the theme based on the provided mode.
+ * @param {string} mode - The theme mode ('dark' or 'light').
+ */
+function updateTheme(mode) {
+  log(`🔄 Updating theme`);
+  togglePumlVisibility(mode);
+}
+
+/**
+ * Toggles the visibility of PUML divs based on the theme.
+ * @param {string} mode - The theme mode ('dark' or 'light').
+ */
+function togglePumlVisibility(mode) {
+  const lightDivs = document.querySelectorAll(`[data-puml-theme="light"]`);
+  const darkDivs = document.querySelectorAll(`[data-puml-theme="dark"]`);
+
+  lightDivs.forEach((div) => {
+    div.style.display = mode === "light" ? "block" : "none";
+  });
+
+  darkDivs.forEach((div) => {
+    div.style.display = mode === "dark" ? "block" : "none";
+  });
+
+  log(`🌓 PUML visibility toggled to ${mode} mode.`);
+}
+
+// Main script logic
+document.addEventListener("DOMContentLoaded", () => {
+  // Handle initial theme setup
+  const mkdocsTheme = getMkdocsThemeState();
+  const systemTheme = getThemeState();
+  let currentTheme;
+
+  if (mkdocsTheme === "auto") {
+    currentTheme = systemTheme;
+  } else {
+    currentTheme = mkdocsTheme;
+  }
+
+  updateTheme(currentTheme);
+
+  // Listen for system preference changes if the theme is set to 'auto'
+  const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
+  darkModeMediaQuery.addEventListener("change", (e) => {
+    const newTheme = e.matches ? "dark" : "light";
+    log(`⚡ System preference changed to: ${newTheme} mode`);
+    // if not in auto mode, do nothing
+    if (getMkdocsThemeState() === "auto") {
+      updateTheme(newTheme);
+    }
+  });
+
+  // Set up event listeners for the theme toggle inputs
+  document.querySelectorAll('input[name="__palette"]').forEach((input) => {
+    input.addEventListener("change", () => {
+      log(`⚡ Mkdocs preference changed`);
+      let newTheme = getMkdocsThemeState();
+      if (newTheme === "auto") {
+        newTheme = getThemeState();
+      }
+      updateTheme(newTheme);
+    });
+  });
+});
diff --git a/pyproject.toml b/pyproject.toml
index 4de80f0..16f6b4e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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/custom.js"]
\ No newline at end of file


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 7e1be75..ed3740b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,7 +8,7 @@
 BASE_PUML_URL = "https://mocked.org/"
 BASE_PUML_KEYWORD = "puml"
 CUSTOM_PUML_KEYWORD = "plantuml"
-TESTDATA_DIR = Path(__file__).resolve().parent.joinpath('testdata')
+TESTDATA_DIR = Path(__file__).resolve().parent.joinpath("testdata")
 
 
 @pytest.fixture(scope="package")
@@ -16,18 +16,21 @@ def diagram_and_encoded():
     """The fixture to return puml diagram and
     the encoded by plantuml.com string
     """
-    return "@startuml\nBob -> Alice : hello\n@enduml", "SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80"
+    return (
+        "@startuml\nBob -> Alice : hello\n@enduml",
+        "SoWkIImgAStDuNBAJrBGjLDmpCbCJbMmKiX8pSd9vt98pKi1IW80",
+    )
 
 
 @pytest.fixture(scope="package")
 def svg_diagram():
-    with open(TESTDATA_DIR.joinpath('plantuml.svg')) as f:
+    with open(TESTDATA_DIR.joinpath("plantuml.svg")) as f:
         return f.read()
 
 
 @pytest.fixture(scope="function")
 def mock_requests(monkeypatch, svg_diagram):
     p = Mock()
-    p.return_value.content = svg_diagram.encode('utf-8')
+    p.return_value.content = svg_diagram.encode("utf-8")
     monkeypatch.setattr(requests, "get", p)
     yield p
diff --git a/tests/plugins/conftest.py b/tests/plugins/conftest.py
index 3e0e16f..d842a03 100644
--- a/tests/plugins/conftest.py
+++ b/tests/plugins/conftest.py
@@ -31,15 +31,15 @@ def is_uuid_valid(uuid_str: str) -> bool:
 @pytest.fixture
 def plugin_config():
     c = Config(schema=PlantUMLPlugin.config_scheme)
-    c['puml_url'] = BASE_PUML_URL
+    c["puml_url"] = BASE_PUML_URL
     return c
 
 
 @pytest.fixture
 def plugin_config_custom_keyword():
     c = Config(schema=PlantUMLPlugin.config_scheme)
-    c['puml_url'] = BASE_PUML_URL
-    c['puml_keyword'] = CUSTOM_PUML_KEYWORD
+    c["puml_url"] = BASE_PUML_URL
+    c["puml_keyword"] = CUSTOM_PUML_KEYWORD
     return c
 
 
@@ -53,7 +53,7 @@ def plugin_environment():
 def plant_uml_plugin(plugin_config):
     plugin = PlantUMLPlugin()
     c = Config(schema=plugin.config_scheme)
-    c['puml_url'] = BASE_PUML_URL
+    c["puml_url"] = BASE_PUML_URL
     plugin.config = c
     plugin.on_config(c)
 
@@ -64,8 +64,8 @@ def plant_uml_plugin(plugin_config):
 def plant_uml_plugin_custom_keyword(plugin_config_custom_keyword):
     plugin = PlantUMLPlugin()
     c = Config(schema=plugin.config_scheme)
-    c['puml_url'] = BASE_PUML_URL
-    c['puml_keyword'] = CUSTOM_PUML_KEYWORD
+    c["puml_url"] = BASE_PUML_URL
+    c["puml_keyword"] = CUSTOM_PUML_KEYWORD
     plugin.config = c
     plugin.on_config(c)
 
@@ -75,25 +75,24 @@ def plant_uml_plugin_custom_keyword(plugin_config_custom_keyword):
 @pytest.fixture
 def diagrams_dict(diagram_and_encoded):
     return {
-        str(uuid.uuid4()): diagram_and_encoded[0],
-        str(uuid.uuid4()): diagram_and_encoded[0],
-        str(uuid.uuid4()): diagram_and_encoded[0],
+        str(uuid.uuid4()): (diagram_and_encoded[0], None),
+        str(uuid.uuid4()): (diagram_and_encoded[0], None),
+        str(uuid.uuid4()): (diagram_and_encoded[0], None),
     }
 
 
 @pytest.fixture(scope="package")
 def md_lines():
-    with open(TESTDATA_DIR.joinpath('markdown.md')) as f:
+    with open(TESTDATA_DIR.joinpath("markdown.md")) as f:
         return f.readlines()
 
 
 @pytest.fixture
 def html_page(plugin_environment, diagrams_dict):
-    page = MagicMock(title='Test, page', file=MagicMock(), config=MagicMock())
+    page = MagicMock(title="Test, page", file=MagicMock(), config=MagicMock())
     template = plugin_environment.get_template("output.html")
     page.content = template.render(
-        uuid_class=PlantUMLPlugin.pre_class_name,
-        uuids=diagrams_dict.keys()
+        uuid_class=PlantUMLPlugin.pre_class_name, uuids=diagrams_dict.keys()
     )
     page.html = page.content
     return page
diff --git a/tests/plugins/test_plugin.py b/tests/plugins/test_plugin.py
index 458e948..ff789f6 100644
--- a/tests/plugins/test_plugin.py
+++ b/tests/plugins/test_plugin.py
@@ -2,31 +2,40 @@
 from mkdocs_puml.puml import PlantUML
 from tests.conftest import BASE_PUML_KEYWORD, BASE_PUML_URL, CUSTOM_PUML_KEYWORD
 from tests.plugins.conftest import is_uuid_valid
+import os
+import shutil
 
 
 def test_on_config(plugin_config):
+    # Test if the plugin is correctly configured with default settings
     plugin = PlantUMLPlugin()
     plugin.config = plugin_config
 
     plugin.on_config(plugin_config)
 
-    assert isinstance(plugin.puml, PlantUML)
-    assert plugin.puml.base_url == BASE_PUML_URL
+    assert isinstance(plugin.puml_light, PlantUML)
+    assert isinstance(plugin.puml_dark, PlantUML)
+    assert plugin.puml_light.base_url == BASE_PUML_URL + "svg/"
+    assert plugin.puml_dark.base_url == BASE_PUML_URL + "dsvg/"
     assert plugin.puml_keyword == BASE_PUML_KEYWORD
 
 
 def test_on_config_custom_keyword(plugin_config_custom_keyword):
+    # Test if the plugin is correctly configured with a custom keyword
     plugin = PlantUMLPlugin()
     plugin.config = plugin_config_custom_keyword
 
     plugin.on_config(plugin_config_custom_keyword)
 
-    assert isinstance(plugin.puml, PlantUML)
-    assert plugin.puml.base_url == BASE_PUML_URL
+    assert isinstance(plugin.puml_light, PlantUML)
+    assert isinstance(plugin.puml_dark, PlantUML)
+    assert plugin.puml_light.base_url == BASE_PUML_URL + "svg/"
+    assert plugin.puml_dark.base_url == BASE_PUML_URL + "dsvg/"
     assert plugin.puml_keyword == CUSTOM_PUML_KEYWORD
 
 
 def test_on_page_markdown(plant_uml_plugin, md_lines):
+    # Test if PlantUML diagrams are correctly extracted from markdown
     plant_uml_plugin.on_page_markdown("\n".join(md_lines))
 
     assert len(plant_uml_plugin.diagrams) == 2
@@ -39,6 +48,7 @@ def test_on_page_markdown(plant_uml_plugin, md_lines):
 
 
 def test_on_page_markdown_custom_keyword(plant_uml_plugin_custom_keyword, md_lines):
+    # Test if PlantUML diagrams are correctly extracted with a custom keyword
     plant_uml_plugin_custom_keyword.on_page_markdown("\n".join(md_lines))
 
     assert len(plant_uml_plugin_custom_keyword.diagrams) == 1
@@ -51,17 +61,118 @@ def test_on_page_markdown_custom_keyword(plant_uml_plugin_custom_keyword, md_lin
 
 
 def test_on_env(mock_requests, plant_uml_plugin, diagrams_dict, plugin_environment):
+    # Test if PlantUML diagrams are correctly converted to SVG
     plant_uml_plugin.diagrams = diagrams_dict
     plant_uml_plugin.on_env(plugin_environment)
 
     assert mock_requests.call_count == len(diagrams_dict)
 
-    for v in diagrams_dict.values():
-        assert v.startswith('<svg')
+    for light_svg, dark_svg in plant_uml_plugin.diagrams.values():
+        assert isinstance(light_svg, str)
+        assert light_svg.startswith("<svg")
+        assert dark_svg is None  # Since auto_dark is False by default
+
+
+def test_on_env_auto_dark(mock_requests, plant_uml_plugin, diagrams_dict, plugin_environment):
+    # Test if PlantUML diagrams are correctly converted to both light and dark SVGs
+    plant_uml_plugin.auto_dark = True
+    plant_uml_plugin.diagrams = diagrams_dict
+    mock_requests.reset_mock()
+    plant_uml_plugin.on_env(plugin_environment)
+
+    assert mock_requests.call_count == 2 * len(diagrams_dict)
+
+    for light_svg, dark_svg in plant_uml_plugin.diagrams.values():
+        assert isinstance(light_svg, str)
+        assert isinstance(dark_svg, str)
+        assert light_svg.startswith("<svg")
+        assert dark_svg.startswith("<svg")
 
 
 def test_on_post_page(plant_uml_plugin, diagrams_dict, html_page):
+    # Test if PlantUML diagrams are correctly inserted into the HTML output
     plant_uml_plugin.diagrams = diagrams_dict
     output = plant_uml_plugin.on_post_page(html_page.content, html_page)
 
     assert output.count(f'<div class="{plant_uml_plugin.div_class_name}">') == len(diagrams_dict)
+    assert '<script src="assets/javascripts/puml/dark.js"></script>' in output
+
+    # Test the case where page.html exists
+    html_page.html = html_page.content
+    output = plant_uml_plugin.on_post_page(html_page.content, html_page)
+    assert html_page.html.count(f'<div class="{plant_uml_plugin.div_class_name}">') == len(
+        diagrams_dict
+    )
+
+
+def test_on_post_page_without_html_attribute(plant_uml_plugin, diagrams_dict, html_page):
+    # Test if the plugin handles pages without the 'html' attribute correctly
+    plant_uml_plugin.diagrams = diagrams_dict
+    delattr(html_page, "html")
+    output = plant_uml_plugin.on_post_page(html_page.content, html_page)
+
+    assert output.count(f'<div class="{plant_uml_plugin.div_class_name}">') == len(diagrams_dict)
+    assert '<script src="assets/javascripts/puml/dark.js"></script>' in output
+
+
+def test_on_post_page_with_existing_script(plant_uml_plugin, diagrams_dict, html_page):
+    # Test if the plugin doesn't duplicate the script tag when it already exists
+    plant_uml_plugin.diagrams = diagrams_dict
+    html_page.content += '<script src="assets/javascripts/puml/dark.js"></script>'
+    output = plant_uml_plugin.on_post_page(html_page.content, html_page)
+
+    assert output.count('<script src="assets/javascripts/puml/dark.js"></script>') == 1
+
+
+def test_on_post_build(tmp_path, plant_uml_plugin):
+    # Test if static files are correctly copied during the build process
+    config = {"site_dir": str(tmp_path)}
+
+    dest_dir = os.path.join(tmp_path, "assets", "javascripts", "puml")
+    if os.path.exists(dest_dir):
+        shutil.rmtree(dest_dir)
+
+    plant_uml_plugin.on_post_build(config)
+
+    assert os.path.exists(dest_dir)
+    static_dir = os.path.join(
+        os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "mkdocs_puml", "static"
+    )
+    for file_name in os.listdir(static_dir):
+        assert os.path.exists(os.path.join(dest_dir, file_name))
+
+    # Test when the directory already exists
+    plant_uml_plugin.on_post_build(config)
+
+    for file_name in os.listdir(static_dir):
+        assert os.path.exists(os.path.join(dest_dir, file_name))
+
+
+def test_on_post_build_with_subdirectory(tmp_path, plant_uml_plugin):
+    # Test if the plugin correctly handles subdirectories in the static folder
+    config = {"site_dir": str(tmp_path)}
+
+    plant_uml_plugin.on_post_build(config)
+
+    dest_dir = os.path.join(tmp_path, "assets", "javascripts", "puml")
+    for item in os.listdir(dest_dir):
+        assert os.path.isfile(os.path.join(dest_dir, item))
+
+
+def test_replace_method(plant_uml_plugin):
+    # Test if the _replace method correctly handles both light and dark SVGs
+    key = "test_key"
+    content = f'<pre class="{plant_uml_plugin.pre_class_name}">{key}</pre>'
+
+    # Test without dark SVG
+    plant_uml_plugin.diagrams[key] = ("<svg>light</svg>", None)
+    result = plant_uml_plugin._replace(key, content)
+    assert f'<div class="{plant_uml_plugin.div_class_name}"><svg>light</svg></div>' in result
+
+    # Test with dark SVG
+    plant_uml_plugin.diagrams[key] = ("<svg>light</svg>", "<svg>dark</svg>")
+    result = plant_uml_plugin._replace(key, content)
+    assert 'data-puml-theme="light"' in result
+    assert 'data-puml-theme="dark"' in result
+    assert "<svg>light</svg>" in result
+    assert "<svg>dark</svg>" in result
diff --git a/tests/test_encoder.py b/tests/test_encoder.py
index 9d9dbc2..239acaf 100644
--- a/tests/test_encoder.py
+++ b/tests/test_encoder.py
@@ -1,7 +1,8 @@
 from mkdocs_puml.encoder import encode
 
 
-def test_encode(diagram_and_encoded: tuple[str]):
+def test_encode(diagram_and_encoded: tuple[str, str]):
+    # Ensures the encoded output matches the expected result.
     diagram, expected = diagram_and_encoded
     encoded = encode(diagram)
 
diff --git a/tests/test_puml.py b/tests/test_puml.py
index 675e502..ce5353f 100644
--- a/tests/test_puml.py
+++ b/tests/test_puml.py
@@ -5,21 +5,28 @@
 
 
 def test_url_with_slash():
+    # Verify base_url ends with slash when provided with trailing slash
     puml = PlantUML(BASE_PUML_URL)
-    assert puml.base_url.endswith('/')
+    assert puml.base_url.endswith("/")
 
 
 def test_url_without_slash():
+    # Ensure base_url ends with slash when provided without trailing slash
     puml = PlantUML(BASE_PUML_URL[:-1])
-    assert puml.base_url.endswith('/')
+    assert puml.base_url.endswith("/")
 
 
 def test_num_worker_less_or_equal_zero():
-    with pytest.raises(ValueError):
+    # Confirm ValueError raised for invalid num_workers
+    with pytest.raises(ValueError, match="`num_workers` argument should be bigger than 0."):
         PlantUML(BASE_PUML_URL, num_workers=0)
 
+    with pytest.raises(ValueError, match="`num_workers` argument should be bigger than 0."):
+        PlantUML(BASE_PUML_URL, num_workers=-1)
 
-def test_translate(diagram_and_encoded: tuple[str], mock_requests):
+
+def test_translate(diagram_and_encoded: tuple[str, str], mock_requests):
+    # Verify translation of multiple diagrams to SVG
     diagram, encoded = diagram_and_encoded
 
     diagrams = [diagram] * 2
@@ -27,7 +34,7 @@ def test_translate(diagram_and_encoded: tuple[str], mock_requests):
     puml = PlantUML(BASE_PUML_URL)
     resp = puml.translate(diagrams)
 
-    assert puml.base_url.endswith('/')
+    assert puml.base_url.endswith("/")
 
     assert len(resp) == 2