diff --git a/build.gradle b/build.gradle index 50b8079518..6c36bcf82d 100644 --- a/build.gradle +++ b/build.gradle @@ -98,7 +98,6 @@ ext { huaweiObsVersion = "3.19.7" templateInheritanceVersion = "0.4.RELEASE" jsoupVersion = "1.13.1" - byteBuddyAgentVersion = "1.10.22" } dependencies { @@ -142,15 +141,12 @@ 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/MarkdownUtils.java b/src/main/java/run/halo/app/utils/MarkdownUtils.java index 8a674fee87..349a338b8b 100644 --- a/src/main/java/run/halo/app/utils/MarkdownUtils.java +++ b/src/main/java/run/halo/app/utils/MarkdownUtils.java @@ -6,7 +6,6 @@ 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; @@ -30,6 +29,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import run.halo.app.model.support.HaloConst; +import run.halo.app.utils.footnotes.FootnoteExtension; /** * Markdown utils. @@ -111,8 +111,6 @@ public static String renderHtml(String markdown) { markdown = markdown .replaceAll(HaloConst.YOUTUBE_VIDEO_REG_PATTERN, HaloConst.YOUTUBE_VIDEO_IFRAME); } - // footnote render method delegation. - FootnoteNodeRendererInterceptor.doDelegationMethod(); Node document = PARSER.parse(markdown); diff --git a/src/main/java/run/halo/app/utils/footnotes/Footnote.java b/src/main/java/run/halo/app/utils/footnotes/Footnote.java new file mode 100644 index 0000000000..cb42eb0881 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/Footnote.java @@ -0,0 +1,140 @@ +package run.halo.app.utils.footnotes; + +import com.vladsch.flexmark.ast.LinkRendered; +import com.vladsch.flexmark.util.ast.DelimitedNode; +import com.vladsch.flexmark.util.ast.DoNotDecorate; +import com.vladsch.flexmark.util.ast.Document; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.ast.ReferencingNode; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import org.jetbrains.annotations.NotNull; +import run.halo.app.utils.footnotes.internal.FootnoteRepository; + +/** + * A Footnote referencing node + */ +public class Footnote extends Node implements DelimitedNode, DoNotDecorate, LinkRendered, + ReferencingNode { + protected BasedSequence openingMarker = BasedSequence.NULL; + protected BasedSequence text = BasedSequence.NULL; + protected BasedSequence closingMarker = BasedSequence.NULL; + protected FootnoteBlock footnoteBlock; + + public int getReferenceOrdinal() { + return referenceOrdinal; + } + + public void setReferenceOrdinal(int referenceOrdinal) { + this.referenceOrdinal = referenceOrdinal; + } + + protected int referenceOrdinal; + + @NotNull + @Override + public BasedSequence getReference() { + return text; + } + + @Override + public FootnoteBlock getReferenceNode(Document document) { + if (footnoteBlock != null || text.isEmpty()) { + return footnoteBlock; + } + footnoteBlock = getFootnoteBlock(FootnoteExtension.FOOTNOTES.get(document)); + return footnoteBlock; + } + + @Override + public FootnoteBlock getReferenceNode(FootnoteRepository repository) { + if (footnoteBlock != null || text.isEmpty()) { + return footnoteBlock; + } + footnoteBlock = getFootnoteBlock(repository); + return footnoteBlock; + } + + @Override + public boolean isDefined() { + return footnoteBlock != null; + } + + /** + * @return true if this node will be rendered as text because it depends on a reference which + * is not defined. + */ + @Override + public boolean isTentative() { + return footnoteBlock == null; + } + + public FootnoteBlock getFootnoteBlock(FootnoteRepository footnoteRepository) { + return text.isEmpty() ? null : footnoteRepository.get(text.toString()); + } + + public FootnoteBlock getFootnoteBlock() { + return footnoteBlock; + } + + public void setFootnoteBlock(FootnoteBlock footnoteBlock) { + this.footnoteBlock = footnoteBlock; + } + + @NotNull + @Override + public BasedSequence[] getSegments() { + return new BasedSequence[] {openingMarker, text, closingMarker}; + } + + @Override + public void getAstExtra(@NotNull StringBuilder out) { + out.append(" ordinal: ") + .append(footnoteBlock != null ? footnoteBlock.getFootnoteOrdinal() : 0).append(" "); + delimitedSegmentSpanChars(out, openingMarker, text, closingMarker, "text"); + } + + public Footnote() { + } + + public Footnote(BasedSequence chars) { + super(chars); + } + + public Footnote(BasedSequence openingMarker, BasedSequence text, BasedSequence closingMarker) { + super(openingMarker + .baseSubSequence(openingMarker.getStartOffset(), closingMarker.getEndOffset())); + this.openingMarker = openingMarker; + this.text = text; + this.closingMarker = closingMarker; + } + + @Override + public BasedSequence getOpeningMarker() { + return openingMarker; + } + + @Override + public void setOpeningMarker(BasedSequence openingMarker) { + this.openingMarker = openingMarker; + } + + @Override + public BasedSequence getText() { + return text; + } + + @Override + public void setText(BasedSequence text) { + this.text = text; + } + + @Override + public BasedSequence getClosingMarker() { + return closingMarker; + } + + @Override + public void setClosingMarker(BasedSequence closingMarker) { + this.closingMarker = closingMarker; + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/FootnoteBlock.java b/src/main/java/run/halo/app/utils/footnotes/FootnoteBlock.java new file mode 100644 index 0000000000..87cb50f7b8 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/FootnoteBlock.java @@ -0,0 +1,171 @@ +package run.halo.app.utils.footnotes; + +import com.vladsch.flexmark.ast.Paragraph; +import com.vladsch.flexmark.ast.ParagraphItemContainer; +import com.vladsch.flexmark.parser.ListOptions; +import com.vladsch.flexmark.util.ast.Block; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.ast.ReferenceNode; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import com.vladsch.flexmark.util.sequence.SequenceUtils; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import run.halo.app.utils.footnotes.internal.FootnoteRepository; + +/** + * A Footnote definition node containing text and other inline nodes nodes as children. + */ +public class FootnoteBlock extends Block + implements ReferenceNode, ParagraphItemContainer { + + protected BasedSequence openingMarker = BasedSequence.NULL; + protected BasedSequence text = BasedSequence.NULL; + protected BasedSequence closingMarker = BasedSequence.NULL; + protected BasedSequence footnote = BasedSequence.NULL; + private int footnoteOrdinal = 0; + private int firstReferenceOffset = Integer.MAX_VALUE; + private int footnoteReferences = 0; + + @Override + public int compareTo(FootnoteBlock other) { + return SequenceUtils.compare(text, other.text, true); + } + + public int getFootnoteReferences() { + return footnoteReferences; + } + + public void setFootnoteReferences(int footnoteReferences) { + this.footnoteReferences = footnoteReferences; + } + + @Nullable + @Override + public Footnote getReferencingNode(@NotNull Node node) { + return node instanceof Footnote ? (Footnote) node : null; + } + + public int getFirstReferenceOffset() { + return firstReferenceOffset; + } + + public void setFirstReferenceOffset(int firstReferenceOffset) { + this.firstReferenceOffset = firstReferenceOffset; + } + + public void addFirstReferenceOffset(int firstReferenceOffset) { + if (this.firstReferenceOffset < firstReferenceOffset) { + this.firstReferenceOffset = firstReferenceOffset; + } + } + + public boolean isReferenced() { + return this.firstReferenceOffset < Integer.MAX_VALUE; + } + + public int getFootnoteOrdinal() { + return footnoteOrdinal; + } + + public void setFootnoteOrdinal(int footnoteOrdinal) { + this.footnoteOrdinal = footnoteOrdinal; + } + + @Override + public void getAstExtra(@NotNull StringBuilder out) { + out.append(" ordinal: ").append(footnoteOrdinal).append(" "); + segmentSpan(out, openingMarker, "open"); + segmentSpan(out, text, "text"); + segmentSpan(out, closingMarker, "close"); + segmentSpan(out, footnote, "footnote"); + } + + @NotNull + @Override + public BasedSequence[] getSegments() { + return new BasedSequence[] {openingMarker, text, closingMarker, footnote}; + } + + public FootnoteBlock() { + } + + public FootnoteBlock(BasedSequence chars) { + super(chars); + } + + public BasedSequence getOpeningMarker() { + return openingMarker; + } + + public void setOpeningMarker(BasedSequence openingMarker) { + this.openingMarker = openingMarker; + } + + public BasedSequence getText() { + return text; + } + + public void setText(BasedSequence text) { + this.text = text; + } + + public BasedSequence getClosingMarker() { + return closingMarker; + } + + public void setClosingMarker(BasedSequence closingMarker) { + this.closingMarker = closingMarker; + } + + public BasedSequence getFootnote() { + return footnote; + } + + public void setFootnote(BasedSequence footnote) { + this.footnote = footnote; + } + + @Override + public boolean isItemParagraph(Paragraph node) { + return node == getFirstChild(); + } + + @Override + public boolean isParagraphWrappingDisabled(Paragraph node, ListOptions listOptions, + DataHolder options) { + return false; + } + + @Override + public boolean isParagraphInTightListItem(Paragraph node) { + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FootnoteBlock that = (FootnoteBlock) o; + return footnoteOrdinal == that.footnoteOrdinal + && firstReferenceOffset == that.firstReferenceOffset + && footnoteReferences == that.footnoteReferences + && Objects.equals(openingMarker, that.openingMarker) + && Objects.equals(text, that.text) + && Objects.equals(closingMarker, that.closingMarker) + && Objects.equals(footnote, that.footnote); + } + + @Override + public int hashCode() { + return Objects + .hash(openingMarker, text, closingMarker, footnote, footnoteOrdinal, + firstReferenceOffset, + footnoteReferences); + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/FootnoteExtension.java b/src/main/java/run/halo/app/utils/footnotes/FootnoteExtension.java new file mode 100644 index 0000000000..7f65b43884 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/FootnoteExtension.java @@ -0,0 +1,98 @@ +package run.halo.app.utils.footnotes; + +import com.vladsch.flexmark.formatter.Formatter; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.KeepType; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.DataKey; +import com.vladsch.flexmark.util.data.MutableDataHolder; +import com.vladsch.flexmark.util.format.options.ElementPlacement; +import com.vladsch.flexmark.util.format.options.ElementPlacementSort; +import org.jetbrains.annotations.NotNull; +import run.halo.app.utils.footnotes.internal.FootnoteBlockParser; +import run.halo.app.utils.footnotes.internal.FootnoteLinkRefProcessor; +import run.halo.app.utils.footnotes.internal.FootnoteNodeFormatter; +import run.halo.app.utils.footnotes.internal.FootnoteNodeRenderer; +import run.halo.app.utils.footnotes.internal.FootnoteRepository; + +/** + * Extension for footnotes + *

+ * Create it with {@link #create()} and then configure it on the builders + *

+ * The parsed footnote references in text regions are turned into {@link Footnote} nodes. The parsed + * footnote definitions are turned into {@link FootnoteBlock} nodes. + */ +public class FootnoteExtension + implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, + Parser.ReferenceHoldingExtension, Formatter.FormatterExtension { + + public static final DataKey FOOTNOTES_KEEP = + new DataKey<>("FOOTNOTES_KEEP", KeepType.FIRST); + + public static final DataKey FOOTNOTES = + new DataKey<>("FOOTNOTES", new FootnoteRepository(null), FootnoteRepository::new); + public static final DataKey FOOTNOTE_REF_PREFIX = + new DataKey<>("FOOTNOTE_REF_PREFIX", ""); + public static final DataKey FOOTNOTE_REF_SUFFIX = + new DataKey<>("FOOTNOTE_REF_SUFFIX", ""); + public static final DataKey FOOTNOTE_BACK_REF_STRING = + new DataKey<>("FOOTNOTE_BACK_REF_STRING", "↩"); + public static final DataKey FOOTNOTE_LINK_REF_CLASS = + new DataKey<>("FOOTNOTE_LINK_REF_CLASS", "footnote-ref"); + public static final DataKey FOOTNOTE_BACK_LINK_REF_CLASS = + new DataKey<>("FOOTNOTE_BACK_LINK_REF_CLASS", "footnote-backref"); + + // formatter options + public static final DataKey FOOTNOTE_PLACEMENT = + new DataKey<>("FOOTNOTE_PLACEMENT", ElementPlacement.AS_IS); + public static final DataKey FOOTNOTE_SORT = + new DataKey<>("FOOTNOTE_SORT", ElementPlacementSort.AS_IS); + + private FootnoteExtension() { + } + + public static FootnoteExtension create() { + return new FootnoteExtension(); + } + + @Override + public void extend(Formatter.Builder formatterBuilder) { + formatterBuilder.nodeFormatterFactory(new FootnoteNodeFormatter.Factory()); + } + + @Override + public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, + @NotNull String rendererType) { + if (htmlRendererBuilder.isRendererType("HTML")) { + htmlRendererBuilder.nodeRendererFactory(new FootnoteNodeRenderer.Factory()); + } + } + + @Override + public void extend(Parser.Builder parserBuilder) { + parserBuilder.customBlockParserFactory(new FootnoteBlockParser.Factory()); + parserBuilder.linkRefProcessorFactory(new FootnoteLinkRefProcessor.Factory()); + } + + @Override + public void rendererOptions(@NotNull MutableDataHolder options) { + + } + + @Override + public void parserOptions(MutableDataHolder options) { + + } + + @Override + public boolean transferReferences(MutableDataHolder document, DataHolder included) { + if (document.contains(FOOTNOTES) && included.contains(FOOTNOTES)) { + return Parser.transferReferences(FOOTNOTES.get(document), FOOTNOTES.get(included), + FOOTNOTES_KEEP.get(document) == KeepType.FIRST); + } + return false; + } + +} diff --git a/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitor.java b/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitor.java new file mode 100644 index 0000000000..2993e353d5 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitor.java @@ -0,0 +1,7 @@ +package run.halo.app.utils.footnotes; + +public interface FootnoteVisitor { + void visit(FootnoteBlock node); + + void visit(Footnote node); +} diff --git a/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitorExt.java b/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitorExt.java new file mode 100644 index 0000000000..da65a90d12 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/FootnoteVisitorExt.java @@ -0,0 +1,13 @@ +package run.halo.app.utils.footnotes; + +import com.vladsch.flexmark.util.ast.VisitHandler; + +public class FootnoteVisitorExt { + + public static VisitHandler[] visitHandlers(V visitor) { + return new VisitHandler[] { + new VisitHandler<>(FootnoteBlock.class, visitor::visit), + new VisitHandler<>(Footnote.class, visitor::visit), + }; + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteBlockParser.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteBlockParser.java new file mode 100644 index 0000000000..0ca6f6349c --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteBlockParser.java @@ -0,0 +1,168 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.parser.block.AbstractBlockParser; +import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory; +import com.vladsch.flexmark.parser.block.BlockContinue; +import com.vladsch.flexmark.parser.block.BlockParser; +import com.vladsch.flexmark.parser.block.BlockParserFactory; +import com.vladsch.flexmark.parser.block.BlockStart; +import com.vladsch.flexmark.parser.block.CustomBlockParserFactory; +import com.vladsch.flexmark.parser.block.MatchedBlockParser; +import com.vladsch.flexmark.parser.block.ParserState; +import com.vladsch.flexmark.util.ast.Block; +import com.vladsch.flexmark.util.ast.BlockContent; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import run.halo.app.utils.footnotes.FootnoteBlock; +import run.halo.app.utils.footnotes.FootnoteExtension; + +public class FootnoteBlockParser extends AbstractBlockParser { + + static String FOOTNOTE_ID = ".*"; + static Pattern FOOTNOTE_ID_PATTERN = Pattern.compile("\\[\\^\\s*(" + FOOTNOTE_ID + ")\\s*\\]"); + static Pattern FOOTNOTE_DEF_PATTERN = + Pattern.compile("^\\[\\^\\s*(" + FOOTNOTE_ID + ")\\s*\\]:"); + + private final FootnoteBlock block = new FootnoteBlock(); + private final FootnoteOptions options; + private final int contentOffset; + private BlockContent content = new BlockContent(); + + public FootnoteBlockParser(FootnoteOptions options, int contentOffset) { + this.options = options; + this.contentOffset = contentOffset; + } + + @Override + public BlockContent getBlockContent() { + return content; + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public BlockContinue tryContinue(ParserState state) { + final int nonSpaceIndex = state.getNextNonSpaceIndex(); + if (state.isBlank()) { + if (block.getFirstChild() == null) { + // Blank line after empty list item + return BlockContinue.none(); + } else { + return BlockContinue.atIndex(nonSpaceIndex); + } + } + + if (state.getIndent() >= options.contentIndent) { + int contentIndent = state.getIndex() + options.contentIndent; + return BlockContinue.atIndex(contentIndent); + } else { + return BlockContinue.none(); + } + } + + @Override + public void addLine(ParserState state, BasedSequence line) { + content.add(line, state.getIndent()); + } + + @Override + public void closeBlock(ParserState state) { + // set the footnote from closingMarker to end + block.setCharsFromContent(); + block.setFootnote(block.getChars() + .subSequence(block.getClosingMarker().getEndOffset() - block.getStartOffset()) + .trimStart()); + // add it to the map + FootnoteRepository footnoteMap = FootnoteExtension.FOOTNOTES.get(state.getProperties()); + footnoteMap.put(footnoteMap.normalizeKey(block.getText()), block); + content = null; + } + + @Override + public boolean isContainer() { + return true; + } + + @Override + public boolean canContain(ParserState state, BlockParser blockParser, Block block) { + return true; + } + + public static class Factory implements CustomBlockParserFactory { + + @Nullable + @Override + public Set> getAfterDependents() { + return null; + } + + @Nullable + @Override + public Set> getBeforeDependents() { + return null; + } + + @Override + public boolean affectsGlobalScope() { + return false; + } + + @NotNull + @Override + public BlockParserFactory apply(@NotNull DataHolder options) { + return new BlockFactory(options); + } + } + + private static class BlockFactory extends AbstractBlockParserFactory { + + private final FootnoteOptions options; + + private BlockFactory(DataHolder options) { + super(options); + this.options = new FootnoteOptions(options); + } + + @Override + public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { + if (state.getIndent() >= 4) { + return BlockStart.none(); + } + + BasedSequence line = state.getLine(); + int nextNonSpace = state.getNextNonSpaceIndex(); + + BasedSequence trySequence = line.subSequence(nextNonSpace, line.length()); + Matcher matcher = FOOTNOTE_DEF_PATTERN.matcher(trySequence); + if (matcher.find()) { + // abbreviation definition + int openingStart = nextNonSpace + matcher.start(); + int openingEnd = nextNonSpace + matcher.end(); + BasedSequence openingMarker = line.subSequence(openingStart, openingStart + 2); + BasedSequence text = line.subSequence(openingStart + 2, openingEnd - 2).trim(); + BasedSequence closingMarker = line.subSequence(openingEnd - 2, openingEnd); + + int contentOffset = options.contentIndent; + + FootnoteBlockParser footnoteBlockParser = + new FootnoteBlockParser(options, contentOffset); + footnoteBlockParser.block.setOpeningMarker(openingMarker); + footnoteBlockParser.block.setText(text); + footnoteBlockParser.block.setClosingMarker(closingMarker); + + return BlockStart.of(footnoteBlockParser) + .atIndex(openingEnd); + } else { + return BlockStart.none(); + } + } + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteFormatOptions.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteFormatOptions.java new file mode 100644 index 0000000000..a5eb79c4b3 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteFormatOptions.java @@ -0,0 +1,17 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.format.options.ElementPlacement; +import com.vladsch.flexmark.util.format.options.ElementPlacementSort; +import run.halo.app.utils.footnotes.FootnoteExtension; + +public class FootnoteFormatOptions { + + public final ElementPlacement footnotePlacement; + public final ElementPlacementSort footnoteSort; + + public FootnoteFormatOptions(DataHolder options) { + footnotePlacement = FootnoteExtension.FOOTNOTE_PLACEMENT.get(options); + footnoteSort = FootnoteExtension.FOOTNOTE_SORT.get(options); + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteLinkRefProcessor.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteLinkRefProcessor.java new file mode 100644 index 0000000000..c6ec6722ba --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteLinkRefProcessor.java @@ -0,0 +1,94 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.parser.LinkRefProcessor; +import com.vladsch.flexmark.parser.LinkRefProcessorFactory; +import com.vladsch.flexmark.util.ast.Document; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import org.jetbrains.annotations.NotNull; +import run.halo.app.utils.footnotes.Footnote; +import run.halo.app.utils.footnotes.FootnoteBlock; +import run.halo.app.utils.footnotes.FootnoteExtension; + +public class FootnoteLinkRefProcessor implements LinkRefProcessor { + + static final boolean WANT_EXCLAMATION_PREFIX = false; + static final int BRACKET_NESTING_LEVEL = 0; + + private final FootnoteRepository footnoteRepository; + + public FootnoteLinkRefProcessor(Document document) { + this.footnoteRepository = FootnoteExtension.FOOTNOTES.get(document); + } + + @Override + public boolean getWantExclamationPrefix() { + return WANT_EXCLAMATION_PREFIX; + } + + @Override + public int getBracketNestingLevel() { + return BRACKET_NESTING_LEVEL; + } + + @Override + public boolean isMatch(@NotNull BasedSequence nodeChars) { + return nodeChars.length() >= 3 && nodeChars.charAt(0) == '[' && nodeChars.charAt(1) == '^' + && nodeChars.endCharAt(1) == ']'; + } + + @NotNull + @Override + public Node createNode(@NotNull BasedSequence nodeChars) { + BasedSequence footnoteId = nodeChars.midSequence(2, -1).trim(); + FootnoteBlock footnoteBlock = + footnoteId.length() > 0 ? footnoteRepository.get(footnoteId.toString()) : null; + + Footnote footnote = + new Footnote(nodeChars.subSequence(0, 2), footnoteId, nodeChars.endSequence(1)); + footnote.setFootnoteBlock(footnoteBlock); + + if (footnoteBlock != null) { + footnoteRepository.addFootnoteReference(footnoteBlock, footnote); + } + return footnote; + } + + @NotNull + @Override + public BasedSequence adjustInlineText(@NotNull Document document, @NotNull Node node) { + assert node instanceof Footnote; + return ((Footnote) node).getText(); + } + + @Override + public boolean allowDelimiters(@NotNull BasedSequence chars, @NotNull Document document, + @NotNull Node node) { + return true; + } + + @Override + public void updateNodeElements(@NotNull Document document, @NotNull Node node) { + + } + + public static class Factory implements LinkRefProcessorFactory { + + @NotNull + @Override + public LinkRefProcessor apply(@NotNull Document document) { + return new FootnoteLinkRefProcessor(document); + } + + @Override + public boolean getWantExclamationPrefix(@NotNull DataHolder options) { + return WANT_EXCLAMATION_PREFIX; + } + + @Override + public int getBracketNestingLevel(@NotNull DataHolder options) { + return BRACKET_NESTING_LEVEL; + } + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeFormatter.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeFormatter.java new file mode 100644 index 0000000000..f6d7eeec02 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeFormatter.java @@ -0,0 +1,109 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.formatter.MarkdownWriter; +import com.vladsch.flexmark.formatter.NodeFormatter; +import com.vladsch.flexmark.formatter.NodeFormatterContext; +import com.vladsch.flexmark.formatter.NodeFormatterFactory; +import com.vladsch.flexmark.formatter.NodeFormattingHandler; +import com.vladsch.flexmark.formatter.NodeRepositoryFormatter; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.DataKey; +import com.vladsch.flexmark.util.format.options.ElementPlacement; +import com.vladsch.flexmark.util.format.options.ElementPlacementSort; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import run.halo.app.utils.footnotes.Footnote; +import run.halo.app.utils.footnotes.FootnoteBlock; +import run.halo.app.utils.footnotes.FootnoteExtension; + +public class FootnoteNodeFormatter + extends NodeRepositoryFormatter { + + public static final DataKey> FOOTNOTE_TRANSLATION_MAP = + new DataKey<>("FOOTNOTE_TRANSLATION_MAP", new HashMap<>()); // translated references + public static final DataKey> FOOTNOTE_UNIQUIFICATION_MAP = + new DataKey<>("FOOTNOTE_UNIQUIFICATION_MAP", new HashMap<>()); // uniquified references + private final FootnoteFormatOptions options; + + public FootnoteNodeFormatter(DataHolder options) { + super(options, FOOTNOTE_TRANSLATION_MAP, FOOTNOTE_UNIQUIFICATION_MAP); + this.options = new FootnoteFormatOptions(options); + } + + @Override + public FootnoteRepository getRepository(DataHolder options) { + return FootnoteExtension.FOOTNOTES.get(options); + } + + @Override + public ElementPlacement getReferencePlacement() { + return options.footnotePlacement; + } + + @Override + public ElementPlacementSort getReferenceSort() { + return options.footnoteSort; + } + + @Override + public void renderReferenceBlock(FootnoteBlock node, NodeFormatterContext context, + MarkdownWriter markdown) { + markdown.blankLine().append("[^"); + markdown.append(transformReferenceId(node.getText().toString(), context)); + markdown.append("]: "); + markdown.pushPrefix().addPrefix(" "); + context.renderChildren(node); + markdown.popPrefix(); + markdown.blankLine(); + } + + @Nullable + @Override + public Set> getNodeFormattingHandlers() { + return new HashSet<>(Arrays.asList( + new NodeFormattingHandler<>(Footnote.class, FootnoteNodeFormatter.this::render), + new NodeFormattingHandler<>(FootnoteBlock.class, FootnoteNodeFormatter.this::render) + )); + } + + @Nullable + @Override + public Set> getNodeClasses() { + if (options.footnotePlacement.isNoChange() || !options.footnoteSort.isUnused()) { + return null; + } + // noinspection ArraysAsListWithZeroOrOneArgument + return new HashSet<>(Arrays.asList( + Footnote.class + )); + } + + private void render(FootnoteBlock node, NodeFormatterContext context, MarkdownWriter markdown) { + renderReference(node, context, markdown); + } + + private void render(Footnote node, NodeFormatterContext context, MarkdownWriter markdown) { + markdown.append("[^"); + if (context.isTransformingText()) { + String referenceId = transformReferenceId(node.getText().toString(), context); + context.nonTranslatingSpan((context1, markdown1) -> markdown1.append(referenceId)); + } else { + markdown.append(node.getText()); + } + markdown.append("]"); + } + + public static class Factory implements NodeFormatterFactory { + + @NotNull + @Override + public NodeFormatter create(@NotNull DataHolder options) { + return new FootnoteNodeFormatter(options); + } + } +} diff --git a/src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeRenderer.java similarity index 52% rename from src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java rename to src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeRenderer.java index 695447052e..0d48d56270 100644 --- a/src/main/java/run/halo/app/utils/FootnoteNodeRendererInterceptor.java +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteNodeRenderer.java @@ -1,122 +1,60 @@ -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; +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.html.HtmlWriter; +import com.vladsch.flexmark.html.renderer.NodeRenderer; import com.vladsch.flexmark.html.renderer.NodeRendererContext; +import com.vladsch.flexmark.html.renderer.NodeRendererFactory; +import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; +import com.vladsch.flexmark.html.renderer.PhasedNodeRenderer; 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.data.DataHolder; import com.vladsch.flexmark.util.sequence.BasedSequence; -import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; 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 java.util.Set; 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()); - } +import org.jetbrains.annotations.NotNull; +import run.halo.app.utils.footnotes.Footnote; +import run.halo.app.utils.footnotes.FootnoteBlock; +import run.halo.app.utils.footnotes.FootnoteExtension; - /** - * 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(); +public class FootnoteNodeRenderer implements PhasedNodeRenderer { - 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"); - }); - } + private final FootnoteRepository footnoteRepository; + private final FootnoteOptions options; + private final boolean recheckUndefinedReferences; + + public FootnoteNodeRenderer(DataHolder options) { + this.options = new FootnoteOptions(options); + this.footnoteRepository = FootnoteExtension.FOOTNOTES.get(options); + this.recheckUndefinedReferences = HtmlRenderer.RECHECK_UNDEFINED_REFERENCES.get(options); + this.footnoteRepository.resolveFootnoteOrdinals(); } - /** - * 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)); + @Override + public Set> getNodeRenderingHandlers() { + return new HashSet<>(Arrays.asList( + new NodeRenderingHandler<>(Footnote.class, this::render), + new NodeRenderingHandler<>(FootnoteBlock.class, this::render) + )); + } + @Override + public Set getRenderingPhases() { + Set set = new HashSet<>(); + set.add(RenderingPhase.BODY_TOP); + set.add(RenderingPhase.BODY_BOTTOM); + return set; + } + + @Override + public void renderDocument(@NotNull NodeRendererContext context, @NotNull HtmlWriter html, + @NotNull Document document, @NotNull RenderingPhase phase) { if (phase == RenderingPhase.BODY_TOP) { if (recheckUndefinedReferences) { // need to see if have undefined footnotes that were defined after parsing @@ -138,7 +76,7 @@ public static void renderDocument(@FieldValue("footnoteRepository") visitor.visit(document); if (hadNewFootnotes[0]) { - footnoteRepository.resolveFootnoteOrdinals(); + this.footnoteRepository.resolveFootnoteOrdinals(); } } } @@ -166,11 +104,14 @@ public static void renderDocument(@FieldValue("footnoteRepository") sb.append(" ").append(footnoteBackRefString).append(""); + sb.append(">").append(options.footnoteBackRefString) + .append(""); html.setLine(html.getLineCount() - 1, "", line.insert(line.lastIndexOf(" { + // 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"); + }); + } + } + + public static class Factory implements NodeRendererFactory { + + @NotNull + @Override + public NodeRenderer apply(@NotNull DataHolder options) { + return new FootnoteNodeRenderer(options); } - return value; } } diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteOptions.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteOptions.java new file mode 100644 index 0000000000..fcfbe92ab1 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteOptions.java @@ -0,0 +1,24 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.DataHolder; +import run.halo.app.utils.footnotes.FootnoteExtension; + +public class FootnoteOptions { + + final String footnoteRefPrefix; + final String footnoteRefSuffix; + final String footnoteBackRefString; + final String footnoteLinkRefClass; + final String footnoteBackLinkRefClass; + final int contentIndent; + + public FootnoteOptions(DataHolder options) { + this.footnoteRefPrefix = FootnoteExtension.FOOTNOTE_REF_PREFIX.get(options); + this.footnoteRefSuffix = FootnoteExtension.FOOTNOTE_REF_SUFFIX.get(options); + this.footnoteBackRefString = FootnoteExtension.FOOTNOTE_BACK_REF_STRING.get(options); + this.footnoteLinkRefClass = FootnoteExtension.FOOTNOTE_LINK_REF_CLASS.get(options); + this.footnoteBackLinkRefClass = FootnoteExtension.FOOTNOTE_BACK_LINK_REF_CLASS.get(options); + this.contentIndent = Parser.LISTS_ITEM_INDENT.get(options); + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteRepository.java b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteRepository.java new file mode 100644 index 0000000000..0aca257ce0 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/internal/FootnoteRepository.java @@ -0,0 +1,109 @@ +package run.halo.app.utils.footnotes.internal; + +import com.vladsch.flexmark.util.ast.Document; +import com.vladsch.flexmark.util.ast.KeepType; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.ast.NodeRepository; +import com.vladsch.flexmark.util.ast.NodeVisitor; +import com.vladsch.flexmark.util.ast.VisitHandler; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.DataKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import run.halo.app.utils.footnotes.Footnote; +import run.halo.app.utils.footnotes.FootnoteBlock; +import run.halo.app.utils.footnotes.FootnoteExtension; + +@SuppressWarnings("WeakerAccess") +public class FootnoteRepository extends NodeRepository { + + private final ArrayList referencedFootnoteBlocks = new ArrayList<>(); + + public static void resolveFootnotes(Document document) { + FootnoteRepository footnoteRepository = FootnoteExtension.FOOTNOTES.get(document); + + 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(); + } + } + + public void addFootnoteReference(FootnoteBlock footnoteBlock, Footnote footnote) { + if (!footnoteBlock.isReferenced()) { + referencedFootnoteBlocks.add(footnoteBlock); + } + + footnoteBlock.setFirstReferenceOffset(footnote.getStartOffset()); + + int referenceOrdinal = footnoteBlock.getFootnoteReferences(); + footnoteBlock.setFootnoteReferences(referenceOrdinal + 1); + footnote.setReferenceOrdinal(referenceOrdinal); + } + + public void resolveFootnoteOrdinals() { + // need to sort by first referenced offset then set each to its ordinal position in the + // array+1 + Collections.sort(referencedFootnoteBlocks, + (f1, f2) -> f1.getFirstReferenceOffset() - f2.getFirstReferenceOffset()); + + int ordinal = 0; + for (FootnoteBlock footnoteBlock : referencedFootnoteBlocks) { + footnoteBlock.setFootnoteOrdinal(++ordinal); + } + } + + public List getReferencedFootnoteBlocks() { + return referencedFootnoteBlocks; + } + + public FootnoteRepository(DataHolder options) { + super(FootnoteExtension.FOOTNOTES_KEEP.get(options)); + } + + @NotNull + @Override + public DataKey getDataKey() { + return FootnoteExtension.FOOTNOTES; + } + + @NotNull + @Override + public DataKey getKeepDataKey() { + return FootnoteExtension.FOOTNOTES_KEEP; + } + + @NotNull + @Override + public Set getReferencedElements(Node parent) { + HashSet references = new HashSet<>(); + visitNodes(parent, value -> { + if (value instanceof Footnote) { + FootnoteBlock reference = + ((Footnote) value).getReferenceNode(FootnoteRepository.this); + if (reference != null) { + references.add(reference); + } + } + }, Footnote.class); + return references; + } +} diff --git a/src/main/java/run/halo/app/utils/footnotes/package-info.java b/src/main/java/run/halo/app/utils/footnotes/package-info.java new file mode 100644 index 0000000000..daf5455044 --- /dev/null +++ b/src/main/java/run/halo/app/utils/footnotes/package-info.java @@ -0,0 +1,9 @@ +package run.halo.app.utils.footnotes; +/* + * This package uses {@link https://github.com/vsch/flexmark-java/tree/master/flexmark-ext-footnotes} + * In order to solve the rendering inconsistent of between flexmark and marked-it. + * Deprecated on 1.5.x version + * + * @author guqing + * @since 1.4.9 + */ \ No newline at end of file diff --git a/src/test/java/run/halo/app/utils/FootnoteNodeRendererInterceptorTest.java b/src/test/java/run/halo/app/utils/FootnoteTest.java similarity index 97% rename from src/test/java/run/halo/app/utils/FootnoteNodeRendererInterceptorTest.java rename to src/test/java/run/halo/app/utils/FootnoteTest.java index b67b565cb3..fd95324027 100644 --- a/src/test/java/run/halo/app/utils/FootnoteNodeRendererInterceptorTest.java +++ b/src/test/java/run/halo/app/utils/FootnoteTest.java @@ -1,12 +1,9 @@ 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; @@ -15,9 +12,10 @@ import java.util.Arrays; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Test; +import run.halo.app.utils.footnotes.FootnoteExtension; /** - * Compare the rendering result of FootnoteNodeRendererInterceptor + * Compare the rendering result of FootnoteNodeRenderer * and markdown-it-footnote. * You can view markdown-it-footnote's rendering HTML results on this * link markdown-it-footnote example page. @@ -25,7 +23,7 @@ * @author guqing * @date 2021-06-26 */ -public class FootnoteNodeRendererInterceptorTest { +public class FootnoteTest { private static final DataHolder OPTIONS = new MutableDataSet().set(Parser.EXTENSIONS, Arrays.asList(EmojiExtension.create(), FootnoteExtension.create())) @@ -38,7 +36,6 @@ public class FootnoteNodeRendererInterceptorTest { private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build(); private String renderHtml(String markdown) { - FootnoteNodeRendererInterceptor.doDelegationMethod(); Node document = PARSER.parse(markdown);