未验证 提交 5be4b837 编写于 作者: G guqing 提交者: GitHub

feat: markdown supports footnote rendering (#1406)

* feat: MarkDown supports footnote rendering

* fix: code style

* feat: Flexmark footnote rendering adapts to markdown-it

* feat: Adaptation markdown-it-footnote back-ref button rendering and add test

* fix: Fix doc

* refactor: FootnoteNodeRenderInterceptorTest

* fix: Fix test code style

* refactor: change footnote back ref string

* refactor: remove unicode
上级 f4807b5b
......@@ -98,6 +98,7 @@ ext {
huaweiObsVersion = "3.19.7"
templateInheritanceVersion = "0.4.RELEASE"
jsoupVersion = "1.13.1"
byteBuddyAgentVersion = "1.10.22"
}
dependencies {
......@@ -141,12 +142,15 @@ dependencies {
implementation "com.vladsch.flexmark:flexmark-ext-superscript:$flexmarkVersion"
implementation "com.vladsch.flexmark:flexmark-ext-yaml-front-matter:$flexmarkVersion"
implementation "com.vladsch.flexmark:flexmark-ext-gitlab:$flexmarkVersion"
implementation "com.vladsch.flexmark:flexmark-ext-footnotes:$flexmarkVersion"
implementation "kr.pe.kwonnam.freemarker:freemarker-template-inheritance:$templateInheritanceVersion"
implementation "net.coobird:thumbnailator:$thumbnailatorVersion"
implementation "net.sf.image4j:image4j:$image4jVersion"
implementation "org.flywaydb:flyway-core:$flywayVersion"
implementation "com.google.zxing:core:$zxingVersion"
implementation "net.bytebuddy:byte-buddy-agent:$byteBuddyAgentVersion"
implementation "org.iq80.leveldb:leveldb:$levelDbVersion"
runtimeOnly "com.h2database:h2:$h2Version"
......
package run.halo.app.utils;
import com.vladsch.flexmark.ast.Link;
import com.vladsch.flexmark.ast.LinkNodeBase;
import com.vladsch.flexmark.ext.footnotes.Footnote;
import com.vladsch.flexmark.ext.footnotes.FootnoteBlock;
import com.vladsch.flexmark.ext.footnotes.internal.FootnoteNodeRenderer;
import com.vladsch.flexmark.ext.footnotes.internal.FootnoteOptions;
import com.vladsch.flexmark.ext.footnotes.internal.FootnoteRepository;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
import com.vladsch.flexmark.html.renderer.RenderingPhase;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.ast.NodeVisitor;
import com.vladsch.flexmark.util.ast.VisitHandler;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import java.lang.reflect.Field;
import java.util.Locale;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.FieldValue;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.yaml.snakeyaml.nodes.SequenceNode;
/**
* <code>Flexmark</code> footnote node render interceptor.
* Delegate the render method to intercept the FootNoteNodeRender by ByteBuddy runtime.
*
* @author guqing
* @date 2021-06-26
*/
public class FootnoteNodeRendererInterceptor {
/**
* Delegate the render method to intercept the FootNoteNodeRender by ByteBuddy runtime.
*/
public static void doDelegationMethod() {
ByteBuddyAgent.install();
new ByteBuddy()
.redefine(FootnoteNodeRenderer.class)
.method(ElementMatchers.named("render").and(ElementMatchers.takesArguments(
Footnote.class, NodeRendererContext.class, HtmlWriter.class)))
.intercept(MethodDelegation.to(FootnoteNodeRendererInterceptor.class))
.method(ElementMatchers.named("renderDocument"))
.intercept(MethodDelegation.to(FootnoteNodeRendererInterceptor.class))
.make()
.load(Thread.currentThread().getContextClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());
}
/**
* footnote render see {@link FootnoteNodeRenderer#renderDocument}.
*
* @param node footnote node
* @param context node renderer context
* @param html html writer
*/
public static void render(Footnote node, NodeRendererContext context, HtmlWriter html) {
FootnoteBlock footnoteBlock = node.getFootnoteBlock();
if (footnoteBlock == null) {
//just text
html.raw("[^");
context.renderChildren(node);
html.raw("]");
} else {
int footnoteOrdinal = footnoteBlock.getFootnoteOrdinal();
int i = node.getReferenceOrdinal();
html.attr("class", "footnote-ref");
html.srcPos(node.getChars()).withAttr()
.tag("sup", false, false, () -> {
// if (!options.footnoteLinkRefClass.isEmpty()) html.attr("class", options
// .footnoteLinkRefClass);
String ordinal = footnoteOrdinal + (i == 0 ? "" : String.format(Locale.US,
":%d", i));
html.attr("id", "fnref"
+ ordinal);
html.attr("href", "#fn" + footnoteOrdinal);
html.withAttr().tag("a");
html.raw("[" + ordinal + "]");
html.tag("/a");
});
}
}
/**
* render document.
*
* @param footnoteRepository footnoteRepository field of FootNoteRenderer class
* @param options options field of FootNoteRenderer class
* @param recheckUndefinedReferences recheckUndefinedReferences field of FootNoteRenderer class
* @param context node render context
* @param html html writer
* @param document document
* @param phase rendering phase
*/
public static void renderDocument(@FieldValue("footnoteRepository")
FootnoteRepository footnoteRepository,
@FieldValue("options") FootnoteOptions options,
@FieldValue("recheckUndefinedReferences")
boolean recheckUndefinedReferences,
@Argument(0) NodeRendererContext context,
@Argument(1) HtmlWriter html, @Argument(2) Document document,
@Argument(3)
RenderingPhase phase) {
final String footnoteBackLinkRefClass =
(String) getFootnoteOptionsFieldValue("footnoteBackLinkRefClass", options);
final String footnoteBackRefString = ObjectUtils
.getDisplayString(getFootnoteOptionsFieldValue("footnoteBackRefString", options));
if (phase == RenderingPhase.BODY_TOP) {
if (recheckUndefinedReferences) {
// need to see if have undefined footnotes that were defined after parsing
boolean[] hadNewFootnotes = {false};
NodeVisitor visitor = new NodeVisitor(
new VisitHandler<>(Footnote.class, node -> {
if (!node.isDefined()) {
FootnoteBlock footonoteBlock =
node.getFootnoteBlock(footnoteRepository);
if (footonoteBlock != null) {
footnoteRepository.addFootnoteReference(footonoteBlock, node);
node.setFootnoteBlock(footonoteBlock);
hadNewFootnotes[0] = true;
}
}
})
);
visitor.visit(document);
if (hadNewFootnotes[0]) {
footnoteRepository.resolveFootnoteOrdinals();
}
}
}
if (phase == RenderingPhase.BODY_BOTTOM) {
// here we dump the footnote blocks that were referenced in the document body, ie.
// ones with footnoteOrdinal > 0
if (footnoteRepository.getReferencedFootnoteBlocks().size() > 0) {
html.attr("class", "footnotes-sep").withAttr().tagVoid("hr");
html.attr("class", "footnotes").withAttr().tagIndent("section", () -> {
html.attr("class", "footnotes-list").withAttr().tagIndent("ol", () -> {
for (FootnoteBlock footnoteBlock : footnoteRepository
.getReferencedFootnoteBlocks()) {
int footnoteOrdinal = footnoteBlock.getFootnoteOrdinal();
html.attr("id", "fn" + footnoteOrdinal)
.attr("class", "footnote-item");
html.withAttr().tagIndent("li", () -> {
context.renderChildren(footnoteBlock);
int lineIndex = html.getLineCount() - 1;
BasedSequence line = html.getLine(lineIndex);
if (line.lastIndexOf("</p>") > -1) {
int iMax = footnoteBlock.getFootnoteReferences();
for (int i = 0; i < iMax; i++) {
StringBuilder sb = new StringBuilder();
sb.append(" <a href=\"#fnref").append(footnoteOrdinal)
.append(i == 0 ? "" : String
.format(Locale.US, ":%d", i)).append("\"");
if (StringUtils.isNotBlank(footnoteBackLinkRefClass)) {
sb.append(" class=\"").append(footnoteBackLinkRefClass)
.append("\"");
}
sb.append(">").append(footnoteBackRefString).append("</a>");
html.setLine(html.getLineCount() - 1, "",
line.insert(line.lastIndexOf("</p"), sb.toString()));
}
} else {
int iMax = footnoteBlock.getFootnoteReferences();
for (int i = 0; i < iMax; i++) {
html.attr("href", "#fnref" + footnoteOrdinal
+ (i == 0 ? "" : String.format(Locale.US, ":%d", i)));
if (StringUtils.isNotBlank(footnoteBackLinkRefClass)) {
html.attr("class", footnoteBackLinkRefClass);
}
html.line().withAttr().tag("a");
html.raw(footnoteBackRefString);
html.tag("/a");
}
}
});
}
});
});
}
}
}
/**
* Gets field value from FootnoteOptions.
*
* @param fieldName field name of FootNoteOptions class, must not be null.
* @param options target object, must not be null.
* @return field value.
*/
private static Object getFootnoteOptionsFieldValue(String fieldName, FootnoteOptions options) {
Assert.notNull(fieldName, "FieldName must not be null");
Assert.notNull(options, "FootnoteOptions type must not be null");
Object value = null;
try {
Field field = FootnoteOptions.class.getDeclaredField(fieldName);
field.setAccessible(true);
value = field.get(options);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return value;
}
}
......@@ -6,6 +6,7 @@ import com.vladsch.flexmark.ext.emoji.EmojiExtension;
import com.vladsch.flexmark.ext.emoji.EmojiImageType;
import com.vladsch.flexmark.ext.emoji.EmojiShortcutType;
import com.vladsch.flexmark.ext.escaped.character.EscapedCharacterExtension;
import com.vladsch.flexmark.ext.footnotes.FootnoteExtension;
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension;
import com.vladsch.flexmark.ext.gitlab.GitLabExtension;
......@@ -51,6 +52,7 @@ public class MarkdownUtils {
TocExtension.create(),
SuperscriptExtension.create(),
YamlFrontMatterExtension.create(),
FootnoteExtension.create(),
GitLabExtension.create()))
.set(TocExtension.LEVELS, 255)
.set(TablesExtension.WITH_CAPTION, false)
......@@ -63,7 +65,8 @@ public class MarkdownUtils {
.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
.set(EmojiExtension.USE_SHORTCUT_TYPE, EmojiShortcutType.EMOJI_CHEAT_SHEET)
.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY)
.set(HtmlRenderer.SOFT_BREAK, "<br />\n");
.set(HtmlRenderer.SOFT_BREAK, "<br />\n")
.set(FootnoteExtension.FOOTNOTE_BACK_REF_STRING, "↩︎");
private static final Parser PARSER = Parser.builder(OPTIONS).build();
......@@ -108,6 +111,8 @@ public class MarkdownUtils {
markdown = markdown
.replaceAll(HaloConst.YOUTUBE_VIDEO_REG_PATTERN, HaloConst.YOUTUBE_VIDEO_IFRAME);
}
// footnote render method delegation.
FootnoteNodeRendererInterceptor.doDelegationMethod();
Node document = PARSER.parse(markdown);
......
package run.halo.app.utils;
import cn.hutool.core.lang.Assert;
import com.vladsch.flexmark.ext.attributes.AttributesExtension;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.emoji.EmojiExtension;
import com.vladsch.flexmark.ext.emoji.EmojiImageType;
import com.vladsch.flexmark.ext.emoji.EmojiShortcutType;
import com.vladsch.flexmark.ext.footnotes.FootnoteExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.data.MutableDataSet;
import java.util.Arrays;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
/**
* Compare the rendering result of FootnoteNodeRendererInterceptor
* and <a href="https://github.com/markdown-it/markdown-it-footnote">markdown-it-footnote</a>.
* You can view <code>markdown-it-footnote's</code> rendering HTML results on this
* link <a href="https://markdown-it.github.io/">markdown-it-footnote example page</a>.
*
* @author guqing
* @date 2021-06-26
*/
public class FootnoteNodeRendererInterceptorTest {
private static final DataHolder OPTIONS =
new MutableDataSet().set(Parser.EXTENSIONS, Arrays.asList(EmojiExtension.create(),
FootnoteExtension.create()))
.set(HtmlRenderer.SOFT_BREAK, "<br />\n")
.set(FootnoteExtension.FOOTNOTE_BACK_REF_STRING, "↩︎")
.set(EmojiExtension.USE_SHORTCUT_TYPE, EmojiShortcutType.EMOJI_CHEAT_SHEET)
.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY);
private static final Parser PARSER = Parser.builder(OPTIONS).build();
private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build();
private String renderHtml(String markdown) {
FootnoteNodeRendererInterceptor.doDelegationMethod();
Node document = PARSER.parse(markdown);
return RENDERER.render(document);
}
@Test
public void duplicatedTest() {
// duplicated
String markdown = "text [^footnote] embedded.\n"
+ "\n"
+ "[^footnote]: footnote text\n"
+ "with continuation\n"
+ "\n"
+ "[^footnote]: duplicated footnote text\n"
+ "with continuation";
String s = renderHtml(markdown);
System.out.println(s);
Assert.isTrue(StringUtils.equals(s, "<p>text <sup class=\"footnote-ref\"><a id=\"fnref1\""
+ " href=\"#fn1\">[1]</a></sup> embedded.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>footnote text<br />\n"
+ "with continuation <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void nestedTest() {
// nested
String markdown = "text [^footnote] embedded.\n"
+ "\n"
+ "[^footnote]: footnote text with [^another] embedded footnote\n"
+ "with continuation\n"
+ "\n"
+ "[^another]: footnote text\n"
+ "with continuation";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>text <sup class=\"footnote-ref\"><a id=\"fnref1\""
+ " href=\"#fn1\">[1]</a></sup> embedded.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>footnote text with <sup class=\"footnote-ref\"><a id=\"fnref2\" "
+ "href=\"#fn2\">[2]</a></sup> embedded footnote<br />\n"
+ "with continuation <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "<li id=\"fn2\" class=\"footnote-item\">\n"
+ "<p>footnote text<br />\n"
+ "with continuation <a href=\"#fnref2\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void circularTest() {
// circular
String markdown = "text [^footnote] embedded.\n"
+ "\n"
+ "[^footnote]: footnote text with [^another] embedded footnote\n"
+ "with continuation\n"
+ "\n"
+ "[^another]: footnote text with [^another] embedded footnote\n"
+ "with continuation";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>text <sup class=\"footnote-ref\"><a id=\"fnref1\""
+ " href=\"#fn1\">[1]</a></sup> embedded.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>footnote text with <sup class=\"footnote-ref\"><a id=\"fnref2\" "
+ "href=\"#fn2\">[2]</a></sup> embedded footnote<br />\n"
+ "with continuation <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "<li id=\"fn2\" class=\"footnote-item\">\n"
+ "<p>footnote text with <sup class=\"footnote-ref\"><a id=\"fnref2:1\" "
+ "href=\"#fn2\">[2:1]</a></sup> embedded footnote<br />\n"
+ "with continuation <a href=\"#fnref2:1\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void compoundTest() {
// compound
String markdown = "This paragraph has a footnote[^footnote].\n"
+ "\n"
+ "[^footnote]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists.\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>This paragraph has a footnote<sup "
+ "class=\"footnote-ref\"><a id=\"fnref1\" href=\"#fn1\">[1]</a></sup>.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>This is the body of the footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.</p>\n"
+ "<p>Multiple paragraphs are supported as other<br />\n"
+ "markdown elements such as lists.</p>\n"
+ "<ul>\n"
+ "<li>item 1</li>\n"
+ "<li>item 2<br />\n"
+ ".</li>\n"
+ "</ul>\n"
+ "<a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void notFootnoteTest() {
// Not a footnote nor a footnote definition if space between [ and ^.
String markdown = "This paragraph has no footnote[ ^footnote].\n"
+ "\n"
+ "[ ^footnote]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists.\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>This paragraph has no footnote[ ^footnote].</p>\n"
+ "<p>[ ^footnote]: This is the body of the footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.</p>\n"
+ "<pre><code>Multiple paragraphs are supported as other \n"
+ "markdown elements such as lists.\n"
+ "\n"
+ "- item 1\n"
+ "- item 2\n"
+ "</code></pre>\n"
+ "<p>.</p>\n"));
}
@Test
public void unusedFootnotesTest() {
// Unused footnotes are not used and do not show up on the page.
String markdown = "This paragraph has a footnote[^2].\n"
+ "\n"
+ "[^1]: This is the body of the unused footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ "[^2]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists.\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>This paragraph has a footnote<sup "
+ "class=\"footnote-ref\"><a id=\"fnref1\" href=\"#fn1\">[1]</a></sup>.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>This is the body of the footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.</p>\n"
+ "<p>Multiple paragraphs are supported as other<br />\n"
+ "markdown elements such as lists.</p>\n"
+ "<ul>\n"
+ "<li>item 1</li>\n"
+ "<li>item 2<br />\n"
+ ".</li>\n"
+ "</ul>\n"
+ "<a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void undefinedFootnotesTest() {
// Undefined footnotes are rendered as if they were text, with emphasis left as is.
String markdown = "This paragraph has a footnote[^**footnote**].\n"
+ "\n"
+ "[^footnote]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists.\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils
.equals(s, "<p>This paragraph has a footnote[^<strong>footnote</strong>].</p>\n"));
}
@Test
public void footnoteNumbersOrderTest() {
// Footnote numbers are assigned in order of their reference in the document.
String markdown = "This paragraph has a footnote[^2]. Followed by another[^1]. \n"
+ "\n"
+ "[^1]: This is the body of the unused footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ "[^2]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists.\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>This paragraph has a footnote<sup "
+ "class=\"footnote-ref\"><a id=\"fnref1\" href=\"#fn1\">[1]</a></sup>. Followed by "
+ "another<sup class=\"footnote-ref\"><a id=\"fnref2\" href=\"#fn2\">[2]</a></sup>"
+ ".</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>This is the body of the footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.</p>\n"
+ "<p>Multiple paragraphs are supported as other<br />\n"
+ "markdown elements such as lists.</p>\n"
+ "<ul>\n"
+ "<li>item 1</li>\n"
+ "<li>item 2<br />\n"
+ ".</li>\n"
+ "</ul>\n"
+ "<a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a>\n"
+ "</li>\n"
+ "<li id=\"fn2\" class=\"footnote-item\">\n"
+ "<p>This is the body of the unused footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>. <a href=\"#fnref2\" class=\"footnote-backref\">↩︎"
+ "</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
@Test
public void containOtherReferencesTest() {
// Footnotes can contain references to other footnotes.
String markdown = "This paragraph has a footnote[^2]. \n"
+ "\n"
+ "[^2]: This is the body of the footnote.\n"
+ "with continuation text. Inline _italic_ and \n"
+ "**bold**.\n"
+ "\n"
+ " Multiple paragraphs are supported as other \n"
+ " markdown elements such as lists and footnotes[^1].\n"
+ " \n"
+ " - item 1\n"
+ " - item 2\n"
+ " \n"
+ " [^1]: This is the body of a nested footnote.\n"
+ " with continuation text. Inline _italic_ and \n"
+ " **bold**.\n"
+ ".";
String s = renderHtml(markdown);
Assert.isTrue(StringUtils.equals(s, "<p>This paragraph has a footnote<sup "
+ "class=\"footnote-ref\"><a id=\"fnref1\" href=\"#fn1\">[1]</a></sup>.</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>This is the body of the footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.</p>\n"
+ "<p>Multiple paragraphs are supported as other<br />\n"
+ "markdown elements such as lists and footnotes<sup class=\"footnote-ref\"><a "
+ "id=\"fnref2\" href=\"#fn2\">[2]</a></sup>.</p>\n"
+ "<ul>\n"
+ "<li>item 1</li>\n"
+ "<li>item 2</li>\n"
+ "</ul>\n"
+ "<a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a>\n"
+ "</li>\n"
+ "<li id=\"fn2\" class=\"footnote-item\">\n"
+ "<p>This is the body of a nested footnote.<br />\n"
+ "with continuation text. Inline <em>italic</em> and<br />\n"
+ "<strong>bold</strong>.<br />\n"
+ ". <a href=\"#fnref2\" class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n"));
}
}
......@@ -2,6 +2,7 @@ package run.halo.app.utils;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
/**
......@@ -30,4 +31,40 @@ class MarkdownUtilsTest {
+ "test---";
Assert.isTrue("test---".equals(MarkdownUtils.removeFrontMatter(markdown)));
}
@Test
void footNotesTest() {
String markdown1 = "驿外[^1]断桥边,寂寞开无主。已是黄昏独自愁,更着风和雨\n"
+ "[^1]: 驿(yì)外:指荒僻、冷清之地。驿,驿站。";
String s1 = MarkdownUtils.renderHtml(markdown1);
Assert.isTrue(StringUtils.isNotBlank(s1));
String s1Expected = "<p>驿外<sup class=\"footnote-ref\"><a id=\"fnref1\" "
+ "href=\"#fn1\">[1]</a></sup>断桥边,寂寞开无主。已是黄昏独自愁,更着风和雨</p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>驿(yì)外:指荒僻、冷清之地。驿,驿站。 <a href=\"#fnref1\" class=\"footnote-backref\">↩︎"
+ "</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n";
Assert.isTrue(StringUtils.equals(s1Expected, s1));
String markdown2 = "Paragraph with a footnote reference[^1]\n"
+ "[^1]: Footnote text added at the bottom of the document";
String s2 = MarkdownUtils.renderHtml(markdown2);
String s2Expected = "<p>Paragraph with a footnote reference<sup class=\"footnote-ref\"><a"
+ " id=\"fnref1\" href=\"#fn1\">[1]</a></sup></p>\n"
+ "<hr class=\"footnotes-sep\" />\n"
+ "<section class=\"footnotes\">\n"
+ "<ol class=\"footnotes-list\">\n"
+ "<li id=\"fn1\" class=\"footnote-item\">\n"
+ "<p>Footnote text added at the bottom of the document <a href=\"#fnref1\" "
+ "class=\"footnote-backref\">↩︎</a></p>\n"
+ "</li>\n"
+ "</ol>\n"
+ "</section>\n";
Assert.isTrue(StringUtils.equals(s2Expected, s2));
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册