mirror of https://github.com/halo-dev/halo
Merge branch 'master' of github.com:halo-dev/halo
commit
5e656fe06b
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -1,6 +1,34 @@
|
|||
# CHANGELOG
|
||||
|
||||
# 1.4.9
|
||||
# 1.4.10
|
||||
|
||||
## Features
|
||||
|
||||
- 编辑器支持脚注语法。halo-dev/halo#1406 halo-dev/halo-admin#341
|
||||
|
||||
## Improvements
|
||||
|
||||
- 优化文章字数计算。halo-dev/halo#1354
|
||||
- Content Api 中的获取文章列表支持传入关键字和分类 id 筛选项。halo-dev/halo#1373
|
||||
- 优化导入 Markdown 时,对多级分类的处理。halo-dev/halo#1380
|
||||
|
||||
## Security Fixes
|
||||
|
||||
- 修复 Freemarker SSTI 漏洞。halo-dev/halo#1402 halo-dev/halo#1427 Thanks @LazyMaple @5wimming
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- 修复在分类文章列表可以显示私密文章的问题。halo-dev/halo#1379
|
||||
- 修复使用后台的小工具数据导出迁移后分类密码消失的问题。halo-dev/halo#1390
|
||||
- 修复在站点初始化的时候,`全局绝对路径` 选项设置错误的问题。halo-dev/halo#1396
|
||||
- 修复 Content Api 的文章点赞接口限流没有按照文章 id 做处理的问题。halo-dev/halo#1410
|
||||
- 修复回收站的文章可以访问的问题。halo-dev/halo#1414
|
||||
- 修复后台评论回复时,输入框无法输入空格的问题。halo-dev/halo-admin#322
|
||||
- 修复后台菜单管理中菜单项的链接过长会导致挡住操作按钮的问题。halo-dev/halo-admin#328
|
||||
- 修复后台日志管理中长文本无法换行的问题。halo-dev/halo-admin#330
|
||||
- 修复后台在登录页面无法通过回车键进行登录的问题。halo-dev/halo-admin#332
|
||||
|
||||
# 1.4.9(deprecated)
|
||||
|
||||
## Features
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
下载最新的 Halo 运行包:
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/halo-dev/halo/releases/download/v1.4.9/halo-1.4.9.jar --output halo.jar
|
||||
curl -L https://github.com/halo-dev/halo/releases/download/v1.4.10/halo-1.4.10.jar --output halo.jar
|
||||
```
|
||||
|
||||
其他地址:https://docs.halo.run/install/downloads
|
||||
|
|
|
@ -6,7 +6,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = "run.halo.app"
|
||||
version = "1.4.9"
|
||||
version = "1.4.10"
|
||||
description = "Halo, An excellent open source blog publishing application."
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
|
@ -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"
|
||||
|
|
|
@ -194,6 +194,7 @@ public class LocalFileHandler implements FileHandler {
|
|||
boolean deleteResult = Files.deleteIfExists(thumbnailPath);
|
||||
if (!deleteResult) {
|
||||
log.warn("Thumbnail: [{}] may not exist", thumbnailPath.toString());
|
||||
throw new FileOperationException("附件缩略图 " + thumbnailName + " 删除失败");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new FileOperationException("附件缩略图 " + thumbnailName + " 删除失败", e);
|
||||
|
|
|
@ -185,6 +185,7 @@ public class QiniuOssFileHandler implements FileHandler {
|
|||
Response response = bucketManager.delete(bucket, key);
|
||||
if (!response.isOK()) {
|
||||
log.warn("附件 " + key + " 从七牛云删除失败");
|
||||
throw new FileOperationException("附件 " + key + " 从七牛云删除失败");
|
||||
}
|
||||
} catch (QiniuException e) {
|
||||
log.error("Qiniu oss error response: [{}]", e.response);
|
||||
|
|
|
@ -132,6 +132,7 @@ public class UpOssFileHandler implements FileHandler {
|
|||
Response result = manager.deleteFile(key, null);
|
||||
if (!result.isSuccessful()) {
|
||||
log.warn("附件 " + key + " 从又拍云删除失败");
|
||||
throw new FileOperationException("附件 " + key + " 从又拍云删除失败");
|
||||
}
|
||||
} catch (IOException | UpException e) {
|
||||
e.printStackTrace();
|
||||
|
|
|
@ -6,7 +6,6 @@ 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;
|
||||
|
@ -30,6 +29,7 @@ import java.util.regex.Pattern;
|
|||
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 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,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<FootnoteRepository, FootnoteBlock> {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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<FootnoteRepository, FootnoteBlock, Footnote>, 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* <p>
|
||||
* Create it with {@link #create()} and then configure it on the builders
|
||||
* <p>
|
||||
* 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<KeepType> FOOTNOTES_KEEP =
|
||||
new DataKey<>("FOOTNOTES_KEEP", KeepType.FIRST);
|
||||
|
||||
public static final DataKey<FootnoteRepository> FOOTNOTES =
|
||||
new DataKey<>("FOOTNOTES", new FootnoteRepository(null), FootnoteRepository::new);
|
||||
public static final DataKey<String> FOOTNOTE_REF_PREFIX =
|
||||
new DataKey<>("FOOTNOTE_REF_PREFIX", "");
|
||||
public static final DataKey<String> FOOTNOTE_REF_SUFFIX =
|
||||
new DataKey<>("FOOTNOTE_REF_SUFFIX", "");
|
||||
public static final DataKey<String> FOOTNOTE_BACK_REF_STRING =
|
||||
new DataKey<>("FOOTNOTE_BACK_REF_STRING", "↩");
|
||||
public static final DataKey<String> FOOTNOTE_LINK_REF_CLASS =
|
||||
new DataKey<>("FOOTNOTE_LINK_REF_CLASS", "footnote-ref");
|
||||
public static final DataKey<String> FOOTNOTE_BACK_LINK_REF_CLASS =
|
||||
new DataKey<>("FOOTNOTE_BACK_LINK_REF_CLASS", "footnote-backref");
|
||||
|
||||
// formatter options
|
||||
public static final DataKey<ElementPlacement> FOOTNOTE_PLACEMENT =
|
||||
new DataKey<>("FOOTNOTE_PLACEMENT", ElementPlacement.AS_IS);
|
||||
public static final DataKey<ElementPlacementSort> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package run.halo.app.utils.footnotes;
|
||||
|
||||
public interface FootnoteVisitor {
|
||||
void visit(FootnoteBlock node);
|
||||
|
||||
void visit(Footnote node);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package run.halo.app.utils.footnotes;
|
||||
|
||||
import com.vladsch.flexmark.util.ast.VisitHandler;
|
||||
|
||||
public class FootnoteVisitorExt {
|
||||
|
||||
public static <V extends FootnoteVisitor> VisitHandler<?>[] visitHandlers(V visitor) {
|
||||
return new VisitHandler<?>[] {
|
||||
new VisitHandler<>(FootnoteBlock.class, visitor::visit),
|
||||
new VisitHandler<>(Footnote.class, visitor::visit),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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<Class<?>> getAfterDependents() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Set<Class<?>> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<FootnoteRepository, FootnoteBlock, Footnote> {
|
||||
|
||||
public static final DataKey<Map<String, String>> FOOTNOTE_TRANSLATION_MAP =
|
||||
new DataKey<>("FOOTNOTE_TRANSLATION_MAP", new HashMap<>()); // translated references
|
||||
public static final DataKey<Map<String, String>> 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<NodeFormattingHandler<?>> getNodeFormattingHandlers() {
|
||||
return new HashSet<>(Arrays.asList(
|
||||
new NodeFormattingHandler<>(Footnote.class, FootnoteNodeFormatter.this::render),
|
||||
new NodeFormattingHandler<>(FootnoteBlock.class, FootnoteNodeFormatter.this::render)
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Set<Class<?>> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,122 +1,60 @@
|
|||
package run.halo.app.utils;
|
||||
package run.halo.app.utils.footnotes.internal;
|
||||
|
||||
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.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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* <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 {
|
||||
public class FootnoteNodeRenderer implements PhasedNodeRenderer {
|
||||
|
||||
/**
|
||||
* Delegate the render method to intercept the FootNoteNodeRender by ByteBuddy runtime.
|
||||
*/
|
||||
public static void doDelegationMethod() {
|
||||
ByteBuddyAgent.install();
|
||||
new ByteBuddy()
|
||||
.redefine(FootnoteNodeRenderer.class)
|
||||
private final FootnoteRepository footnoteRepository;
|
||||
private final FootnoteOptions options;
|
||||
private final boolean recheckUndefinedReferences;
|
||||
|
||||
.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());
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
|
||||
return new HashSet<>(Arrays.asList(
|
||||
new NodeRenderingHandler<>(Footnote.class, this::render),
|
||||
new NodeRenderingHandler<>(FootnoteBlock.class, this::render)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<RenderingPhase> getRenderingPhases() {
|
||||
Set<RenderingPhase> 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 class FootnoteNodeRendererInterceptor {
|
|||
|
||||
visitor.visit(document);
|
||||
if (hadNewFootnotes[0]) {
|
||||
footnoteRepository.resolveFootnoteOrdinals();
|
||||
this.footnoteRepository.resolveFootnoteOrdinals();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,11 +104,14 @@ public class FootnoteNodeRendererInterceptor {
|
|||
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)
|
||||
if (StringUtils
|
||||
.isNotBlank(options.footnoteBackLinkRefClass)) {
|
||||
sb.append(" class=\"")
|
||||
.append(options.footnoteBackLinkRefClass)
|
||||
.append("\"");
|
||||
}
|
||||
sb.append(">").append(footnoteBackRefString).append("</a>");
|
||||
sb.append(">").append(options.footnoteBackRefString)
|
||||
.append("</a>");
|
||||
html.setLine(html.getLineCount() - 1, "",
|
||||
line.insert(line.lastIndexOf("</p"), sb.toString()));
|
||||
}
|
||||
|
@ -179,11 +120,12 @@ public class FootnoteNodeRendererInterceptor {
|
|||
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);
|
||||
if (StringUtils
|
||||
.isNotBlank(options.footnoteBackLinkRefClass)) {
|
||||
html.attr("class", options.footnoteBackLinkRefClass);
|
||||
}
|
||||
html.line().withAttr().tag("a");
|
||||
html.raw(footnoteBackRefString);
|
||||
html.raw(options.footnoteBackRefString);
|
||||
html.tag("/a");
|
||||
}
|
||||
}
|
||||
|
@ -195,25 +137,44 @@ public class FootnoteNodeRendererInterceptor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
private void render(FootnoteBlock node, NodeRendererContext context, HtmlWriter html) {
|
||||
|
||||
Object value = null;
|
||||
try {
|
||||
Field field = FootnoteOptions.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
value = field.get(options);
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
private 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements NodeRendererFactory {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public NodeRenderer apply(@NotNull DataHolder options) {
|
||||
return new FootnoteNodeRenderer(options);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<FootnoteBlock> {
|
||||
|
||||
private final ArrayList<FootnoteBlock> 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<FootnoteBlock> getReferencedFootnoteBlocks() {
|
||||
return referencedFootnoteBlocks;
|
||||
}
|
||||
|
||||
public FootnoteRepository(DataHolder options) {
|
||||
super(FootnoteExtension.FOOTNOTES_KEEP.get(options));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataKey<FootnoteRepository> getDataKey() {
|
||||
return FootnoteExtension.FOOTNOTES;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataKey<KeepType> getKeepDataKey() {
|
||||
return FootnoteExtension.FOOTNOTES_KEEP;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<FootnoteBlock> getReferencedElements(Node parent) {
|
||||
HashSet<FootnoteBlock> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*/
|
File diff suppressed because one or more lines are too long
|
@ -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 com.vladsch.flexmark.util.data.MutableDataSet;
|
|||
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 <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>.
|
||||
|
@ -25,7 +23,7 @@ import org.junit.jupiter.api.Test;
|
|||
* @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);
|
||||
|
Loading…
Reference in New Issue