mirror of https://github.com/halo-dev/halo
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 unicodepull/1415/head
parent
f4807b5b56
commit
5be4b83799
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
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);
|
||||
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue