Skip to content

Commit 2305f51

Browse files
mermaid: links support (#1482)
1 parent e80e7e2 commit 2305f51

19 files changed

Lines changed: 394 additions & 30 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.slf4j.simpleLogger.log.io.netty.resolver.dns.DnsServerAddressStreamProviders=off

znai-diagrams/src/main/java/org/testingisdocumenting/znai/diagrams/mermaid/MermaidFencePlugin.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ public PluginResult process(ComponentsRegistry componentsRegistry, Path markupPa
5252
this.content = content;
5353
Map<String, Object> props = new LinkedHashMap<>(pluginParams.getOpts().toMap());
5454
processIconPacks(componentsRegistry, props);
55-
props.put("mermaid", content);
55+
56+
String processedContent = processLinks(componentsRegistry, markupPath, content);
57+
props.put("mermaid", processedContent);
5658

5759
return PluginResult.docElement("Mermaid", props);
5860
}

znai-diagrams/src/main/java/org/testingisdocumenting/znai/diagrams/mermaid/MermaidIncludePlugin.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ public PluginResult process(ComponentsRegistry componentsRegistry, ParserHandler
5757
content = componentsRegistry.resourceResolver().textContent(mermaidPath);
5858

5959
Map<String, Object> props = new LinkedHashMap<>(pluginParams.getOpts().toMap());
60-
props.put("mermaid", content);
60+
61+
String processedContent = processLinks(componentsRegistry, markupPath, content);
62+
props.put("mermaid", processedContent);
6163
processIconPacks(componentsRegistry, props);
6264
return PluginResult.docElement("Mermaid", props);
6365
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.znai.diagrams.mermaid;
18+
19+
import org.testingisdocumenting.znai.structure.DocStructure;
20+
import org.testingisdocumenting.znai.structure.DocUrl;
21+
import org.testingisdocumenting.znai.utils.UrlUtils;
22+
23+
import java.nio.file.Path;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
26+
27+
class MermaidLinkResolver {
28+
// matches: click nodeId "url" or click nodeId href "url"
29+
private static final Pattern CLICK_URL_PATTERN = Pattern.compile(
30+
"click\\s+\\S+\\s+(?:href\\s+)?\"([^\"]+)\"");
31+
32+
private MermaidLinkResolver() {
33+
}
34+
35+
static String validateAndResolveLinks(DocStructure docStructure, Path markupPath, String mermaidContent) {
36+
Matcher matcher = CLICK_URL_PATTERN.matcher(mermaidContent);
37+
StringBuilder result = new StringBuilder();
38+
while (matcher.find()) {
39+
String url = matcher.group(1);
40+
if (UrlUtils.isExternal(url)) {
41+
matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group()));
42+
continue;
43+
}
44+
45+
DocUrl docUrl = new DocUrl(url);
46+
docStructure.validateUrl(markupPath, "inside mermaid diagram", docUrl);
47+
48+
String resolvedUrl = docStructure.createUrl(markupPath, docUrl);
49+
String replacement = matcher.group().replace("\"" + url + "\"", "\"" + resolvedUrl + "\"");
50+
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
51+
}
52+
matcher.appendTail(result);
53+
return result.toString();
54+
}
55+
}

znai-diagrams/src/main/java/org/testingisdocumenting/znai/diagrams/mermaid/MermaidPluginBase.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.testingisdocumenting.znai.structure.DocStructure;
2424
import org.testingisdocumenting.znai.utils.UrlUtils;
2525

26+
import java.nio.file.Path;
2627
import java.util.HashMap;
2728
import java.util.List;
2829
import java.util.Map;
@@ -35,17 +36,18 @@ public Stream<AuxiliaryFile> auxiliaryFiles(ComponentsRegistry componentsRegistr
3536
return additionalAuxiliaryFiles.entrySet().stream().filter(e -> e.getValue() == Boolean.FALSE).map(Map.Entry::getKey);
3637
}
3738

39+
protected String processLinks(ComponentsRegistry componentsRegistry, Path markupPath, String content) {
40+
return MermaidLinkResolver.validateAndResolveLinks(componentsRegistry.docStructure(), markupPath, content);
41+
}
42+
3843
protected void processIconPacks(ComponentsRegistry componentsRegistry, Map<String, Object> props) {
3944
ResourcesResolver resourcesResolver = componentsRegistry.resourceResolver();
4045
DocStructure docStructure = componentsRegistry.docStructure();
4146
if (!props.containsKey("iconpacks")) {
4247
return;
4348
}
44-
if ((props.get("iconpacks") instanceof List<?>)) {
45-
List<?> iconPacks = (List<?>) props.get("iconpacks");
46-
iconPacks.forEach(iconPack -> {
47-
tweakIconpackUrl(resourcesResolver, docStructure, iconPack);
48-
}
49+
if ((props.get("iconpacks") instanceof List<?> iconPacks)) {
50+
iconPacks.forEach(iconPack -> tweakIconpackUrl(resourcesResolver, docStructure, iconPack)
4951
);
5052
}
5153
}

znai-diagrams/src/test/groovy/org/testingisdocumenting/znai/diagrams/mermaid/MermaidFencePluginTest.groovy

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package org.testingisdocumenting.znai.diagrams.mermaid
1818

1919
import org.testingisdocumenting.znai.extensions.include.PluginsTestUtils
2020
import org.testingisdocumenting.znai.extensions.PluginParamsFactory
21+
import org.junit.After
22+
import org.junit.Before
2123
import org.junit.Test
2224

2325
import static org.testingisdocumenting.znai.parser.TestComponentsRegistry.TEST_COMPONENTS_REGISTRY
@@ -26,6 +28,13 @@ import static org.testingisdocumenting.webtau.Matchers.throwException
2628

2729
class MermaidFencePluginTest {
2830
static PluginParamsFactory pluginParamsFactory = TEST_COMPONENTS_REGISTRY.pluginParamsFactory()
31+
32+
@Before
33+
@After
34+
void init() {
35+
TEST_COMPONENTS_REGISTRY.docStructure().clear()
36+
}
37+
2938
@Test
3039
void "should process mermaid diagram without iconpacks parameter"() {
3140
def mermaidContent = '''graph TD
@@ -88,6 +97,37 @@ class MermaidFencePluginTest {
8897
} should throwException(IllegalArgumentException.class, "iconpack url is missing")
8998
}
9099

100+
@Test
101+
void "should resolve relative links and keep external links"() {
102+
TEST_COMPONENTS_REGISTRY.docStructure().addValidLink("doc/page")
103+
TEST_COMPONENTS_REGISTRY.docStructure().addValidLink("ref/another#section")
104+
105+
def mermaidContent = '''flowchart TD
106+
A[Start] --> B[Process] --> C[End]
107+
click A "doc/page"
108+
click B href "ref/another#section" "Go to page"
109+
click C "https://example.com"'''
110+
111+
def elements = process(mermaidContent, Collections.emptyMap())
112+
113+
elements.mermaid.should == '''flowchart TD
114+
A[Start] --> B[Process] --> C[End]
115+
click A "/test-doc/doc/page"
116+
click B href "/test-doc/ref/another#section" "Go to page"
117+
click C "https://example.com"'''
118+
}
119+
120+
@Test
121+
void "should validate links and report invalid ones"() {
122+
def mermaidContent = '''flowchart TD
123+
A[Start] --> B[End]
124+
click A "doc/page"'''
125+
126+
code {
127+
process(mermaidContent, Collections.emptyMap())
128+
} should throwException(~/no valid link.*doc\/page/)
129+
}
130+
91131
private static def process(String content, Map<String, ?> params) {
92132
return PluginsTestUtils.processFenceAndGetProps(pluginParamsFactory.create("mermaid", "", params), content)
93133
}

znai-docs/znai/llm.txt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ answer-link: znai-from-export/introduction/getting-started#command-line
146146
## CLI download
147147

148148
Download and unzip
149-
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.86/znai-dist-1.86-znai.zip). Add
150-
it to your `PATH`.
149+
[znai](https://repo.maven.apache.org/maven2/org/testingisdocumenting/znai/znai-dist/1.87-SNAPSHOT/znai-dist-1.87-SNAPSHOT-znai.zip).
150+
Add it to your `PATH`.
151151

152152
## Brew
153153

@@ -162,7 +162,7 @@ answer-link: znai-from-export/introduction/getting-started#maven-plugin
162162
<plugin>
163163
<groupId>org.testingisdocumenting.znai</groupId>
164164
<artifactId>znai-maven-plugin</artifactId>
165-
<version>1.86</version>
165+
<version>1.87-SNAPSHOT</version>
166166
</plugin>
167167
```
168168

@@ -5096,6 +5096,27 @@ classDiagram
50965096
}
50975097
```
50985098

5099+
# Visuals :: Mermaid Diagrams :: Links
5100+
answer-link: znai-from-export/visuals/mermaid-diagrams#links
5101+
5102+
Use mermaid `click` statements to add links to diagram nodes. Relative links will be resolved and validated against your
5103+
documentation structure.
5104+
5105+
```
5106+
```mermaid
5107+
flowchart TD
5108+
A[Start] --> B{Is it?}
5109+
B -- Yes --> C[OK]
5110+
B -- No ----> D[End]
5111+
click A "visuals/mermaid-diagrams"
5112+
click D href "https://mermaid-js.github.io/mermaid/#/" "Mermaid docs"
5113+
```
5114+
```
5115+
5116+
5117+
Note: Relative links like `visuals/mermaid-diagrams` are validated during build time, the same way regular markdown
5118+
links are validated.
5119+
50995120
# Visuals :: Mermaid Diagrams :: Wide Mode
51005121
answer-link: znai-from-export/visuals/mermaid-diagrams#wide-mode
51015122

@@ -9043,6 +9064,11 @@ export PATH=$(pwd)/dist:$PATH
90439064
znai --version
90449065
```
90459066

9067+
# Release Notes :: 2026 :: 1.87
9068+
answer-link: znai-from-export/release-notes/2026#187
9069+
9070+
9071+
90469072
# Release Notes :: 2026 :: 1.86
90479073
answer-link: znai-from-export/release-notes/2026#186
90489074

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add: Mermaid diagrams links are automatically validated and clicking them performs fast navigation without page reloads

znai-docs/znai/release-notes/2026.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 1.87
2+
3+
:include-markdowns: 1.87
4+
15
# 1.86
26

37
:include-markdowns: 1.86

znai-docs/znai/visuals/mermaid-diagrams.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,30 @@ Use include plugin to render a Mermaid diagram from a file.
3737

3838
:include-file: mermaid/class-diagram.mmd { autoTitle: true }
3939

40+
# Links
41+
42+
Use mermaid `click` statements to add links to diagram nodes. Relative links will be resolved and validated against your documentation structure.
43+
44+
```mermaid
45+
flowchart TD
46+
A[Start] --> B{Is it?}
47+
B -- Yes --> C[OK]
48+
B -- No ----> D[End]
49+
click A "visuals/mermaid-diagrams"
50+
click D href "https://mermaid-js.github.io/mermaid/#/" "Mermaid docs"
51+
```
52+
53+
```mermaid
54+
flowchart TD
55+
A[Start] --> B{Is it?}
56+
B -- Yes --> C[OK]
57+
B -- No ----> D[End]
58+
click A "introduction/getting-started"
59+
click D href "https://mermaid-js.github.io/mermaid/#/" "Mermaid docs"
60+
```
61+
62+
Note: Relative links like `visuals/mermaid-diagrams` are validated during build time, the same way regular markdown links are validated.
63+
4064
# Wide Mode
4165

4266
Use `wide: true` to use as much horizontal space as required and available.

0 commit comments

Comments
 (0)