diff --git a/build.gradle b/build.gradle
index 6f677d639..afc3ada1e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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"
diff --git a/src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java b/src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java
new file mode 100644
index 000000000..695447052
--- /dev/null
+++ b/src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java
@@ -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;
+
+/**
+ * Flexmark
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("
markdown-it-footnote's
rendering HTML results on this
+ * link markdown-it-footnote example page.
+ *
+ * @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, "text [1] embedded.
\n" + + "footnote text
\n"
+ + "with continuation ↩︎
text [1] embedded.
\n" + + "text [1] embedded.
\n" + + "This paragraph has a footnote[1].
\n" + + "This is the body of the footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
Multiple paragraphs are supported as other
\n"
+ + "markdown elements such as lists.
This paragraph has no footnote[ ^footnote].
\n" + + "[ ^footnote]: This is the body of the footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
Multiple paragraphs are supported as other \n"
+ + "markdown elements such as lists.\n"
+ + "\n"
+ + "- item 1\n"
+ + "- item 2\n"
+ + "
\n"
+ + ".
\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, "This paragraph has a footnote[1].
\n" + + "This is the body of the footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
Multiple paragraphs are supported as other
\n"
+ + "markdown elements such as lists.
This paragraph has a footnote[^footnote].
\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, "This paragraph has a footnote[1]. Followed by " + + "another[2]" + + ".
\n" + + "This is the body of the footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
Multiple paragraphs are supported as other
\n"
+ + "markdown elements such as lists.
This is the body of the unused footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold. ↩︎"
+ + "
This paragraph has a footnote[1].
\n" + + "This is the body of the footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
Multiple paragraphs are supported as other
\n"
+ + "markdown elements such as lists and footnotes[2].
This is the body of a nested footnote.
\n"
+ + "with continuation text. Inline italic and
\n"
+ + "bold.
\n"
+ + ". ↩︎
驿外[1]断桥边,寂寞开无主。已是黄昏独自愁,更着风和雨
\n" + + "驿(yì)外:指荒僻、冷清之地。驿,驿站。 ↩︎" + + "
\n" + + "Paragraph with a footnote reference[1]
\n" + + "Footnote text added at the bottom of the document ↩︎
\n" + + "