Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions core/src/org/labkey/core/wiki/MarkdownServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@
import org.commonmark.ext.image.attributes.ImageAttributesExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.html.CoreHtmlNodeRenderer;
import org.commonmark.node.HtmlInline;
import org.commonmark.node.HtmlBlock;
import org.labkey.api.markdown.MarkdownService;

import java.util.List;
import java.util.Set;

public class MarkdownServiceImpl implements MarkdownService
{
Expand Down Expand Up @@ -55,10 +60,65 @@ public MarkdownServiceImpl()
.softbreak("<br>\n") // See Issue #34169
.sanitizeUrls(true)
.escapeHtml(true)
.nodeRendererFactory(CommentNodeRenderer::new)
.extensions(extensions)
.build();
}

private static class CommentNodeRenderer extends CoreHtmlNodeRenderer
{
private final HtmlNodeRendererContext _context;

public CommentNodeRenderer(HtmlNodeRendererContext context)
{
super(context);
_context = context;
}

@Override
public Set<Class<? extends Node>> getNodeTypes()
{
return Set.of(HtmlInline.class, HtmlBlock.class);
}

@Override
public void render(Node node)
{
if (node instanceof HtmlInline inline)
{
String literal = inline.getLiteral();
if (isComment(literal))
{
_context.getWriter().raw(literal);
}
else
{
_context.getWriter().text(literal);
}
}
else if (node instanceof HtmlBlock block)
{
String literal = block.getLiteral();
if (isComment(literal))
{
_context.getWriter().raw(literal);
}
else
{
_context.getWriter().tag("p");
_context.getWriter().text(literal);
_context.getWriter().tag("/p");
_context.getWriter().line();
}
}
}

private boolean isComment(String literal)
{
return literal != null && literal.trim().startsWith("<!--") && literal.trim().endsWith("-->");
}
}

@Override
public String toHtml(String mdText)
{
Expand Down
27 changes: 21 additions & 6 deletions core/src/org/labkey/core/wiki/MarkdownTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class MarkdownTestCase extends Assert
@Test
public void testMdHeadingToHtml()
{
MarkdownService markdownService = MarkdownService.get();
MarkdownService markdownService = new MarkdownServiceImpl();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious... why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets the test run as a standalone unit test, not needing to be run inside the webapp.

String testMdText = "# This is a H1 header";
String expectedHtmlText = "<div class=\"lk-markdown-container\"><h1 id=\"this-is-a-h1-header\">This is a H1 header</h1>\n</div>";
String htmlText = markdownService.toHtml(testMdText);
Expand All @@ -28,7 +28,7 @@ public void testMdHeadingToHtml()
@Test
public void testMdBoldToHtml()
{
MarkdownService markdownService = MarkdownService.get();
MarkdownService markdownService = new MarkdownServiceImpl();
String testMdText = "**This is bold text**";
String expectedHtmlText = "<div class=\"lk-markdown-container\"><p><strong>This is bold text</strong></p>\n</div>";
String htmlText = markdownService.toHtml(testMdText);
Expand All @@ -41,11 +41,10 @@ public void testMdBoldToHtml()
@Test
public void testMdHtmlTags()
{
MarkdownService markdownService = MarkdownService.get();

MarkdownService markdownService = new MarkdownServiceImpl();
String testMdText = "<h2>header</h2>";
String expectedHtmlText = "<div class=\"lk-markdown-container\"><p>&lt;h2&gt;header&lt;/h2&gt;</p>\n</div>";
String htmlText = markdownService.toHtml(testMdText);
String expectedHtmlText = "<div class=\"lk-markdown-container\"><p>&lt;h2&gt;header&lt;/h2&gt;</p>\n</div>";
assertEquals("The MarkdownService failed to correctly escape html tags.", expectedHtmlText, htmlText);

testMdText = "<script>alert()</script>";
Expand All @@ -60,7 +59,7 @@ public void testMdHtmlTags()
@Test
public void testMdComplexToHtml()
{
MarkdownService markdownService = MarkdownService.get();
MarkdownService markdownService = new MarkdownServiceImpl();
// this sample of markdown and translation taken from part of: https://markdown-it.github.io/
String testMdText = """
---
Expand Down Expand Up @@ -341,4 +340,20 @@ public void testMdComplexToHtml()
String htmlText = markdownService.toHtml(testMdText);
assertEquals("The MarkdownService failed to correctly translate complex markdown text to html.", expectedHtmlText, htmlText);
}
@Test
public void testHtmlComments()
{
MarkdownService markdownService = new MarkdownServiceImpl();

String testMdText = "Text before <!-- comment --> text after";
String htmlText = markdownService.toHtml(testMdText);

assertTrue("Comment was encoded: " + htmlText, htmlText.contains("<!-- comment -->"));
assertFalse("Comment should not be encoded: " + htmlText, htmlText.contains("&lt;!--"));

// Verification for <script> still being encoded
String scriptMd = "<script>alert('hi')</script>";
String scriptHtml = markdownService.toHtml(scriptMd);
assertTrue("Script tags should still be encoded: " + scriptHtml, scriptHtml.contains("&lt;script&gt;"));
}
}