mkdocs2confluence 0.9.4__tar.gz → 0.9.6__tar.gz
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.
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/PKG-INFO +6 -1
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/README.md +5 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/pyproject.toml +1 -1
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/PKG-INFO +6 -1
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/mermaid.py +58 -3
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_mermaid.py +108 -4
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/LICENSE +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/setup.cfg +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/cli.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/emitter/xhtml.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/ir/document.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/config.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/nav.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/page.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/parser/markdown.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/pdf/render.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/render.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/server.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/client.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/command.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/comments.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/github.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/platform.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/state.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/images.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_abbrevs.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_children_macro.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_cli.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_editlink.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_emitter.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_extra_css.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_footer.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_frontmatter.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_icons.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_images.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_internallinks.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_ir.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_linkdefs.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_loader.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_page_loader.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_parser.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_pdf.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_preprocess.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_preview.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_publish_client.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_publish_config.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_publish_pipeline.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_server.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_sync_anchoring.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_sync_command.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_sync_comments.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_sync_github.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_sync_state.py +0 -0
- {mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/tests/test_treeutil.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
|
|
5
5
|
Author: Anders Hybertz
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -245,6 +245,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
|
|
|
245
245
|
|
|
246
246
|
If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
|
|
247
247
|
|
|
248
|
+
**Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
|
|
249
|
+
|
|
248
250
|
#### Styling from extra.css
|
|
249
251
|
|
|
250
252
|
If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
|
|
@@ -368,6 +370,7 @@ confluence:
|
|
|
368
370
|
| `^^inserted^^` | `<u>` (pymdownx.caret insert) |
|
|
369
371
|
| `` `inline code` `` | `<code>` |
|
|
370
372
|
| `[text](url)` | `<a href="...">` |
|
|
373
|
+
| `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
|
|
371
374
|
| `https://bare-url` | `<a href="...">` (autolink) |
|
|
372
375
|
| `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
|
|
373
376
|
| `` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
|
|
@@ -433,6 +436,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
|
|
|
433
436
|
- **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
|
|
434
437
|
- **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
|
|
435
438
|
|
|
439
|
+
The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
|
|
440
|
+
|
|
436
441
|
### Section index child pages
|
|
437
442
|
|
|
438
443
|
When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
|
|
@@ -205,6 +205,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
|
|
|
205
205
|
|
|
206
206
|
If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
|
|
207
207
|
|
|
208
|
+
**Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
|
|
209
|
+
|
|
208
210
|
#### Styling from extra.css
|
|
209
211
|
|
|
210
212
|
If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
|
|
@@ -328,6 +330,7 @@ confluence:
|
|
|
328
330
|
| `^^inserted^^` | `<u>` (pymdownx.caret insert) |
|
|
329
331
|
| `` `inline code` `` | `<code>` |
|
|
330
332
|
| `[text](url)` | `<a href="...">` |
|
|
333
|
+
| `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
|
|
331
334
|
| `https://bare-url` | `<a href="...">` (autolink) |
|
|
332
335
|
| `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
|
|
333
336
|
| `` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
|
|
@@ -393,6 +396,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
|
|
|
393
396
|
- **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
|
|
394
397
|
- **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
|
|
395
398
|
|
|
399
|
+
The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
|
|
400
|
+
|
|
396
401
|
### Section index child pages
|
|
397
402
|
|
|
398
403
|
When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mkdocs2confluence"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.6"
|
|
4
4
|
description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "GPL-3.0-or-later" }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs2confluence
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
|
|
5
5
|
Author: Anders Hybertz
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -245,6 +245,8 @@ mk2conf publish [--config PATH] [--page PATH] [--section SECTION] [--dry-run] [-
|
|
|
245
245
|
|
|
246
246
|
If Kroki is unreachable the run continues, falling back to the `code` macro for affected diagrams.
|
|
247
247
|
|
|
248
|
+
**Automatic mermaid.ink fallback:** when using the public `kroki.io` service and a Mermaid diagram receives a 504 (gateway timeout), mk2conf automatically retries that diagram via [mermaid.ink](https://mermaid.ink) before giving up. No configuration needed — the fallback is transparent and only applies to Mermaid diagrams on the public service. Self-hosted Kroki instances never contact mermaid.ink, preserving network isolation.
|
|
249
|
+
|
|
248
250
|
#### Styling from extra.css
|
|
249
251
|
|
|
250
252
|
If `mkdocs.yml` lists `extra_css:` files, mk2conf reads them and applies a whitelisted set of CSS properties as inline `style="..."` attributes on Confluence output.
|
|
@@ -368,6 +370,7 @@ confluence:
|
|
|
368
370
|
| `^^inserted^^` | `<u>` (pymdownx.caret insert) |
|
|
369
371
|
| `` `inline code` `` | `<code>` |
|
|
370
372
|
| `[text](url)` | `<a href="...">` |
|
|
373
|
+
| `[text][label]` / `[text][]` with `[label]: url` | Resolved to inline link before parsing |
|
|
371
374
|
| `https://bare-url` | `<a href="...">` (autolink) |
|
|
372
375
|
| `[text](file.pdf)` | `<ac:link><ri:attachment .../>` (uploaded as attachment) |
|
|
373
376
|
| `` | `<ac:image>` with `<ri:attachment>` (local) or `<ri:url>` (remote) |
|
|
@@ -433,6 +436,8 @@ When `repo_url` + `edit_uri` are set in `mkdocs.yml`, a **Page source** footer p
|
|
|
433
436
|
- **View history** — links to the file's commit history (derived automatically for GitHub and GitLab URLs)
|
|
434
437
|
- **Last commit** — short commit SHA (linked to that commit), message, author, and relative date from `git log` at publish time (omitted if git is unavailable or the file is untracked)
|
|
435
438
|
|
|
439
|
+
The commit SHA and message are also written to the **Confluence version history** on every publish (`sha: message`), so the page history in Confluence stays in sync with your git log.
|
|
440
|
+
|
|
436
441
|
### Section index child pages
|
|
437
442
|
|
|
438
443
|
When a MkDocs navigation section has an `index.md`, the published Confluence page automatically includes the native **Children Display macro** below the page content. This renders a live, auto-maintained list of all direct child pages — no manual curation needed. The macro is injected by default on every section index page.
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/mermaid.py
RENAMED
|
@@ -10,17 +10,28 @@ of the Mermaid source so unchanged diagrams are never re-fetched.
|
|
|
10
10
|
When Kroki is unavailable (network error, HTTP error, timeout, or bad response)
|
|
11
11
|
each affected diagram is left unchanged so the emitter falls back to a fenced
|
|
12
12
|
code block. The rest of the pipeline continues unaffected.
|
|
13
|
+
|
|
14
|
+
Fallback behaviour
|
|
15
|
+
------------------
|
|
16
|
+
When the public ``https://kroki.io`` service returns a 504 (gateway timeout)
|
|
17
|
+
for a Mermaid diagram, mk2conf automatically retries via ``mermaid.ink`` before
|
|
18
|
+
giving up. This fallback does **not** apply to self-hosted Kroki instances
|
|
19
|
+
(configured as ``kroki:<url>``) — those run in isolation and should not reach
|
|
20
|
+
out to external services.
|
|
13
21
|
"""
|
|
14
22
|
|
|
15
23
|
from __future__ import annotations
|
|
16
24
|
|
|
25
|
+
import base64
|
|
17
26
|
import dataclasses
|
|
18
27
|
import hashlib
|
|
28
|
+
import json
|
|
19
29
|
import sys
|
|
20
30
|
import threading
|
|
21
31
|
import time
|
|
22
32
|
import urllib.error
|
|
23
33
|
import urllib.request
|
|
34
|
+
import zlib
|
|
24
35
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
25
36
|
from pathlib import Path
|
|
26
37
|
from typing import cast
|
|
@@ -30,7 +41,8 @@ from mkdocs_to_confluence.ir.treeutil import replace_nodes
|
|
|
30
41
|
|
|
31
42
|
_CACHE_DIR = Path.home() / ".cache" / "mk2conf" / "mermaid"
|
|
32
43
|
DEFAULT_KROKI_URL = "https://kroki.io"
|
|
33
|
-
|
|
44
|
+
_MERMAID_INK_URL = "https://mermaid.ink"
|
|
45
|
+
_TIMEOUT = 30 # seconds
|
|
34
46
|
_MIN_PNG_BYTES = 67 # smallest valid PNG (1×1 px) is 67 bytes
|
|
35
47
|
_CACHE_LOCK = threading.Lock()
|
|
36
48
|
_MAX_WORKERS = 8
|
|
@@ -57,6 +69,21 @@ def _kroki_png(source: str, kroki_url: str) -> bytes:
|
|
|
57
69
|
return cast(bytes, resp.read())
|
|
58
70
|
|
|
59
71
|
|
|
72
|
+
def _mermaid_ink_png(source: str) -> bytes:
|
|
73
|
+
"""Fetch a PNG rendering of *source* from mermaid.ink (GET, pako-encoded).
|
|
74
|
+
|
|
75
|
+
Encodes the diagram as ``{"code": source}`` compressed with zlib and
|
|
76
|
+
base64url-encoded — the pako format that mermaid.ink expects.
|
|
77
|
+
"""
|
|
78
|
+
payload = json.dumps({"code": source, "mermaid": {"theme": "default"}}, separators=(",", ":"))
|
|
79
|
+
compressed = zlib.compress(payload.encode("utf-8"))
|
|
80
|
+
encoded = base64.urlsafe_b64encode(compressed).decode().rstrip("=")
|
|
81
|
+
url = f"{_MERMAID_INK_URL}/img/pako:{encoded}?type=png"
|
|
82
|
+
req = urllib.request.Request(url, headers={"User-Agent": "mk2conf/1.0"})
|
|
83
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 # nosec B310
|
|
84
|
+
return cast(bytes, resp.read())
|
|
85
|
+
|
|
86
|
+
|
|
60
87
|
def _cache_path(source: str) -> Path:
|
|
61
88
|
digest = hashlib.sha256(source.encode()).hexdigest()
|
|
62
89
|
return _CACHE_DIR / f"mermaid_{digest}.png"
|
|
@@ -71,6 +98,11 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
|
|
|
71
98
|
|
|
72
99
|
Transient HTTP errors (429, 5xx) and network blips are retried up to
|
|
73
100
|
``_RETRY_ATTEMPTS`` times with exponential backoff.
|
|
101
|
+
|
|
102
|
+
When using the public kroki.io and all retries are exhausted due to kroki
|
|
103
|
+
being unreachable (504 or timeout), one final attempt is made via
|
|
104
|
+
mermaid.ink before giving up. Self-hosted Kroki instances never fall back
|
|
105
|
+
to mermaid.ink.
|
|
74
106
|
"""
|
|
75
107
|
path = _cache_path(source)
|
|
76
108
|
if path.exists():
|
|
@@ -78,7 +110,10 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
|
|
|
78
110
|
print(" rendering mermaid diagram (cached)")
|
|
79
111
|
return path
|
|
80
112
|
|
|
113
|
+
use_public_kroki = kroki_url.rstrip("/") == DEFAULT_KROKI_URL.rstrip("/")
|
|
81
114
|
last_exc: Exception | None = None
|
|
115
|
+
kroki_unavailable = False # True when kroki is unreachable (504 or timeout)
|
|
116
|
+
|
|
82
117
|
for attempt in range(_RETRY_ATTEMPTS):
|
|
83
118
|
if attempt > 0:
|
|
84
119
|
delay = _RETRY_BACKOFF * (2 ** (attempt - 1))
|
|
@@ -96,17 +131,34 @@ def _render_one(source: str, kroki_url: str, *, quiet: bool = False) -> Path | N
|
|
|
96
131
|
except urllib.error.HTTPError as exc:
|
|
97
132
|
if exc.code in _RETRYABLE_HTTP:
|
|
98
133
|
last_exc = exc
|
|
134
|
+
kroki_unavailable = exc.code == 504
|
|
99
135
|
continue # retry
|
|
100
136
|
_warn(f"mermaid diagram: Kroki returned HTTP {exc.code} {exc.reason} — falling back to code block")
|
|
101
137
|
return None
|
|
102
138
|
except urllib.error.URLError as exc:
|
|
103
139
|
last_exc = exc
|
|
140
|
+
kroki_unavailable = True # timeout or connection refused — kroki unreachable
|
|
104
141
|
continue # retry — network blip
|
|
105
142
|
except (OSError, ValueError) as exc:
|
|
106
143
|
_warn(f"mermaid diagram: {exc} — falling back to code block")
|
|
107
144
|
return None
|
|
108
145
|
|
|
109
|
-
# All retries exhausted
|
|
146
|
+
# All Kroki retries exhausted — try mermaid.ink if on public kroki.io and kroki was unreachable.
|
|
147
|
+
if use_public_kroki and kroki_unavailable:
|
|
148
|
+
try:
|
|
149
|
+
_warn("mermaid diagram: kroki.io unreachable, trying mermaid.ink as fallback")
|
|
150
|
+
png = _mermaid_ink_png(source)
|
|
151
|
+
if len(png) < _MIN_PNG_BYTES:
|
|
152
|
+
raise ValueError(f"mermaid.ink returned {len(png)} bytes (expected a valid PNG)")
|
|
153
|
+
with _CACHE_LOCK:
|
|
154
|
+
path.write_bytes(png)
|
|
155
|
+
if not quiet:
|
|
156
|
+
print(" rendering mermaid diagram (via mermaid.ink fallback)")
|
|
157
|
+
return path
|
|
158
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError) as exc:
|
|
159
|
+
_warn(f"mermaid diagram: mermaid.ink fallback also failed ({exc}) — falling back to code block")
|
|
160
|
+
return None
|
|
161
|
+
|
|
110
162
|
_warn(f"mermaid diagram: failed after {_RETRY_ATTEMPTS} attempts ({last_exc}) — falling back to code block")
|
|
111
163
|
return None
|
|
112
164
|
|
|
@@ -125,7 +177,10 @@ def render_mermaid_diagrams(
|
|
|
125
177
|
upload as page attachments.
|
|
126
178
|
|
|
127
179
|
Diagrams that fail to render are left unchanged (code-block fallback).
|
|
128
|
-
|
|
180
|
+
When using public kroki.io and a diagram fails with a 504, one automatic
|
|
181
|
+
retry via mermaid.ink is attempted before falling back to a code block.
|
|
182
|
+
Self-hosted Kroki instances never contact mermaid.ink.
|
|
183
|
+
The pipeline always produces valid output regardless of service availability.
|
|
129
184
|
"""
|
|
130
185
|
try:
|
|
131
186
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -102,7 +102,7 @@ def test_render_deduplicates_identical_diagrams(tmp_path):
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def test_render_fallback_on_network_error(tmp_path, capsys):
|
|
105
|
-
"""When Kroki is unreachable, node
|
|
105
|
+
"""When Kroki is unreachable and mermaid.ink also fails, node falls back to code block."""
|
|
106
106
|
import urllib.error
|
|
107
107
|
|
|
108
108
|
node = MermaidDiagram(source=_SAMPLE_SOURCE)
|
|
@@ -114,6 +114,10 @@ def test_render_fallback_on_network_error(tmp_path, capsys):
|
|
|
114
114
|
"mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
115
115
|
side_effect=urllib.error.URLError("timed out"),
|
|
116
116
|
),
|
|
117
|
+
patch(
|
|
118
|
+
"mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
119
|
+
side_effect=urllib.error.URLError("timed out"),
|
|
120
|
+
),
|
|
117
121
|
):
|
|
118
122
|
updated_nodes, attachments = render_mermaid_diagrams((node,))
|
|
119
123
|
|
|
@@ -218,7 +222,7 @@ def test_render_multiple_diagrams_concurrently(tmp_path):
|
|
|
218
222
|
|
|
219
223
|
|
|
220
224
|
def test_render_one_failure_does_not_block_others(tmp_path):
|
|
221
|
-
"""If one diagram fails, the rest still render successfully."""
|
|
225
|
+
"""If one diagram fails (both kroki and mermaid.ink), the rest still render successfully."""
|
|
222
226
|
import urllib.error
|
|
223
227
|
|
|
224
228
|
good_source = "graph TD\n A --> B\n"
|
|
@@ -231,7 +235,9 @@ def test_render_one_failure_does_not_block_others(tmp_path):
|
|
|
231
235
|
return _FAKE_PNG
|
|
232
236
|
|
|
233
237
|
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
234
|
-
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png", side_effect=fake_kroki)
|
|
238
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png", side_effect=fake_kroki), \
|
|
239
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
240
|
+
side_effect=urllib.error.URLError("timeout")):
|
|
235
241
|
updated, attachments = render_mermaid_diagrams(nodes)
|
|
236
242
|
|
|
237
243
|
# One attachment for the successful diagram
|
|
@@ -258,7 +264,7 @@ def test_render_one_cached(tmp_path):
|
|
|
258
264
|
|
|
259
265
|
|
|
260
266
|
def test_render_one_network_failure_returns_none(tmp_path):
|
|
261
|
-
"""_render_one returns None after all retries
|
|
267
|
+
"""_render_one returns None after all retries when both kroki and mermaid.ink fail."""
|
|
262
268
|
import urllib.error
|
|
263
269
|
|
|
264
270
|
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
@@ -266,6 +272,8 @@ def test_render_one_network_failure_returns_none(tmp_path):
|
|
|
266
272
|
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
267
273
|
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
268
274
|
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
275
|
+
side_effect=urllib.error.URLError("connection refused")), \
|
|
276
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
269
277
|
side_effect=urllib.error.URLError("connection refused")):
|
|
270
278
|
result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
|
|
271
279
|
|
|
@@ -365,3 +373,99 @@ def test_render_mermaid_cached_quiet_suppresses_stdout(tmp_path, capsys):
|
|
|
365
373
|
assert result is not None
|
|
366
374
|
out, _ = capsys.readouterr()
|
|
367
375
|
assert out == ""
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ── mermaid.ink fallback ──────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_render_one_504_on_public_kroki_falls_back_to_mermaid_ink(tmp_path, capsys):
|
|
382
|
+
"""Public kroki.io 504 → automatic fallback to mermaid.ink."""
|
|
383
|
+
import urllib.error
|
|
384
|
+
|
|
385
|
+
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
386
|
+
|
|
387
|
+
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
388
|
+
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
389
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
390
|
+
side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
|
|
391
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
392
|
+
return_value=_FAKE_PNG) as mock_ink:
|
|
393
|
+
result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
|
|
394
|
+
|
|
395
|
+
assert result is not None
|
|
396
|
+
assert result.exists()
|
|
397
|
+
mock_ink.assert_called_once_with(_SAMPLE_SOURCE)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_render_one_timeout_on_public_kroki_falls_back_to_mermaid_ink(tmp_path):
|
|
401
|
+
"""Public kroki.io timeout → automatic fallback to mermaid.ink."""
|
|
402
|
+
import urllib.error
|
|
403
|
+
|
|
404
|
+
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
405
|
+
|
|
406
|
+
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
407
|
+
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
408
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
409
|
+
side_effect=urllib.error.URLError("timed out")), \
|
|
410
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
411
|
+
return_value=_FAKE_PNG) as mock_ink:
|
|
412
|
+
result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
|
|
413
|
+
|
|
414
|
+
assert result is not None
|
|
415
|
+
assert result.exists()
|
|
416
|
+
mock_ink.assert_called_once_with(_SAMPLE_SOURCE)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_render_one_504_on_self_hosted_kroki_does_not_fall_back(tmp_path):
|
|
420
|
+
"""Self-hosted Kroki 504 → no mermaid.ink fallback (privacy isolation)."""
|
|
421
|
+
import urllib.error
|
|
422
|
+
|
|
423
|
+
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
424
|
+
|
|
425
|
+
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
426
|
+
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
427
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
428
|
+
side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
|
|
429
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
430
|
+
return_value=_FAKE_PNG) as mock_ink:
|
|
431
|
+
result = _render_one(_SAMPLE_SOURCE, "https://my-internal-kroki.corp")
|
|
432
|
+
|
|
433
|
+
assert result is None
|
|
434
|
+
mock_ink.assert_not_called()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def test_render_one_timeout_on_self_hosted_kroki_does_not_fall_back(tmp_path):
|
|
438
|
+
"""Self-hosted Kroki timeout → no mermaid.ink fallback (privacy isolation)."""
|
|
439
|
+
import urllib.error
|
|
440
|
+
|
|
441
|
+
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
442
|
+
|
|
443
|
+
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
444
|
+
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
445
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
446
|
+
side_effect=urllib.error.URLError("timed out")), \
|
|
447
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
448
|
+
return_value=_FAKE_PNG) as mock_ink:
|
|
449
|
+
result = _render_one(_SAMPLE_SOURCE, "https://my-internal-kroki.corp")
|
|
450
|
+
|
|
451
|
+
assert result is None
|
|
452
|
+
mock_ink.assert_not_called()
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_render_one_mermaid_ink_fallback_also_fails_returns_none(tmp_path, capsys):
|
|
456
|
+
"""When mermaid.ink fallback also fails, returns None (code-block fallback)."""
|
|
457
|
+
import urllib.error
|
|
458
|
+
|
|
459
|
+
from mkdocs_to_confluence.transforms.mermaid import _render_one
|
|
460
|
+
|
|
461
|
+
with patch("mkdocs_to_confluence.transforms.mermaid._CACHE_DIR", tmp_path), \
|
|
462
|
+
patch("mkdocs_to_confluence.transforms.mermaid.time.sleep"), \
|
|
463
|
+
patch("mkdocs_to_confluence.transforms.mermaid._kroki_png",
|
|
464
|
+
side_effect=urllib.error.HTTPError("url", 504, "Gateway Timeout", {}, None)), \
|
|
465
|
+
patch("mkdocs_to_confluence.transforms.mermaid._mermaid_ink_png",
|
|
466
|
+
side_effect=urllib.error.URLError("connection refused")):
|
|
467
|
+
result = _render_one(_SAMPLE_SOURCE, "https://kroki.io")
|
|
468
|
+
|
|
469
|
+
assert result is None
|
|
470
|
+
_, err = capsys.readouterr()
|
|
471
|
+
assert "mermaid.ink" in err
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs2confluence.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/emitter/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/emitter/xhtml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/config.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/loader/extra_css.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/parser/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/parser/markdown.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/pdf/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/pdf/generator.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/fence.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/icons.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/includes.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preprocess/linkdefs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/render.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/preview/server.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/client.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/publisher/pipeline.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/anchoring.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/command.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/comments.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/sync/platform.py
RENAMED
|
File without changes
|
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/__init__.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/abbrevs.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/assets.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/editlink.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/footer.py
RENAMED
|
File without changes
|
{mkdocs2confluence-0.9.4 → mkdocs2confluence-0.9.6}/src/mkdocs_to_confluence/transforms/images.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|