java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Markdown语法转HTML标签

SpringBoot实现Markdown语法转HTML标签的详细步骤

作者:无缘之缘

这篇文章主要介绍了SpringBoot实现Markdown转HTML的步骤,涵盖项目配置、测试语法扩展(自动链接、表情、表格等)、解决GitLab和脚注问题,并添加自定义解析器以确保正确解析,需要的朋友可以参考下

1、项目启动

pom.xml文件

	<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.1</version>
    </parent>
    <dependencies>
        <!--  将 Markdown 转换为 HTML  -->
        <dependency>
            <groupId>com.vladsch.flexmark</groupId>
            <artifactId>flexmark-all</artifactId>
            <version>0.62.2</version>
        </dependency>

        <!--以下为基础依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

核心文件

public class MarkdownConverter {
    // 定义一个静态方法,将 Markdown 文本转换为 HTML
    public static String markdownToHtml(String markdown) {
        // 创建一个 MutableDataSet 对象来配置 Markdown 解析器的选项
        MutableDataSet options = new MutableDataSet();

        // 添加各种 Markdown 解析器的扩展
        options.set(Parser.EXTENSIONS, Arrays.asList(
                AutolinkExtension.create(),     // 自动链接扩展,将URL文本转换为链接(如 http://example.com)自动变为 <a href="..." rel="external nofollow" >
                EmojiExtension.create(),        // 表情符号扩展,用于解析表情符号(如 :smile: 转换为 😄)
                GitLabExtension.create(),       // GitLab特有的Markdown扩展(如支持 ~~删除线~~)
                FootnoteExtension.create(),     // 脚注扩展,用于添加和解析脚注(如 [^1])
                TaskListExtension.create(),     // 任务列表扩展,用于创建任务列表(如 - [x] 已完成)
//                CustomAdmonitionExtension.create(),   // 提示框扩展,用于创建提示框(如 !!! note 创建提示框)
                TablesExtension.create()));     // 表格扩展,用于解析和渲染表格(如 | 表头1 | 表头2 |)


        // 使用配置的选项构建一个 Markdown 解析器
        Parser parser = Parser.builder(options).build();//Markdown-->抽象语法树
        // 使用相同的选项构建一个 HTML 渲染器
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();//抽象语法树-->HTML

        // 解析传入的 Markdown 文本并将其渲染为 HTML
        return renderer.render(parser.parse(markdown));
    }
}

controller代码

@RestController
public class MarkdownController {

    @PostMapping("/test")
    public String markdownToHtml(String content) {
        return MarkdownConverter.markdownToHtml(content);
    }
}

启动入口

@SpringBootApplication
public class RunApplication {
    public static void main(String[] args) {
        SpringApplication.run(RunApplication.class, args);
    }
}

2、运行测试

2.1 基础markdown语法测试

输入内容

# 标题1

## 标题2

**粗体文本**

*斜体文本*

`行内代码`

> 引用文本

结果如下

2.2 自动链接扩展测试文本

输入内容

访问我们的网站 http://example.com 获取更多信息

结果如下

2.3 表情符号扩展测试文本

输入内容

这是一个微笑表情 :smile: 和大笑表情 :laughing:

结果如下

2.4 GitLab扩展测试文本

输入内容:

这是删除线文本 ~~删除线~~

结果如下:

这里看到删除线并不能正确解析,该问题待解决

2.5 脚注扩展测试文本

输入内容:

这是一个带有脚注的文本[^1].
[^1]: 这是脚注的内容。

结果如下:

2.6 任务列表扩展测试文本

输入内容:

- [x] 已完成的任务
- [ ] 未完成的任务

结果如下:

2.7 表格扩展测试文本

输入内容:

| 表头1 | 表头2 |
|-------|-------|
| 内容1 | 内容2 |

结果如下:

3、自定义解析器

我们想解析

!!! note
    这是一个提示框的内容

3.1 自定义解析器

增加以下两个类

public class CustomAdmonitionBlockParser extends AbstractBlockParser {
    final private static String ADMONITION_START_FORMAT = "^(\\?{3}\\+|\\?{3}|!{3}|:{3})\\s*(%s)(?:\\s+(%s))?\\s*$";

    final AdmonitionBlock block;
    //private BlockContent content = new BlockContent();
    final private AdmonitionOptions options;
    final private int contentIndent;
    private boolean hadBlankLine;
    private boolean isOver;

    CustomAdmonitionBlockParser(AdmonitionOptions options, int contentIndent) {
        this.options = options;
        this.contentIndent = contentIndent;
        this.block = new AdmonitionBlock();
    }

    private int getContentIndent() {
        return contentIndent;
    }

    @Override
    public Block getBlock() {
        return block;
    }

    @Override
    public boolean isContainer() {
        return true;
    }

    @Override
    public boolean canContain(ParserState state, BlockParser blockParser, final Block block) {
        return true;
    }

    @Override
    public BlockContinue tryContinue(ParserState state) {
        // 获取当前行内容
        BasedSequence line = state.getLine();
        final int nonSpaceIndex = state.getNextNonSpaceIndex();

        // 判断是否是终止符 "!!!"
        if (isOver) {
            return BlockContinue.none();
        }

        if (line.startsWith("!!!") || line.startsWith("???") || line.startsWith(":::")) {
            isOver = true;// 停止解析
        }

        // 如果当前行是空行,则继续解析,同时标记块中出现过空行
        if (state.isBlank()) {
            hadBlankLine = true;
            return BlockContinue.atIndex(nonSpaceIndex);
        }

        // 如果允许懒惰继续(lazy continuation),且未遇到空行
        if (!hadBlankLine && options.allowLazyContinuation) {
            return BlockContinue.atIndex(nonSpaceIndex);
        }

        // 如果缩进足够,则继续解析当前行
        if (state.getIndent() >= options.contentIndent) {
            int contentIndent = state.getColumn() + options.contentIndent;
            return BlockContinue.atColumn(contentIndent);
        }

        // 默认情况,继续解析当前行
        return BlockContinue.atIndex(nonSpaceIndex);
    }

    @Override
    public void closeBlock(ParserState state) {
        block.setCharsFromContent();
    }

    public static class Factory implements CustomBlockParserFactory {
        @Nullable
        @Override
        public Set<Class<?>> getAfterDependents() {
            return null;
        }

        @Nullable
        @Override
        public Set<Class<?>> getBeforeDependents() {
            return null;
        }

        @Override
        public @Nullable SpecialLeadInHandler getLeadInHandler(@NotNull DataHolder options) {
            return AdmonitionLeadInHandler.HANDLER;
        }

        @Override
        public boolean affectsGlobalScope() {
            return false;
        }

        @NotNull
        @Override
        public BlockParserFactory apply(@NotNull DataHolder options) {
            return new BlockFactory(options);
        }
    }

    static class AdmonitionLeadInHandler implements SpecialLeadInHandler {
        final static SpecialLeadInHandler HANDLER = new AdmonitionLeadInHandler();

        @Override
        public boolean escape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer<CharSequence> consumer) {
            if ((sequence.length() == 3 || sequence.length() == 4 && sequence.charAt(3) == '+') && (sequence.startsWith("???") || sequence.startsWith("!!!") || sequence.startsWith(":::"))) {
                consumer.accept("\\");
                consumer.accept(sequence);
                return true;
            }
            return false;
        }

        @Override
        public boolean unEscape(@NotNull BasedSequence sequence, @Nullable DataHolder options, @NotNull Consumer<CharSequence> consumer) {
            if ((sequence.length() == 4 || sequence.length() == 5 && sequence.charAt(4) == '+') && (sequence.startsWith("\\???") || sequence.startsWith("\\!!!") || sequence.startsWith("\\:::"))) {
                consumer.accept(sequence.subSequence(1));
                return true;
            }
            return false;
        }
    }

    static boolean isMarker(
            final ParserState state,
            final int index,
            final boolean inParagraph,
            final boolean inParagraphListItem,
            final AdmonitionOptions options
    ) {
        final boolean allowLeadingSpace = options.allowLeadingSpace;
        final boolean interruptsParagraph = options.interruptsParagraph;
        final boolean interruptsItemParagraph = options.interruptsItemParagraph;
        final boolean withLeadSpacesInterruptsItemParagraph = options.withSpacesInterruptsItemParagraph;
        CharSequence line = state.getLine();
        if (!inParagraph || interruptsParagraph) {
            if ((allowLeadingSpace || state.getIndent() == 0) && (!inParagraphListItem || interruptsItemParagraph)) {
                if (inParagraphListItem && !withLeadSpacesInterruptsItemParagraph) {
                    return state.getIndent() == 0;
                } else {
                    return state.getIndent() < state.getParsing().CODE_BLOCK_INDENT;
                }
            }
        }
        return false;
    }

    private static class BlockFactory extends AbstractBlockParserFactory {
        final private AdmonitionOptions options;

        BlockFactory(DataHolder options) {
            super(options);
            this.options = new AdmonitionOptions(options);
        }

        @Override
        public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
            if (state.getIndent() >= 4) {
                return BlockStart.none();
            }

            int nextNonSpace = state.getNextNonSpaceIndex();
            BlockParser matched = matchedBlockParser.getBlockParser();
            boolean inParagraph = matched.isParagraphParser();
            boolean inParagraphListItem = inParagraph && matched.getBlock().getParent() instanceof ListItem && matched.getBlock() == matched.getBlock().getParent().getFirstChild();

            if (isMarker(state, nextNonSpace, inParagraph, inParagraphListItem, options)) {
                BasedSequence line = state.getLine();
                BasedSequence trySequence = line.subSequence(nextNonSpace, line.length());
                Parsing parsing = state.getParsing();
                Pattern startPattern = Pattern.compile(String.format(ADMONITION_START_FORMAT, parsing.ATTRIBUTENAME, parsing.LINK_TITLE_STRING));
                Matcher matcher = startPattern.matcher(trySequence);

                if (matcher.find()) {
                    // admonition block
                    BasedSequence openingMarker = line.subSequence(nextNonSpace + matcher.start(1), nextNonSpace + matcher.end(1));
                    BasedSequence info = line.subSequence(nextNonSpace + matcher.start(2), nextNonSpace + matcher.end(2));
                    BasedSequence titleChars = matcher.group(3) == null ? BasedSequence.NULL : line.subSequence(nextNonSpace + matcher.start(3), nextNonSpace + matcher.end(3));

                    int contentOffset = options.contentIndent;

                    CustomAdmonitionBlockParser admonitionBlockParser = new CustomAdmonitionBlockParser (options, contentOffset);
                    admonitionBlockParser.block.setOpeningMarker(openingMarker);
                    admonitionBlockParser.block.setInfo(info);
                    admonitionBlockParser.block.setTitleChars(titleChars);

                    return BlockStart.of(admonitionBlockParser)
                            .atIndex(line.length());
                } else {
                    return BlockStart.none();
                }
            } else {
                return BlockStart.none();
            }
        }
    }
}
public class CustomAdmonitionExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, Formatter.FormatterExtension
        // , Parser.ReferenceHoldingExtension
{
    final public static DataKey<Integer> CONTENT_INDENT = new DataKey<>("ADMONITION.CONTENT_INDENT", 4);
    final public static DataKey<Boolean> ALLOW_LEADING_SPACE = new DataKey<>("ADMONITION.ALLOW_LEADING_SPACE", true);
    final public static DataKey<Boolean> INTERRUPTS_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_PARAGRAPH", true);
    final public static DataKey<Boolean> INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.INTERRUPTS_ITEM_PARAGRAPH", true);
    final public static DataKey<Boolean> WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH = new DataKey<>("ADMONITION.WITH_SPACES_INTERRUPTS_ITEM_PARAGRAPH", true);
    final public static DataKey<Boolean> ALLOW_LAZY_CONTINUATION = new DataKey<>("ADMONITION.ALLOW_LAZY_CONTINUATION", true);
    final public static DataKey<String> UNRESOLVED_QUALIFIER = new DataKey<>("ADMONITION.UNRESOLVED_QUALIFIER", "note");
    final public static DataKey<Map<String, String>> QUALIFIER_TYPE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TYPE_MAP", CustomAdmonitionExtension::getQualifierTypeMap);
    final public static DataKey<Map<String, String>> QUALIFIER_TITLE_MAP = new DataKey<>("ADMONITION.QUALIFIER_TITLE_MAP", CustomAdmonitionExtension::getQualifierTitleMap);
    final public static DataKey<Map<String, String>> TYPE_SVG_MAP = new DataKey<>("ADMONITION.TYPE_SVG_MAP", CustomAdmonitionExtension::getQualifierSvgValueMap);

    public static Map<String, String> getQualifierTypeMap() {
        HashMap<String, String> infoSvgMap = new HashMap<>();
        // qualifier type map
        infoSvgMap.put("abstract", "abstract");
        infoSvgMap.put("summary", "abstract");
        infoSvgMap.put("tldr", "abstract");

        infoSvgMap.put("bug", "bug");

        infoSvgMap.put("danger", "danger");
        infoSvgMap.put("error", "danger");

        infoSvgMap.put("example", "example");
        infoSvgMap.put("snippet", "example");

        infoSvgMap.put("fail", "fail");
        infoSvgMap.put("failure", "fail");
        infoSvgMap.put("missing", "fail");

        infoSvgMap.put("faq", "faq");
        infoSvgMap.put("question", "faq");
        infoSvgMap.put("help", "faq");

        infoSvgMap.put("info", "info");
        infoSvgMap.put("todo", "info");

        infoSvgMap.put("note", "note");
        infoSvgMap.put("seealso", "note");

        infoSvgMap.put("quote", "quote");
        infoSvgMap.put("cite", "quote");

        infoSvgMap.put("success", "success");
        infoSvgMap.put("check", "success");
        infoSvgMap.put("done", "success");

        infoSvgMap.put("tip", "tip");
        infoSvgMap.put("hint", "tip");
        infoSvgMap.put("important", "tip");

        infoSvgMap.put("warning", "warning");
        infoSvgMap.put("caution", "warning");
        infoSvgMap.put("attention", "warning");

        return infoSvgMap;
    }

    public static Map<String, String> getQualifierTitleMap() {
        HashMap<String, String> infoTitleMap = new HashMap<>();
        infoTitleMap.put("abstract", "Abstract");
        infoTitleMap.put("summary", "Summary");
        infoTitleMap.put("tldr", "TLDR");

        infoTitleMap.put("bug", "Bug");

        infoTitleMap.put("danger", "Danger");
        infoTitleMap.put("error", "Error");

        infoTitleMap.put("example", "Example");
        infoTitleMap.put("snippet", "Snippet");

        infoTitleMap.put("fail", "Fail");
        infoTitleMap.put("failure", "Failure");
        infoTitleMap.put("missing", "Missing");

        infoTitleMap.put("faq", "Faq");
        infoTitleMap.put("question", "Question");
        infoTitleMap.put("help", "Help");

        infoTitleMap.put("info", "Info");
        infoTitleMap.put("todo", "To Do");

        infoTitleMap.put("note", "Note");
        infoTitleMap.put("seealso", "See Also");

        infoTitleMap.put("quote", "Quote");
        infoTitleMap.put("cite", "Cite");

        infoTitleMap.put("success", "Success");
        infoTitleMap.put("check", "Check");
        infoTitleMap.put("done", "Done");

        infoTitleMap.put("tip", "Tip");
        infoTitleMap.put("hint", "Hint");
        infoTitleMap.put("important", "Important");

        infoTitleMap.put("warning", "Warning");
        infoTitleMap.put("caution", "Caution");
        infoTitleMap.put("attention", "Attention");

        return infoTitleMap;
    }

    public static Map<String, String> getQualifierSvgValueMap() {
        HashMap<String, String> typeSvgMap = new HashMap<>();
        typeSvgMap.put("abstract", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-abstract.svg")));
        typeSvgMap.put("bug", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-bug.svg")));
        typeSvgMap.put("danger", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-danger.svg")));
        typeSvgMap.put("example", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-example.svg")));
        typeSvgMap.put("fail", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-fail.svg")));
        typeSvgMap.put("faq", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-faq.svg")));
        typeSvgMap.put("info", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-info.svg")));
        typeSvgMap.put("note", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-note.svg")));
        typeSvgMap.put("quote", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-quote.svg")));
        typeSvgMap.put("success", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-success.svg")));
        typeSvgMap.put("tip", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-tip.svg")));
        typeSvgMap.put("warning", getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/images/adm-warning.svg")));
        return typeSvgMap;
    }

    public static String getInputStreamContent(InputStream inputStream) {
        try {
            InputStreamReader streamReader = new InputStreamReader(inputStream);
            StringWriter stringWriter = new StringWriter();
            copy(streamReader, stringWriter);
            stringWriter.close();
            return stringWriter.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    public static String getDefaultCSS() {
        return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.css"));
    }

    public static String getDefaultScript() {
        return getInputStreamContent(CustomAdmonitionExtension.class.getResourceAsStream("/admonition.js"));
    }

    public static void copy(Reader reader, Writer writer) throws IOException {
        char[] buffer = new char[4096];
        int n;
        while (-1 != (n = reader.read(buffer))) {
            writer.write(buffer, 0, n);
        }
        writer.flush();
        reader.close();
    }

    private CustomAdmonitionExtension() {
    }

    public static CustomAdmonitionExtension create() {
        return new CustomAdmonitionExtension();
    }

    @Override
    public void extend(Formatter.Builder formatterBuilder) {
        formatterBuilder.nodeFormatterFactory(new AdmonitionNodeFormatter.Factory());
    }

    @Override
    public void rendererOptions(@NotNull MutableDataHolder options) {

    }

    @Override
    public void parserOptions(MutableDataHolder options) {

    }

    @Override
    public void extend(Parser.Builder parserBuilder) {
        parserBuilder.customBlockParserFactory(new CustomAdmonitionBlockParser.Factory());
    }

    @Override
    public void extend(@NotNull HtmlRenderer.Builder htmlRendererBuilder, @NotNull String rendererType) {
        if (htmlRendererBuilder.isRendererType("HTML")) {
            htmlRendererBuilder.nodeRendererFactory(new AdmonitionNodeRenderer.Factory());
        } else if (htmlRendererBuilder.isRendererType("JIRA")) {

        }
    }
}

MarkdownConverter类中增加以下解析器

options.set(Parser.EXTENSIONS, Arrays.asList(
    // ... 其他扩展
    CustomAdmonitionExtension.create(),   // 提示框扩展,用于创建提示框(如 !!! note 创建提示框)
));

3.2 测试自定义解析器

输入内容:

!!! note
    这是一个提示框的内容

结果如下:

以上就是SpringBoot实现Markdown语法转HTML标签的详细步骤的详细内容,更多关于SpringBoot Markdown语法转HTML标签的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文