Skip to content

Commit b738154

Browse files
committed
HMR-157 Parse and render Jira ticket numbers
1 parent 49ec9c2 commit b738154

9 files changed

Lines changed: 401 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>com.vladsch.flexmark</groupId>
6+
<artifactId>flexmark-java</artifactId>
7+
<version>0.65.2-SNAPSHOT</version>
8+
</parent>
9+
10+
<artifactId>flexmark-ext-jira-ticket-links</artifactId>
11+
<name>flexmark-java extension for parsing Jira ticket numbers and rendering links.</name>
12+
<description>flexmark-java extension for parsing Jira ticket numbers and rendering links to Jira tickets, e.g. translating HMR-157 to a link.
13+
Intended to be configured for your Jira instance, i.e. setting the base Jira URL.
14+
</description>
15+
16+
<dependencies>
17+
<dependency>
18+
<groupId>com.vladsch.flexmark</groupId>
19+
<artifactId>flexmark</artifactId>
20+
</dependency>
21+
<dependency>
22+
<groupId>com.vladsch.flexmark</groupId>
23+
<artifactId>flexmark-util</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>com.vladsch.flexmark</groupId>
27+
<artifactId>flexmark-util-data</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>com.vladsch.flexmark</groupId>
31+
<artifactId>flexmark-test-util</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>com.vladsch.flexmark</groupId>
36+
<artifactId>flexmark-core-test</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
</dependencies>
40+
</project>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2026 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets;
8+
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import com.vladsch.flexmark.ext.jira.tickets.internal.JiraTicketNumberInLineParserExtension;
12+
import com.vladsch.flexmark.ext.jira.tickets.internal.JiraTicketNumberNodeRenderer;
13+
import com.vladsch.flexmark.html.HtmlRenderer;
14+
import com.vladsch.flexmark.parser.Parser;
15+
import com.vladsch.flexmark.util.data.DataKey;
16+
import com.vladsch.flexmark.util.data.MutableDataHolder;
17+
18+
public class JiraTicketExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
19+
20+
public static final DataKey<String> JIRA_URL = new DataKey<>("JIRA_URL", "https://your.atlassian.net/browse/");
21+
22+
private JiraTicketExtension() {
23+
}
24+
25+
public static JiraTicketExtension create() {
26+
return new JiraTicketExtension();
27+
}
28+
29+
@Override
30+
public void parserOptions(MutableDataHolder options) {
31+
32+
}
33+
34+
@Override
35+
public void rendererOptions(@NotNull MutableDataHolder options) {
36+
37+
}
38+
39+
@Override
40+
public void extend(Parser.Builder parserBuilder) {
41+
parserBuilder.customInlineParserExtensionFactory(new JiraTicketNumberInLineParserExtension.Factory());
42+
}
43+
44+
@Override
45+
public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, @NotNull String rendererType) {
46+
if (htmlRendererBuilder.isRendererType("HTML")) {
47+
htmlRendererBuilder.nodeRendererFactory(new JiraTicketNumberNodeRenderer.Factory());
48+
}
49+
}
50+
51+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2026 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets;
8+
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import com.vladsch.flexmark.util.ast.Node;
12+
import com.vladsch.flexmark.util.sequence.BasedSequence;
13+
14+
/**
15+
* Node representing Jira ticket numbers like HMR-157.
16+
*/
17+
public class JiraTicketNumberNode extends Node {
18+
19+
protected BasedSequence text = BasedSequence.NULL;
20+
21+
public JiraTicketNumberNode() {
22+
}
23+
24+
public JiraTicketNumberNode(BasedSequence chars) {
25+
super(chars);
26+
}
27+
28+
@Override
29+
public @NotNull BasedSequence[] getSegments() {
30+
return EMPTY_SEGMENTS;
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2026 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets.internal;
8+
9+
import java.util.Set;
10+
import java.util.regex.Pattern;
11+
12+
import org.jetbrains.annotations.NotNull;
13+
import org.jetbrains.annotations.Nullable;
14+
15+
import com.vladsch.flexmark.ext.jira.tickets.JiraTicketNumberNode;
16+
import com.vladsch.flexmark.parser.InlineParser;
17+
import com.vladsch.flexmark.parser.InlineParserExtension;
18+
import com.vladsch.flexmark.parser.InlineParserExtensionFactory;
19+
import com.vladsch.flexmark.parser.LightInlineParser;
20+
import com.vladsch.flexmark.util.sequence.BasedSequence;
21+
22+
public class JiraTicketNumberInLineParserExtension implements InlineParserExtension {
23+
24+
private static final Pattern REGEX_JIRA_TICKET_NUMBER = Pattern.compile("^[A-Z]+-\\d+\\b");
25+
26+
public JiraTicketNumberInLineParserExtension(LightInlineParser inlineParser) {
27+
28+
}
29+
30+
@Override
31+
public void finalizeDocument(@NotNull InlineParser inlineParser) {
32+
// nothing to do
33+
}
34+
35+
@Override
36+
public void finalizeBlock(@NotNull InlineParser inlineParser) {
37+
// nothing to do
38+
}
39+
40+
@Override
41+
public boolean parse(@NotNull LightInlineParser inlineParser) {
42+
BasedSequence match = inlineParser.match(REGEX_JIRA_TICKET_NUMBER);
43+
if (match == null) {
44+
return false;
45+
}
46+
47+
inlineParser.flushTextNode();
48+
49+
JiraTicketNumberNode jiraTicket = new JiraTicketNumberNode(match);
50+
inlineParser.getBlock().appendChild(jiraTicket);
51+
52+
return true;
53+
}
54+
55+
public static class Factory implements InlineParserExtensionFactory {
56+
@Nullable
57+
@Override
58+
public Set<Class<?>> getAfterDependents() {
59+
return null;
60+
}
61+
62+
@NotNull
63+
@Override
64+
public CharSequence getCharacters() {
65+
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
66+
}
67+
68+
@Nullable
69+
@Override
70+
public Set<Class<?>> getBeforeDependents() {
71+
return null;
72+
}
73+
74+
@NotNull
75+
@Override
76+
public InlineParserExtension apply(@NotNull LightInlineParser inlineParser) {
77+
return new JiraTicketNumberInLineParserExtension(inlineParser);
78+
}
79+
80+
@Override
81+
public boolean affectsGlobalScope() {
82+
return false;
83+
}
84+
}
85+
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2026 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets.internal;
8+
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
12+
import org.jetbrains.annotations.NotNull;
13+
import org.jetbrains.annotations.Nullable;
14+
15+
import com.vladsch.flexmark.ext.jira.tickets.JiraTicketNumberNode;
16+
import com.vladsch.flexmark.html.HtmlWriter;
17+
import com.vladsch.flexmark.html.renderer.NodeRenderer;
18+
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
19+
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
20+
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
21+
import com.vladsch.flexmark.util.data.DataHolder;
22+
23+
public class JiraTicketNumberNodeRenderer implements NodeRenderer {
24+
25+
private final JiraTicketNumbersOptions options;
26+
27+
public JiraTicketNumberNodeRenderer(DataHolder options) {
28+
this.options = new JiraTicketNumbersOptions(options);
29+
}
30+
31+
@Override
32+
public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
33+
Set<NodeRenderingHandler<?>> set = new HashSet<>();
34+
set.add(new NodeRenderingHandler<>(JiraTicketNumberNode.class, this::render));
35+
return set;
36+
}
37+
38+
private void render(JiraTicketNumberNode node, NodeRendererContext context, HtmlWriter htmlWriter) {
39+
if (context.isDoNotRenderLinks()) {
40+
htmlWriter.text(node.getChars());
41+
} else {
42+
String targetUrl = options.jiraTicketUrl + node.getChars();
43+
44+
htmlWriter.srcPos(node.getChars())
45+
.attr("href", targetUrl)
46+
.withAttr()
47+
.tag("a");
48+
htmlWriter.text(node.getChars());
49+
50+
htmlWriter.tag("/a");
51+
}
52+
}
53+
54+
public static class Factory implements NodeRendererFactory {
55+
@NotNull
56+
@Override
57+
public NodeRenderer apply(@NotNull DataHolder options) {
58+
return new JiraTicketNumberNodeRenderer(options);
59+
}
60+
}
61+
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2026 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets.internal;
8+
9+
import org.jetbrains.annotations.NotNull;
10+
11+
import com.vladsch.flexmark.ext.jira.tickets.JiraTicketExtension;
12+
import com.vladsch.flexmark.util.data.DataHolder;
13+
import com.vladsch.flexmark.util.data.MutableDataHolder;
14+
import com.vladsch.flexmark.util.data.MutableDataSetter;
15+
16+
public class JiraTicketNumbersOptions implements MutableDataSetter {
17+
18+
public final String jiraTicketUrl;
19+
20+
public JiraTicketNumbersOptions(DataHolder options) {
21+
jiraTicketUrl = JiraTicketExtension.JIRA_URL.get(options);
22+
}
23+
24+
@NotNull
25+
@Override
26+
public MutableDataHolder setIn(@NotNull MutableDataHolder dataHolder) {
27+
dataHolder.set(JiraTicketExtension.JIRA_URL, jiraTicketUrl);
28+
return dataHolder;
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* This work is made available under the terms of the BSD 2-Clause "Simplified" License.
3+
* The BSD accompanies this distribution (LICENSE.txt).
4+
*
5+
* Copyright © 2022-2024 Advantest Europe GmbH. All rights reserved.
6+
*/
7+
package com.vladsch.flexmark.ext.jira.tickets;
8+
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import org.jetbrains.annotations.NotNull;
15+
import org.junit.runners.Parameterized;
16+
17+
import com.vladsch.flexmark.core.test.util.RendererSpecTest;
18+
import com.vladsch.flexmark.parser.Parser;
19+
import com.vladsch.flexmark.test.util.spec.ResourceLocation;
20+
import com.vladsch.flexmark.test.util.spec.SpecExample;
21+
import com.vladsch.flexmark.util.data.DataHolder;
22+
import com.vladsch.flexmark.util.data.MutableDataSet;
23+
24+
public class JiraTicketNumberParserRendererSpecTest extends RendererSpecTest {
25+
26+
private static final String SPEC_RESOURCE = "/ext_jira_ast_spec.md";
27+
28+
@NotNull
29+
public static final ResourceLocation RESOURCE_LOCATION = ResourceLocation.of(SPEC_RESOURCE);
30+
31+
private static final DataHolder OPTIONS = new MutableDataSet()
32+
.set(Parser.EXTENSIONS, Collections.singleton(JiraTicketExtension.create()))
33+
.toImmutable();
34+
35+
private static final Map<String, DataHolder> optionsMap = new HashMap<>();
36+
static {
37+
optionsMap.put("custom_root", new MutableDataSet().set(JiraTicketExtension.JIRA_URL, "https://www.your-domain.com/browse/"));
38+
}
39+
40+
public JiraTicketNumberParserRendererSpecTest(@NotNull SpecExample example) {
41+
super(example, optionsMap, OPTIONS);
42+
}
43+
44+
@Parameterized.Parameters(name = "{0}")
45+
public static List<Object[]> data() {
46+
return getTestData(RESOURCE_LOCATION);
47+
}
48+
}

0 commit comments

Comments
 (0)