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

Feature/add icon packs #5

Merged
merged 2 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ venv/
ENV/
env.bak/
venv.bak/
.venv/

# Spyder project settings
.spyderproject
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ repos:
rev: v0.17.2
hooks:
- id: markdownlint-cli2
exclude: tests/data/test_1.md
exclude: tests/data/.*.md


- repo: https://github.com/codespell-project/codespell
Expand Down
54 changes: 54 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,43 @@ graph TB
</script>
```

### Adding Custom Icon Packs

Some newer MermaidJS diagram types (most notably [Architecture](https://mermaid.js.org/syntax/architecture.html)),
support referencing custom icon packs that are registered (i.e. https://mermaid.js.org/config/icons.html).

To register packs, you can add them to the extension config with a structure of ```icon_packs: {"pack_name" : "pack_url" }```, i.e.:

```python
import markdown

html = markdown.markdown(
text,
extensions=["markdown-mermaidjs"],
extension_configs={
"markdown_mermaidjs": {
"icon_packs": {
"logos": "https://unpkg.com/@iconify-json/logos@1/icons.json",
"hugeicons": "https://unpkg.com/@iconify-json/hugeicons@1/icons.json",
}
}
},
)
```

The resulting HTML should be nearly identical, but the icon packs should be registered, e.g.:

```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.registerIconPacks([
{ name: 'logos', loader: () => fetch('https://unpkg.com/@iconify-json/logos@1/icons.json').then((res) => res.json()) },
{ name: 'hugeicons', loader: () => fetch('https://unpkg.com/@iconify-json/hugeicons@1/icons.json').then((res) => res.json()) }
]);
mermaid.initialize({ startOnLoad: true });
</script>
```

### Use it with [Pelican](https://getpelican.com/)

Add `"markdown_mermaidjs": {}` to `MARKDOWN["extension_configs"]` in your `pelicanconf.py`.
Expand All @@ -97,6 +134,23 @@ MARKDOWN = {
}
```

#### Icon Packs via Pelican

Similarly, with the extension config, you can add it in the `pelicanconf.py`.

```python
MARKDOWN = {
"extension_configs": {
"markdown_mermaidjs": {
"icon_packs": {
"logos": "https://unpkg.com/@iconify-json/logos@1/icons.json",
"hugeicons": "https://unpkg.com/@iconify-json/hugeicons@1/icons.json",
}
},
},
}
```

## Contributing

See [Contributing](contributing.md)
Expand Down
23 changes: 23 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ python -m pipx ensurepath
* [uv](https://github.com/astral-sh/uv/): for dependency management
* [invoke](https://github.com/pyinvoke/invoke): for task management

#### Notes for Windows Developers

By default, the tasks leverage [pty](https://docs.python.org/3/library/pty.html) under the covers, which
does not support Windows. Either run this via WSL or pass --no-pty to commands that demand it.

### Step 6. Create your local Python virtual environment and install dependencies

```sh
Expand All @@ -45,13 +50,25 @@ Note that this project follows [conventional-commit](https://www.conventionalcom
inv git.commit
```

or on Windows (currently throws an error when run outside of cmd.exe):

```sh
inv git.commit --no-pty
```

### Step 8. Run test cases
Make sure all test cases pass.

```sh
inv test
```

or on Windows:

```sh
inv test --no-pty
```

### Step 9. Run test coverage
Check the test coverage and see where you can add test cases.

Expand Down Expand Up @@ -82,4 +99,10 @@ Ensure the packages installed are secure, and no server vulnerability is introdu
inv secure
```

or on Windows:

```sh
inv secure --no-pty
```

### Step 13. Create a Pull Request and celebrate 🎉
120 changes: 84 additions & 36 deletions markdown_mermaidjs/markdown_mermaidjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,105 @@
from markdown import Markdown


MERMAID_CODEBLOCK_START = re.compile(r"^(?P<code_block_sign>[\~\`]{3})[Mm]ermaid\s*$")
MERMAID_JS_SCRIPT = """
class MermaidPreprocessor(Preprocessor):
MERMAID_CODEBLOCK_START = re.compile(
r"^(?P<code_block_sign>[\~\`]{3})[Mm]ermaid\s*$"
)

def __init__(self, md: Markdown, icon_packs: dict | None = None) -> None:
self.icon_packs = icon_packs
super().__init__(md)

@property
def icon_packs_calls(self) -> list[str]:
return (
[
f"{{ name: '{name}', loader: () => fetch('{url}').then((res) => res.json()) }}"
for name, url in self.icon_packs.items()
]
if self.icon_packs
else []
)

def generate_mermaid_init_script(self) -> list[str]:
icon_packs_calls = ""

calls = self.icon_packs_calls
if len(calls):
callstr = "\n,".join(calls)
icon_packs_calls = f"""
mermaid.registerIconPacks([
{callstr}
]);"""

script_module = f"""
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true });
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';{icon_packs_calls}
mermaid.initialize({{ startOnLoad: true }});
</script>
"""


def add_mermaid_script_and_tag(lines: list[str]) -> list[str]:
result_lines: list[str] = []
in_mermaid_codeblock: bool = False
exist_mermaid_codeblock: bool = False

codeblock_end_pattern = re.compile("```")
for line in lines:
if in_mermaid_codeblock:
match_codeblock_end = codeblock_end_pattern.match(line)
if match_codeblock_end:
in_mermaid_codeblock = False
result_lines.append("</div>")
return script_module.split("\n")

def add_mermaid_script_and_tag(self, lines: list[str]) -> list[str]:
result_lines: list[str] = []
in_mermaid_codeblock: bool = False
exist_mermaid_codeblock: bool = False

codeblock_end_pattern = re.compile("```")
for line in lines:
if in_mermaid_codeblock:
match_codeblock_end = codeblock_end_pattern.match(line)
if match_codeblock_end:
in_mermaid_codeblock = False
result_lines.append("</div>")
continue

match_mermaid_codeblock_start = self.MERMAID_CODEBLOCK_START.match(line)
if match_mermaid_codeblock_start:
exist_mermaid_codeblock = True
in_mermaid_codeblock = True
codeblock_sign = match_mermaid_codeblock_start.group("code_block_sign")
codeblock_end_pattern = re.compile(rf"{codeblock_sign}\s*")
result_lines.append('<div class="mermaid">')
continue

match_mermaid_codeblock_start = MERMAID_CODEBLOCK_START.match(line)
if match_mermaid_codeblock_start:
exist_mermaid_codeblock = True
in_mermaid_codeblock = True
codeblock_sign = match_mermaid_codeblock_start.group("code_block_sign")
codeblock_end_pattern = re.compile(rf"{codeblock_sign}\s*")
result_lines.append('<div class="mermaid">')
continue

result_lines.append(line)

if exist_mermaid_codeblock:
result_lines.extend(MERMAID_JS_SCRIPT.split("\n"))
return result_lines
result_lines.append(line)

if exist_mermaid_codeblock:
result_lines.extend(self.generate_mermaid_init_script())
return result_lines

class MermaidPreprocessor(Preprocessor):
def run(self, lines: list[str]) -> list[str]:
return add_mermaid_script_and_tag(lines)
return self.add_mermaid_script_and_tag(lines)


class MermaidExtension(Extension):
"""Add source code highlighting to markdown codeblocks."""
"""Add mermaid diagram markdown codeblocks."""

def __init__(self, **kwargs: dict[str, Any]) -> None:
self.config = {
"icon_packs": [
{},
"Dictionary of icon packs to use: { name(str) : url(str) }. Default: {} (no icon packs). example: { 'logos' : 'https://unpkg.com/@iconify-json/logos@1/icons.json' } corresponds to the json file example here: https://mermaid.js.org/config/icons.html",
Lee-W marked this conversation as resolved.
Show resolved Hide resolved
],
}

super().__init__(**kwargs)

self.icon_packs: dict[str, str] = {}
config_packs = (
self.getConfig("icon_packs", default={}) or {}
) # for the None case
self.icon_packs.update(config_packs)

def extendMarkdown(self, md: Markdown) -> None:
"""Add HilitePostprocessor to Markdown instance."""
"""Add MermaidExtension to Markdown instance."""
# Insert a preprocessor before ReferencePreprocessor
md.preprocessors.register(MermaidPreprocessor(md), "mermaid", 35)

md.preprocessors.register(
MermaidPreprocessor(md, icon_packs=self.icon_packs), "mermaid", 35
)
md.registerExtension(self)


Expand Down
14 changes: 8 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
[project]
name = "markdown-mermaidjs"
version = "1.0.0"
authors = [{ name = "Wei Lee", email = "weilee.rx@gmail.com" }]
authors = [
{ name = "Wei Lee", email = "weilee.rx@gmail.com" },
{ name = "Owyn Richen", email = "owynrichen@gmail.com" },
]
description = "Python-Markdown extension to add Mermaid graph"
license = "GPL-3.0-only"
readme = "docs/README.md"
Expand Down Expand Up @@ -35,15 +38,14 @@ test = [
"pytest-cov>=6.0.0",
"pytest-mock>=3.14.0",
]
style = [
"mypy>=1.14.1",
"ruff>=0.9.2",
"types-markdown>=3.7.0.20241204",
]
style = ["mypy>=1.14.1", "ruff>=0.9.2", "types-markdown>=3.7.0.20241204"]
security = ["bandit>=1.2.2", "pip-audit>=2.7.3"]
git = ["commitizen>=4.1.0", "pre-commit>=4.0.1"]
doc = ["mkdocs>=1.6.1", "mkdocs-material>=9.5.50"]

[tool.uv]
default-groups = ["dev", "test", "style", "security", "git"]


[tool.commitizen]
name = "cz_conventional_commits"
Expand Down
36 changes: 36 additions & 0 deletions tests/data/test_icons_1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Title

Testing 'registerIconPacks' as used by https://mermaid.js.org/syntax/architecture.html.

```mermaid
architecture-beta
group api(logos:aws-lambda)[API]

service db(logos:aws-aurora)[Database] in api
service disk1(logos:aws-glacier)[Storage] in api
service disk2(logos:aws-s3)[Storage] in api
service server(logos:aws-ec2)[Server] in api

db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
```

It only supports full url lazy-loading: https://mermaid.js.org/config/icons.html

~~~mermaid
architecture-beta
group api(logos:aws-lambda)[API]

service db(logos:aws-aurora)[Database] in api
service disk1(logos:aws-glacier)[Storage] in api
service disk2(logos:aws-s3)[Storage] in api
service server(logos:aws-ec2)[Server] in api

db:L -- R:server
disk1:T -- B:server
disk2:T -- B:db
~~~


End of the file
5 changes: 5 additions & 0 deletions tests/data/test_icons_2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Title

Some text.

End of the file
Loading