mddoco 0.1.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.
- mddoco/__init__.py +1 -0
- mddoco/cli.py +92 -0
- mddoco/converter.py +46 -0
- mddoco/graph.py +117 -0
- mddoco/mermaid.py +20 -0
- mddoco/pdf.py +33 -0
- mddoco/renderer.py +33 -0
- mddoco/scanner.py +21 -0
- mddoco/themes/default.html +167 -0
- mddoco/themes/professional.html +230 -0
- mddoco/toc.py +15 -0
- mddoco/writer.py +9 -0
- mddoco-0.1.0.dist-info/METADATA +163 -0
- mddoco-0.1.0.dist-info/RECORD +18 -0
- mddoco-0.1.0.dist-info/WHEEL +5 -0
- mddoco-0.1.0.dist-info/entry_points.txt +2 -0
- mddoco-0.1.0.dist-info/licenses/LICENSE +21 -0
- mddoco-0.1.0.dist-info/top_level.txt +1 -0
mddoco/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
mddoco/cli.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mddoco.converter import convert_files
|
|
6
|
+
from mddoco.pdf import html_to_pdf
|
|
7
|
+
from mddoco.renderer import render_html
|
|
8
|
+
from mddoco.scanner import find_markdown_files
|
|
9
|
+
from mddoco.toc import combine_toc
|
|
10
|
+
from mddoco.writer import write_output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command()
|
|
14
|
+
@click.argument("input_path", type=click.Path(exists=True, path_type=Path))
|
|
15
|
+
@click.option(
|
|
16
|
+
"--output", "-o", "output_path",
|
|
17
|
+
default=".", show_default=True,
|
|
18
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
19
|
+
help="Directory to write the output file.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--format", "-f", "fmt",
|
|
23
|
+
default="html", show_default=True,
|
|
24
|
+
type=click.Choice(["html", "pdf"], case_sensitive=False),
|
|
25
|
+
help="Output format.",
|
|
26
|
+
)
|
|
27
|
+
@click.option("--title", "-t", default=None, help="Document title.")
|
|
28
|
+
@click.option(
|
|
29
|
+
"--theme",
|
|
30
|
+
default="default", show_default=True,
|
|
31
|
+
help="Theme name to use for rendering.",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--toc/--no-toc",
|
|
35
|
+
default=False, show_default=True,
|
|
36
|
+
help="Generate a table of contents.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--toc-depth",
|
|
40
|
+
default=3, show_default=True,
|
|
41
|
+
type=click.IntRange(1, 6),
|
|
42
|
+
help="Maximum heading depth included in the TOC.",
|
|
43
|
+
)
|
|
44
|
+
def main(
|
|
45
|
+
input_path: Path,
|
|
46
|
+
output_path: Path,
|
|
47
|
+
fmt: str,
|
|
48
|
+
title: str | None,
|
|
49
|
+
theme: str,
|
|
50
|
+
toc: bool,
|
|
51
|
+
toc_depth: int,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Convert markdown files in INPUT_PATH to a single document."""
|
|
54
|
+
try:
|
|
55
|
+
md_files = find_markdown_files(input_path)
|
|
56
|
+
except (ValueError, FileNotFoundError) as exc:
|
|
57
|
+
raise click.ClickException(str(exc)) from exc
|
|
58
|
+
|
|
59
|
+
click.echo(f"Found {len(md_files)} markdown file(s).")
|
|
60
|
+
|
|
61
|
+
raw = convert_files(md_files, toc=toc, toc_depth=toc_depth)
|
|
62
|
+
sections = [(p, html) for p, html, _ in raw]
|
|
63
|
+
toc_html = combine_toc([t for _, _, t in raw]) if toc else None
|
|
64
|
+
has_mermaid = any('<div class="mermaid">' in html for _, html in sections)
|
|
65
|
+
|
|
66
|
+
resolved = input_path.resolve()
|
|
67
|
+
stem = resolved.stem if resolved.is_file() else resolved.name
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
content = render_html(
|
|
71
|
+
sections,
|
|
72
|
+
title=title,
|
|
73
|
+
toc_html=toc_html or None,
|
|
74
|
+
theme=theme,
|
|
75
|
+
has_mermaid=has_mermaid,
|
|
76
|
+
)
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
raise click.ClickException(str(exc)) from exc
|
|
79
|
+
|
|
80
|
+
if fmt == "html":
|
|
81
|
+
output_file = write_output(content, output_path, f"{stem}.html")
|
|
82
|
+
click.echo(f"Written to {output_file}")
|
|
83
|
+
|
|
84
|
+
elif fmt == "pdf":
|
|
85
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
dest = output_path / f"{stem}.pdf"
|
|
87
|
+
click.echo("Rendering PDF...")
|
|
88
|
+
try:
|
|
89
|
+
html_to_pdf(content, dest)
|
|
90
|
+
except RuntimeError as exc:
|
|
91
|
+
raise click.ClickException(str(exc)) from exc
|
|
92
|
+
click.echo(f"Written to {dest}")
|
mddoco/converter.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import markdown
|
|
4
|
+
|
|
5
|
+
from mddoco.graph import process_graph
|
|
6
|
+
from mddoco.mermaid import process_mermaid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_file(
|
|
10
|
+
path: Path,
|
|
11
|
+
toc: bool = False,
|
|
12
|
+
toc_depth: int = 3,
|
|
13
|
+
) -> tuple[str, str]:
|
|
14
|
+
"""Convert a markdown file to an HTML fragment.
|
|
15
|
+
|
|
16
|
+
Returns (html_body, toc_html). toc_html is empty when toc=False.
|
|
17
|
+
"""
|
|
18
|
+
text = path.read_text(encoding="utf-8")
|
|
19
|
+
|
|
20
|
+
extensions = ["tables", "fenced_code", "attr_list"]
|
|
21
|
+
extension_configs: dict = {}
|
|
22
|
+
|
|
23
|
+
if toc:
|
|
24
|
+
extensions.append("toc")
|
|
25
|
+
extension_configs["toc"] = {"toc_depth": toc_depth}
|
|
26
|
+
|
|
27
|
+
md = markdown.Markdown(extensions=extensions, extension_configs=extension_configs)
|
|
28
|
+
html = md.convert(text)
|
|
29
|
+
html, _ = process_mermaid(html)
|
|
30
|
+
html, _ = process_graph(html)
|
|
31
|
+
|
|
32
|
+
toc_html: str = md.toc if toc else ""
|
|
33
|
+
return html, toc_html
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def convert_files(
|
|
37
|
+
paths: list[Path],
|
|
38
|
+
toc: bool = False,
|
|
39
|
+
toc_depth: int = 3,
|
|
40
|
+
) -> list[tuple[Path, str, str]]:
|
|
41
|
+
"""Convert markdown files, returning (path, html_body, toc_html) tuples."""
|
|
42
|
+
results = []
|
|
43
|
+
for p in paths:
|
|
44
|
+
html, toc_html = convert_file(p, toc=toc, toc_depth=toc_depth)
|
|
45
|
+
results.append((p, html, toc_html))
|
|
46
|
+
return results
|
mddoco/graph.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from html import escape, unescape
|
|
5
|
+
|
|
6
|
+
import matplotlib
|
|
7
|
+
matplotlib.use('Agg') # non-interactive backend — must be set before importing pyplot
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
|
|
10
|
+
_DEFAULT_COLOURS = [
|
|
11
|
+
'#2d6cbe', '#e74c3c', '#27ae60', '#e67e22',
|
|
12
|
+
'#8e44ad', '#16a085', '#2c3e50', '#d35400',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def graph_it(data, output=None):
|
|
17
|
+
dpi = 100
|
|
18
|
+
width_px = data.get('width_px', 640)
|
|
19
|
+
height_px = data.get('height_px', 480)
|
|
20
|
+
fig, ax = plt.subplots(figsize=(width_px / dpi, height_px / dpi), dpi=dpi)
|
|
21
|
+
|
|
22
|
+
orientation = data.get('orientation', 'vertical').lower()
|
|
23
|
+
if orientation not in ('horizontal', 'vertical'):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Invalid orientation '{orientation}' — must be 'horizontal' or 'vertical'"
|
|
26
|
+
)
|
|
27
|
+
horizontal = orientation == 'horizontal'
|
|
28
|
+
|
|
29
|
+
x = data['data']['x']
|
|
30
|
+
|
|
31
|
+
# Auto-generate series from data keys (everything except 'x') if not provided
|
|
32
|
+
series_list = data.get('series') or [
|
|
33
|
+
{'label': k} for k in data['data'] if k != 'x'
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for i, series in enumerate(series_list):
|
|
37
|
+
label = series['label']
|
|
38
|
+
values = data['data'][label]
|
|
39
|
+
series_type = series.get('type', 'line')
|
|
40
|
+
colour = series.get('colour', _DEFAULT_COLOURS[i % len(_DEFAULT_COLOURS)])
|
|
41
|
+
|
|
42
|
+
if series_type == 'bar':
|
|
43
|
+
colour_val = colour if isinstance(colour, list) else [colour] * len(values)
|
|
44
|
+
if horizontal:
|
|
45
|
+
ax.barh(x, values, color=colour_val, label=label)
|
|
46
|
+
else:
|
|
47
|
+
ax.bar(x, values, color=colour_val, label=label)
|
|
48
|
+
elif series_type in ('line', 'line2'):
|
|
49
|
+
marker = 'o' if series.get('marker', False) else 'None'
|
|
50
|
+
linestyle = ':' if series_type == 'line2' else '-'
|
|
51
|
+
if horizontal:
|
|
52
|
+
ax.plot(values, x, color=colour, marker=marker, linewidth=2, linestyle=linestyle, label=label)
|
|
53
|
+
else:
|
|
54
|
+
ax.plot(x, values, color=colour, marker=marker, linewidth=2, linestyle=linestyle, label=label)
|
|
55
|
+
|
|
56
|
+
if 'min' in data or 'max' in data:
|
|
57
|
+
lo = data.get('min', None)
|
|
58
|
+
hi = data.get('max', None)
|
|
59
|
+
if horizontal:
|
|
60
|
+
ax.set_xlim(lo, hi)
|
|
61
|
+
else:
|
|
62
|
+
ax.set_ylim(lo, hi)
|
|
63
|
+
|
|
64
|
+
title = data.get('title', '')
|
|
65
|
+
if title:
|
|
66
|
+
ax.set_title(title)
|
|
67
|
+
|
|
68
|
+
if data.get('show_legend', True):
|
|
69
|
+
ax.legend(loc='upper left')
|
|
70
|
+
|
|
71
|
+
ax.spines['top'].set_visible(False)
|
|
72
|
+
ax.spines['right'].set_visible(False)
|
|
73
|
+
|
|
74
|
+
if output:
|
|
75
|
+
plt.savefig(output, format="svg", bbox_inches='tight')
|
|
76
|
+
plt.close(fig)
|
|
77
|
+
else:
|
|
78
|
+
buf = io.StringIO()
|
|
79
|
+
plt.savefig(buf, format="svg", bbox_inches='tight')
|
|
80
|
+
plt.close(fig)
|
|
81
|
+
return buf.getvalue()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_GRAPH_BLOCK = re.compile(
|
|
85
|
+
r'<pre><code class="language-graph">(.*?)</code></pre>',
|
|
86
|
+
re.DOTALL,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
_SVG_HEADER = re.compile(r'^.*?(?=<svg)', re.DOTALL)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _render_graph(source: str) -> str:
|
|
93
|
+
"""Parse JSON source, render via graph_it, and return an inline SVG element."""
|
|
94
|
+
data = json.loads(unescape(source))
|
|
95
|
+
svg = graph_it(data)
|
|
96
|
+
svg = _SVG_HEADER.sub('', svg) # strip XML declaration and DOCTYPE
|
|
97
|
+
return f'<div class="graph">{svg}</div>'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def process_graph(html: str) -> tuple[str, bool]:
|
|
101
|
+
"""Replace fenced graph blocks with rendered SVG charts.
|
|
102
|
+
|
|
103
|
+
Falls back to an inline error message if the block cannot be parsed or rendered.
|
|
104
|
+
Returns (processed_html, found).
|
|
105
|
+
"""
|
|
106
|
+
found = bool(_GRAPH_BLOCK.search(html))
|
|
107
|
+
|
|
108
|
+
def replace(m: re.Match) -> str:
|
|
109
|
+
try:
|
|
110
|
+
return _render_graph(m.group(1))
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
return (
|
|
113
|
+
f'<p class="graph-error"><strong>Graph error:</strong> '
|
|
114
|
+
f'{escape(str(exc))}</p>'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return _GRAPH_BLOCK.sub(replace, html), found
|
mddoco/mermaid.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
_MERMAID_BLOCK = re.compile(
|
|
4
|
+
r'<pre><code class="language-mermaid">(.*?)</code></pre>',
|
|
5
|
+
re.DOTALL,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def process_mermaid(html: str) -> tuple[str, bool]:
|
|
10
|
+
"""Replace fenced mermaid blocks with Mermaid.js div elements.
|
|
11
|
+
|
|
12
|
+
Returns (processed_html, found) where found indicates whether any
|
|
13
|
+
mermaid blocks were present.
|
|
14
|
+
"""
|
|
15
|
+
found = bool(_MERMAID_BLOCK.search(html))
|
|
16
|
+
processed = _MERMAID_BLOCK.sub(
|
|
17
|
+
lambda m: f'<div class="mermaid">{m.group(1)}</div>',
|
|
18
|
+
html,
|
|
19
|
+
)
|
|
20
|
+
return processed, found
|
mddoco/pdf.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def html_to_pdf(html_content: str, dest: Path) -> None:
|
|
6
|
+
"""Render an HTML string to a PDF file using a headless Chromium browser.
|
|
7
|
+
|
|
8
|
+
Navigates via a temporary file:// URL so that external resources (CDN
|
|
9
|
+
scripts, Mermaid.js, etc.) load correctly before the page is printed.
|
|
10
|
+
"""
|
|
11
|
+
try:
|
|
12
|
+
from playwright.sync_api import sync_playwright
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
"Playwright is required for PDF output. "
|
|
16
|
+
"Run: pip install playwright && playwright install chromium"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
with tempfile.NamedTemporaryFile(
|
|
20
|
+
suffix=".html", delete=False, mode="w", encoding="utf-8"
|
|
21
|
+
) as f:
|
|
22
|
+
f.write(html_content)
|
|
23
|
+
tmp_path = Path(f.name)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with sync_playwright() as p:
|
|
27
|
+
browser = p.chromium.launch()
|
|
28
|
+
page = browser.new_page()
|
|
29
|
+
page.goto(tmp_path.as_uri(), wait_until="networkidle")
|
|
30
|
+
page.pdf(path=str(dest), format="A4", print_background=True)
|
|
31
|
+
browser.close()
|
|
32
|
+
finally:
|
|
33
|
+
tmp_path.unlink(missing_ok=True)
|
mddoco/renderer.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _make_env() -> Environment:
|
|
8
|
+
themes_dir = files("mddoco").joinpath("themes")
|
|
9
|
+
return Environment(
|
|
10
|
+
loader=FileSystemLoader(str(themes_dir)),
|
|
11
|
+
autoescape=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def render_html(
|
|
16
|
+
sections: list[tuple[Path, str]],
|
|
17
|
+
title: str | None = None,
|
|
18
|
+
toc_html: str | None = None,
|
|
19
|
+
theme: str = "default",
|
|
20
|
+
has_mermaid: bool = False,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Render a full HTML document from (path, html_fragment) pairs."""
|
|
23
|
+
env = _make_env()
|
|
24
|
+
try:
|
|
25
|
+
template = env.get_template(f"{theme}.html")
|
|
26
|
+
except TemplateNotFound:
|
|
27
|
+
raise ValueError(f"Theme '{theme}' not found.")
|
|
28
|
+
return template.render(
|
|
29
|
+
title=title,
|
|
30
|
+
toc_html=toc_html,
|
|
31
|
+
sections=sections,
|
|
32
|
+
has_mermaid=has_mermaid,
|
|
33
|
+
)
|
mddoco/scanner.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def find_markdown_files(input_path: Path) -> list[Path]:
|
|
5
|
+
"""Return markdown files to process.
|
|
6
|
+
|
|
7
|
+
If input_path is a file, return it directly (must be a .md file).
|
|
8
|
+
If input_path is a directory, return all *.md files sorted by relative path.
|
|
9
|
+
"""
|
|
10
|
+
if input_path.is_file():
|
|
11
|
+
if input_path.suffix.lower() != ".md":
|
|
12
|
+
raise ValueError(f"File is not a markdown file: {input_path}")
|
|
13
|
+
return [input_path]
|
|
14
|
+
|
|
15
|
+
if input_path.is_dir():
|
|
16
|
+
files = sorted(input_path.rglob("*.md"))
|
|
17
|
+
if not files:
|
|
18
|
+
raise FileNotFoundError(f"No markdown files found in: {input_path}")
|
|
19
|
+
return files
|
|
20
|
+
|
|
21
|
+
raise ValueError(f"Input path does not exist: {input_path}")
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% if title %}{{ title }}{% else %}Document{% endif %}</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
|
|
12
|
+
font-size: 16px;
|
|
13
|
+
line-height: 1.7;
|
|
14
|
+
color: #24292e;
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 2rem 1rem;
|
|
17
|
+
background: #fff;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.container {
|
|
21
|
+
max-width: 860px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
h1, h2, h3, h4, h5, h6 {
|
|
26
|
+
margin-top: 1.5em;
|
|
27
|
+
margin-bottom: 0.5em;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
line-height: 1.25;
|
|
30
|
+
}
|
|
31
|
+
h1 { font-size: 2em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
|
|
32
|
+
h2 { font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 0.3em; }
|
|
33
|
+
h3 { font-size: 1.25em; }
|
|
34
|
+
h4 { font-size: 1em; }
|
|
35
|
+
|
|
36
|
+
.doc-title {
|
|
37
|
+
font-size: 2.5em;
|
|
38
|
+
margin-top: 0;
|
|
39
|
+
border-bottom: 2px solid #24292e;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
a { color: #0366d6; text-decoration: none; }
|
|
43
|
+
a:hover { text-decoration: underline; }
|
|
44
|
+
|
|
45
|
+
p { margin: 0.75em 0; }
|
|
46
|
+
|
|
47
|
+
code {
|
|
48
|
+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
49
|
+
font-size: 0.875em;
|
|
50
|
+
background: #f6f8fa;
|
|
51
|
+
padding: 0.2em 0.4em;
|
|
52
|
+
border-radius: 3px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pre {
|
|
56
|
+
background: #f6f8fa;
|
|
57
|
+
border: 1px solid #eaecef;
|
|
58
|
+
border-radius: 6px;
|
|
59
|
+
padding: 1rem;
|
|
60
|
+
overflow-x: auto;
|
|
61
|
+
line-height: 1.45;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pre code {
|
|
65
|
+
background: none;
|
|
66
|
+
padding: 0;
|
|
67
|
+
border-radius: 0;
|
|
68
|
+
font-size: 0.875em;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
table {
|
|
72
|
+
border-collapse: collapse;
|
|
73
|
+
width: 100%;
|
|
74
|
+
margin: 1em 0;
|
|
75
|
+
outline: 1px solid #c8ccd0;
|
|
76
|
+
}
|
|
77
|
+
th, td {
|
|
78
|
+
border: 1px solid #c8ccd0;
|
|
79
|
+
padding: 0.5em 1em;
|
|
80
|
+
text-align: left;
|
|
81
|
+
}
|
|
82
|
+
th { background: #f0f3f6; font-weight: 600; }
|
|
83
|
+
tr:nth-child(even) td { background: #fafbfc; }
|
|
84
|
+
|
|
85
|
+
blockquote {
|
|
86
|
+
margin: 1em 0;
|
|
87
|
+
padding: 0.5em 1em;
|
|
88
|
+
border-left: 4px solid #dfe2e5;
|
|
89
|
+
color: #6a737d;
|
|
90
|
+
}
|
|
91
|
+
blockquote p { margin: 0; }
|
|
92
|
+
|
|
93
|
+
img { max-width: 100%; height: auto; }
|
|
94
|
+
|
|
95
|
+
ul, ol { padding-left: 1.5em; margin: 0.75em 0; }
|
|
96
|
+
li { margin: 0.25em 0; }
|
|
97
|
+
|
|
98
|
+
hr, .section-divider {
|
|
99
|
+
border: none;
|
|
100
|
+
border-top: 1px solid #eaecef;
|
|
101
|
+
margin: 3rem 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Table of Contents */
|
|
105
|
+
.toc-block {
|
|
106
|
+
background: #f6f8fa;
|
|
107
|
+
border: 1px solid #eaecef;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
padding: 1rem 1.5rem;
|
|
110
|
+
margin: 1.5rem 0 2.5rem;
|
|
111
|
+
display: inline-block;
|
|
112
|
+
min-width: 260px;
|
|
113
|
+
}
|
|
114
|
+
.toc-block .toc-heading {
|
|
115
|
+
font-size: 0.75em;
|
|
116
|
+
font-weight: 700;
|
|
117
|
+
text-transform: uppercase;
|
|
118
|
+
letter-spacing: 0.08em;
|
|
119
|
+
color: #6a737d;
|
|
120
|
+
margin: 0 0 0.5rem;
|
|
121
|
+
}
|
|
122
|
+
.toc-block ul {
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding-left: 1.25em;
|
|
125
|
+
list-style: none;
|
|
126
|
+
}
|
|
127
|
+
.toc-block ul ul { padding-left: 1em; }
|
|
128
|
+
.toc-block li { margin: 0.2em 0; }
|
|
129
|
+
.toc-block a { color: #0366d6; font-size: 0.9em; }
|
|
130
|
+
|
|
131
|
+
/* Mermaid diagrams */
|
|
132
|
+
.mermaid {
|
|
133
|
+
text-align: center;
|
|
134
|
+
margin: 1.5em 0;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
137
|
+
{% if has_mermaid %}
|
|
138
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
139
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script>
|
|
140
|
+
{% endif %}
|
|
141
|
+
</head>
|
|
142
|
+
<body>
|
|
143
|
+
<div class="container">
|
|
144
|
+
|
|
145
|
+
{% if title %}
|
|
146
|
+
<h1 class="doc-title">{{ title }}</h1>
|
|
147
|
+
{% endif %}
|
|
148
|
+
|
|
149
|
+
{% if toc_html %}
|
|
150
|
+
<nav class="toc-block" aria-label="Table of contents">
|
|
151
|
+
<p class="toc-heading">Contents</p>
|
|
152
|
+
{{ toc_html | safe }}
|
|
153
|
+
</nav>
|
|
154
|
+
{% endif %}
|
|
155
|
+
|
|
156
|
+
<main>
|
|
157
|
+
{% for path, body in sections %}
|
|
158
|
+
{% if not loop.first %}
|
|
159
|
+
<hr class="section-divider">
|
|
160
|
+
{% endif %}
|
|
161
|
+
<section>{{ body | safe }}</section>
|
|
162
|
+
{% endfor %}
|
|
163
|
+
</main>
|
|
164
|
+
|
|
165
|
+
</div>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{% if title %}{{ title }}{% else %}Document{% endif %}</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--primary: #1a3a5c;
|
|
12
|
+
--accent: #2d6cbe;
|
|
13
|
+
--accent-light: #e8f0fb;
|
|
14
|
+
--border: #b0bec5;
|
|
15
|
+
--muted: #546e7a;
|
|
16
|
+
--bg: #ffffff;
|
|
17
|
+
--bg-alt: #f4f6f8;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
22
|
+
font-size: 15px;
|
|
23
|
+
line-height: 1.75;
|
|
24
|
+
color: #263238;
|
|
25
|
+
margin: 0;
|
|
26
|
+
padding: 0;
|
|
27
|
+
background: var(--bg-alt);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.page-wrap {
|
|
31
|
+
max-width: 900px;
|
|
32
|
+
margin: 2rem auto;
|
|
33
|
+
background: var(--bg);
|
|
34
|
+
border: 1px solid var(--border);
|
|
35
|
+
border-radius: 4px;
|
|
36
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── Title banner ─────────────────────────────────────────── */
|
|
40
|
+
.doc-header {
|
|
41
|
+
background: var(--primary);
|
|
42
|
+
color: #fff;
|
|
43
|
+
padding: 2.5rem 3rem 2rem;
|
|
44
|
+
border-radius: 4px 4px 0 0;
|
|
45
|
+
}
|
|
46
|
+
.doc-header .doc-title {
|
|
47
|
+
font-size: 2rem;
|
|
48
|
+
font-weight: 700;
|
|
49
|
+
letter-spacing: 0.02em;
|
|
50
|
+
margin: 0;
|
|
51
|
+
color: #fff;
|
|
52
|
+
border: none;
|
|
53
|
+
}
|
|
54
|
+
.doc-header .doc-subtitle {
|
|
55
|
+
margin: 0.4rem 0 0;
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
opacity: 0.7;
|
|
58
|
+
letter-spacing: 0.03em;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.content {
|
|
62
|
+
padding: 2rem 3rem 3rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ── Headings ──────────────────────────────────────────────── */
|
|
66
|
+
h1, h2, h3, h4, h5, h6 {
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
line-height: 1.25;
|
|
69
|
+
margin-top: 2em;
|
|
70
|
+
margin-bottom: 0.5em;
|
|
71
|
+
}
|
|
72
|
+
h1 {
|
|
73
|
+
font-size: 1.75em;
|
|
74
|
+
color: var(--primary);
|
|
75
|
+
border-bottom: 3px solid var(--accent);
|
|
76
|
+
padding-bottom: 0.35em;
|
|
77
|
+
}
|
|
78
|
+
h2 {
|
|
79
|
+
font-size: 1.35em;
|
|
80
|
+
color: var(--accent);
|
|
81
|
+
border-bottom: 1px solid var(--border);
|
|
82
|
+
padding-bottom: 0.25em;
|
|
83
|
+
}
|
|
84
|
+
h3 { font-size: 1.1em; color: var(--primary); }
|
|
85
|
+
h4 { font-size: 1em; color: var(--muted); }
|
|
86
|
+
|
|
87
|
+
a { color: var(--accent); text-decoration: none; }
|
|
88
|
+
a:hover { text-decoration: underline; }
|
|
89
|
+
|
|
90
|
+
p { margin: 0.75em 0; }
|
|
91
|
+
|
|
92
|
+
/* ── Code ──────────────────────────────────────────────────── */
|
|
93
|
+
code {
|
|
94
|
+
font-family: 'Cascadia Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
95
|
+
font-size: 0.85em;
|
|
96
|
+
background: var(--bg-alt);
|
|
97
|
+
border: 1px solid var(--border);
|
|
98
|
+
padding: 0.15em 0.4em;
|
|
99
|
+
border-radius: 3px;
|
|
100
|
+
}
|
|
101
|
+
pre {
|
|
102
|
+
background: #1e2a38;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
padding: 1.25rem;
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
line-height: 1.5;
|
|
107
|
+
}
|
|
108
|
+
pre code {
|
|
109
|
+
background: none;
|
|
110
|
+
border: none;
|
|
111
|
+
padding: 0;
|
|
112
|
+
font-size: 0.875em;
|
|
113
|
+
color: #cdd9e5;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ── Tables ────────────────────────────────────────────────── */
|
|
117
|
+
table {
|
|
118
|
+
border-collapse: collapse;
|
|
119
|
+
width: 100%;
|
|
120
|
+
margin: 1.25em 0;
|
|
121
|
+
outline: 1px solid var(--border);
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
overflow: hidden;
|
|
124
|
+
}
|
|
125
|
+
th {
|
|
126
|
+
background: var(--primary);
|
|
127
|
+
color: #fff;
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
padding: 0.6em 1em;
|
|
130
|
+
text-align: left;
|
|
131
|
+
border: 1px solid #2a4f78;
|
|
132
|
+
}
|
|
133
|
+
td {
|
|
134
|
+
padding: 0.55em 1em;
|
|
135
|
+
border: 1px solid var(--border);
|
|
136
|
+
}
|
|
137
|
+
tr:nth-child(even) td { background: var(--accent-light); }
|
|
138
|
+
tr:hover td { background: #dde8f7; }
|
|
139
|
+
|
|
140
|
+
/* ── Blockquote ────────────────────────────────────────────── */
|
|
141
|
+
blockquote {
|
|
142
|
+
margin: 1em 0;
|
|
143
|
+
padding: 0.75em 1.25em;
|
|
144
|
+
border-left: 4px solid var(--accent);
|
|
145
|
+
background: var(--accent-light);
|
|
146
|
+
color: var(--muted);
|
|
147
|
+
border-radius: 0 4px 4px 0;
|
|
148
|
+
}
|
|
149
|
+
blockquote p { margin: 0; }
|
|
150
|
+
|
|
151
|
+
img { max-width: 100%; height: auto; }
|
|
152
|
+
|
|
153
|
+
ul, ol { padding-left: 1.5em; margin: 0.75em 0; }
|
|
154
|
+
li { margin: 0.3em 0; }
|
|
155
|
+
|
|
156
|
+
hr, .section-divider {
|
|
157
|
+
border: none;
|
|
158
|
+
border-top: 1px solid var(--border);
|
|
159
|
+
margin: 2.5rem 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ── Table of Contents ─────────────────────────────────────── */
|
|
163
|
+
.toc-block {
|
|
164
|
+
background: var(--accent-light);
|
|
165
|
+
border: 1px solid #b3c8ee;
|
|
166
|
+
border-left: 4px solid var(--accent);
|
|
167
|
+
border-radius: 0 4px 4px 0;
|
|
168
|
+
padding: 1rem 1.5rem;
|
|
169
|
+
margin: 1.5rem 0 2.5rem;
|
|
170
|
+
display: inline-block;
|
|
171
|
+
min-width: 280px;
|
|
172
|
+
}
|
|
173
|
+
.toc-block .toc-heading {
|
|
174
|
+
font-size: 0.7em;
|
|
175
|
+
font-weight: 700;
|
|
176
|
+
text-transform: uppercase;
|
|
177
|
+
letter-spacing: 0.1em;
|
|
178
|
+
color: var(--accent);
|
|
179
|
+
margin: 0 0 0.5rem;
|
|
180
|
+
}
|
|
181
|
+
.toc-block ul {
|
|
182
|
+
margin: 0;
|
|
183
|
+
padding-left: 1.25em;
|
|
184
|
+
list-style: none;
|
|
185
|
+
}
|
|
186
|
+
.toc-block ul ul { padding-left: 1em; }
|
|
187
|
+
.toc-block li { margin: 0.2em 0; }
|
|
188
|
+
.toc-block a { color: var(--primary); font-size: 0.9em; }
|
|
189
|
+
.toc-block a:hover { color: var(--accent); }
|
|
190
|
+
|
|
191
|
+
/* ── Mermaid ───────────────────────────────────────────────── */
|
|
192
|
+
.mermaid { text-align: center; margin: 1.5em 0; }
|
|
193
|
+
</style>
|
|
194
|
+
{% if has_mermaid %}
|
|
195
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
196
|
+
<script>mermaid.initialize({ startOnLoad: true, theme: 'neutral' });</script>
|
|
197
|
+
{% endif %}
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<div class="page-wrap">
|
|
201
|
+
|
|
202
|
+
{% if title %}
|
|
203
|
+
<header class="doc-header">
|
|
204
|
+
<h1 class="doc-title">{{ title }}</h1>
|
|
205
|
+
<p class="doc-subtitle">Generated by mddoco</p>
|
|
206
|
+
</header>
|
|
207
|
+
{% endif %}
|
|
208
|
+
|
|
209
|
+
<div class="content">
|
|
210
|
+
|
|
211
|
+
{% if toc_html %}
|
|
212
|
+
<nav class="toc-block" aria-label="Table of contents">
|
|
213
|
+
<p class="toc-heading">Contents</p>
|
|
214
|
+
{{ toc_html | safe }}
|
|
215
|
+
</nav>
|
|
216
|
+
{% endif %}
|
|
217
|
+
|
|
218
|
+
<main>
|
|
219
|
+
{% for path, body in sections %}
|
|
220
|
+
{% if not loop.first %}
|
|
221
|
+
<hr class="section-divider">
|
|
222
|
+
{% endif %}
|
|
223
|
+
<section>{{ body | safe }}</section>
|
|
224
|
+
{% endfor %}
|
|
225
|
+
</main>
|
|
226
|
+
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</body>
|
|
230
|
+
</html>
|
mddoco/toc.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
_INNER_UL = re.compile(r"<ul>(.*)</ul>", re.DOTALL)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def combine_toc(fragments: list[str]) -> str:
|
|
7
|
+
"""Merge per-file TOC HTML fragments into a single <ul> block."""
|
|
8
|
+
items: list[str] = []
|
|
9
|
+
for fragment in fragments:
|
|
10
|
+
m = _INNER_UL.search(fragment)
|
|
11
|
+
if m:
|
|
12
|
+
items.append(m.group(1).strip())
|
|
13
|
+
if not items:
|
|
14
|
+
return ""
|
|
15
|
+
return "<ul>\n" + "\n".join(items) + "\n</ul>"
|
mddoco/writer.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def write_output(content: str, output_path: Path, filename: str) -> Path:
|
|
5
|
+
"""Write content to output_path/filename, creating directories as needed."""
|
|
6
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
7
|
+
dest = output_path / filename
|
|
8
|
+
dest.write_text(content, encoding="utf-8")
|
|
9
|
+
return dest
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mddoco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markdown to HTML/PDF document converter
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Requires-Dist: markdown>=3.5
|
|
10
|
+
Requires-Dist: jinja2>=3.1
|
|
11
|
+
Requires-Dist: playwright>=1.40
|
|
12
|
+
Requires-Dist: matplotlib>=3.7
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# mddoco
|
|
16
|
+
|
|
17
|
+
A CLI tool that converts markdown files to a single HTML (or future PDF) document.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install mddoco
|
|
23
|
+
playwright install chromium
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or for development:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e .
|
|
30
|
+
playwright install chromium
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> Playwright (Chromium) is required for PDF output only. HTML output works without it.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
mddoco [OPTIONS] INPUT_PATH
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`INPUT_PATH` can be a **directory** (all `*.md` files are found recursively and sorted) or a **single `.md` file**.
|
|
42
|
+
|
|
43
|
+
### Options
|
|
44
|
+
|
|
45
|
+
| Option | Short | Default | Description |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `--output PATH` | `-o` | `.` | Directory to write the output file |
|
|
48
|
+
| `--format [html\|pdf]` | `-f` | `html` | Output format |
|
|
49
|
+
| `--title TEXT` | `-t` | _(none)_ | Document title shown at the top of the page |
|
|
50
|
+
| `--theme NAME` | | `default` | Theme to use for rendering |
|
|
51
|
+
| `--toc / --no-toc` | | off | Generate a table of contents |
|
|
52
|
+
| `--toc-depth N` | | `3` | Maximum heading depth included in the TOC (1–6) |
|
|
53
|
+
|
|
54
|
+
### Examples
|
|
55
|
+
|
|
56
|
+
Convert all markdown files in a directory:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mddoco ./docs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Convert a single file:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mddoco README.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With a title, TOC, and custom output directory:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mddoco ./docs --title "My Project" --toc --output ./out
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
All matched markdown files are combined into a single HTML document in the output directory. The filename is derived from the input folder or file name.
|
|
77
|
+
|
|
78
|
+
## Themes
|
|
79
|
+
|
|
80
|
+
Themes are self-contained Jinja2 HTML files with embedded CSS. The `default` theme includes:
|
|
81
|
+
|
|
82
|
+
- Clean typographic layout
|
|
83
|
+
- Syntax-highlighted code blocks
|
|
84
|
+
- Styled tables and blockquotes
|
|
85
|
+
- Optional document title
|
|
86
|
+
- Optional table of contents
|
|
87
|
+
- [Mermaid.js](https://mermaid.js.org) diagram support (loaded only when diagrams are present)
|
|
88
|
+
|
|
89
|
+
### Graph diagrams
|
|
90
|
+
|
|
91
|
+
Use ` ```graph ` fenced blocks with a JSON payload. The only required field is `data`.
|
|
92
|
+
|
|
93
|
+
**Minimal example** — all defaults applied:
|
|
94
|
+
|
|
95
|
+
````markdown
|
|
96
|
+
```graph
|
|
97
|
+
{
|
|
98
|
+
"data": {
|
|
99
|
+
"x": ["Jan", "Feb", "Mar"],
|
|
100
|
+
"Sales": [100, 150, 120]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
````
|
|
105
|
+
|
|
106
|
+
Series are inferred from the data keys (everything except `x`). Each series defaults to a line chart in blue.
|
|
107
|
+
|
|
108
|
+
**Full schema:**
|
|
109
|
+
|
|
110
|
+
| Field | Required | Default | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `data.x` | yes | — | Category labels |
|
|
113
|
+
| `data.<name>` | yes | — | Values for each series (one key per series) |
|
|
114
|
+
| `title` | no | _(none)_ | Chart title |
|
|
115
|
+
| `orientation` | no | `vertical` | `vertical` or `horizontal` |
|
|
116
|
+
| `show_legend` | no | `true` | Show the legend |
|
|
117
|
+
| `width_px` | no | `640` | Width in pixels |
|
|
118
|
+
| `height_px` | no | `480` | Height in pixels |
|
|
119
|
+
| `min` / `max` | no | — | Axis bounds |
|
|
120
|
+
| `series` | no | _(auto)_ | Override series definitions (see below) |
|
|
121
|
+
|
|
122
|
+
**Series fields** (all optional when auto-generated):
|
|
123
|
+
|
|
124
|
+
| Field | Default | Description |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `label` | _(data key)_ | Must match a key in `data` |
|
|
127
|
+
| `type` | `line` | `bar`, `line` (solid), or `line2` (dotted) |
|
|
128
|
+
| `colour` | _(palette)_ | Colour string, or list of colours per bar |
|
|
129
|
+
| `marker` | `false` | Show point markers (line types only) |
|
|
130
|
+
|
|
131
|
+
**Full example:**
|
|
132
|
+
|
|
133
|
+
````markdown
|
|
134
|
+
```graph
|
|
135
|
+
{
|
|
136
|
+
"title": "Sales vs Target",
|
|
137
|
+
"orientation": "vertical",
|
|
138
|
+
"show_legend": true,
|
|
139
|
+
"data": {
|
|
140
|
+
"x": ["Jan", "Feb", "Mar", "Apr", "May"],
|
|
141
|
+
"Sales": [85, 92, 78, 96, 110],
|
|
142
|
+
"Target": [90, 90, 90, 90, 90]
|
|
143
|
+
},
|
|
144
|
+
"series": [
|
|
145
|
+
{ "label": "Sales", "type": "bar", "colour": "#3498db" },
|
|
146
|
+
{ "label": "Target", "type": "line", "colour": "#e74c3c", "marker": false }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
````
|
|
151
|
+
|
|
152
|
+
### Mermaid diagrams
|
|
153
|
+
|
|
154
|
+
Use standard fenced code blocks in your markdown:
|
|
155
|
+
|
|
156
|
+
````markdown
|
|
157
|
+
```mermaid
|
|
158
|
+
graph TD
|
|
159
|
+
A[Start] --> B[End]
|
|
160
|
+
```
|
|
161
|
+
````
|
|
162
|
+
|
|
163
|
+
mddoco detects mermaid blocks automatically and loads the Mermaid.js CDN only when needed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mddoco/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
mddoco/cli.py,sha256=dzlJd3EbaMlf2SZFhA_LTfs0Z7ZcPlnC0L89V7iUgfA,2790
|
|
3
|
+
mddoco/converter.py,sha256=_82oF32T-uc9AazSj1DIs_8d-SNR4476Jcan_3l8uTA,1231
|
|
4
|
+
mddoco/graph.py,sha256=x9VpO2-bajmzQxqJkmIgl2M8u8dC_neoJ4dKfdaWRo8,3814
|
|
5
|
+
mddoco/mermaid.py,sha256=bWq3nY19PhkSS2_3xR3Md34tsIk_rZ7mi6kKyDxHthg,545
|
|
6
|
+
mddoco/pdf.py,sha256=a-O1Xbd35xmCCF4_YJs5yzPdt-KgZgjlWUSWQq4a2XY,1115
|
|
7
|
+
mddoco/renderer.py,sha256=l3kt4aKQWeXOD-kAr388K7rqGUacaQk9HPmstMsjWx0,894
|
|
8
|
+
mddoco/scanner.py,sha256=EX3RTMQGZrAb6xOlVQvdjaA9-xErRntf7zFg5mvLyPk,732
|
|
9
|
+
mddoco/toc.py,sha256=4h3lSjPmWvdwWeRgX6-nCikenF0QVayZ8Aqd_0pbYUA,423
|
|
10
|
+
mddoco/writer.py,sha256=KnTaFm3BI-Skx-UDZmUJNV7JI9f1Pq8fwOOoAyDvKp0,330
|
|
11
|
+
mddoco/themes/default.html,sha256=ePIOAIm7A7zwCSeRM4qomLQRlk7ie4uLPojHQN0ywlQ,4641
|
|
12
|
+
mddoco/themes/professional.html,sha256=8cwh-3L3bcBIRmjZ57Iw8FwI3X8UUKcH4_0J0C110NI,7661
|
|
13
|
+
mddoco-0.1.0.dist-info/licenses/LICENSE,sha256=JTTt6kL0A6OVPRn5wc8igU30mo5Wi2wZ2wai5dcY3H0,1068
|
|
14
|
+
mddoco-0.1.0.dist-info/METADATA,sha256=4KAzsXwFmSDFJv4IMfjEzM_I4hWkd1HCOlE9K4ZUz54,4084
|
|
15
|
+
mddoco-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
mddoco-0.1.0.dist-info/entry_points.txt,sha256=PbJ-QZBElwOzXK1NpkO07SdArunbFgyPycv4_UxPAko,43
|
|
17
|
+
mddoco-0.1.0.dist-info/top_level.txt,sha256=Eg5Eig5yjjCYwuJ1D3NnrHnnMBJo83ryWd_tPVNtbOo,7
|
|
18
|
+
mddoco-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Phil Massyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mddoco
|