JavaScript Library – Markdown

介绍

什么是 Markdown?

Markdown 是一种极简化版的 HTML 表达语言。

直接看例子就能体会了

图1

图2

简单说,它就是用一种更简单,更简短的纯文本去表达 HTML。

很多地方都可以看到 Markdown 的运用,比如 Github 提交 Issue 采用的就是 Markdown 语法

还有,我们经常看的 CHANGELOG.md,这个 .md 指的就是 Markdown 文档格式。

 

主要参考

YouTube – The Only Markdown Crash Course You Will Ever Need

官网 – Markdown Guide

JavaScript Library – marked

.NET Library – markdig

List of Markdown Libraries

 

Basic Syntax

Markdown 有分 basic syntax 和 extended syntax。

extended syntax 不一定所以的 library 都支持,或统一,所以我们在使用时要自行确认一下。

注:不熟悉 HTML tags 的可以参考这篇 HTML – HTML Tags & Semantic HTML 语义化 HTML

<p>

paragraph

<!-- parse to -->

<p>paragraph</p>

上半段是 Markdown 语法,下半段是 HTML。

普通一个 text 会被 parse to <p>。

<br> 分段

Hello  <!--注:这里结尾有 2 个 space 哦 -->
World

<!-- parse to -->

<p>Hello<br>World</p>

注意,Hello 的后面放了 2 个 space,然后 new line。

这表示要 <br>。

如果你觉得 2 个 space 不容易被看见,怕出错,它也支持直接写 <br>。

Hello<br>World

<!-- parse to -->

<p>Hello<br>World</p>

如果要大分段,则使用 empty line

Hello

World

<!-- parse to -->

<p>Hello</p>
<p>World</p>

后面不需要 space 了。

<h1> – <h6>

# Title
## Title
### Title
#### Title
##### Title
###### Title

<!-- parse to -->

<h1>Title</h1>
<h2>Title</h2>
<h3>Title</h3>
<h4>Title</h4>
<h5>Title</h5>
<h6>Title</h6>

就是一路加井号就对了。

<strong> & <em>

bold 和 italics

**bold**               <!-- parse to --> <p><strong>bold</strong></p>
*italics*              <!-- parse to --> <p><em>italics</em></p>
***italics and bold*** <!-- parse to --> <p><em><strong>italics and bold</strong></em></p>

如果我们想要 asterisk 符号,那就用反斜杠破掉它。

\*pure asterisk\* <!-- parse to --> <p>*pure asterisk*</p>

<blockquotes>

> blockquote

<!-- parse to -->

<blockquote>
  <p>blockquote</p>
</blockquote>

想要 multiple line,就继续加 >

> # Title
> description...

<!-- parse to -->

<blockquote>
  <h1>Title</h1>
  <p>description...</p>
</blockquote>

注:有些 Markdown Library 比较 smart (e.g. JS -- marked),即使没有 > 它也知道是 multiple line,只要 new line 就可以了,但也有些比较不 smart (.NET -- markdig),所以最好还是给每行加上 > 吧。

要 nested 就 double >>

> blockquote
>> nested blockquote

<!-- parse to -->

<blockquote>
  <p>blockquote</p>
  <blockquote>
    <p>nested blockquote</p>
  </blockquote>
</blockquote>

<ol>, <ul>, <li>

ol > li

1. item 1
2. item 2
3. item 3
3. item 4
3. item 5

<!-- parse to-->

<ol>
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item 4</li>
  <li>item 5</li>
</ol>

号码不需要顺序,只要是号码就可以了

另外,号码必须是 positive,如果是 starts with zero,它会 parse to <ol start="0">

ul > li

* item 1
* item 2

<!-- parse to-->

<ul>
  <li>item 1</li>
  <li>item 2</li>
</ul>

除了 asterisk * 符号,用 dash - 或 plus + 符号也可以,效果一样。

nested 

* item 1
  0. item 1.0
  1. item 1.1
* item 2

<!-- parse to-->

<ul>
  <li>item 1
    <ol start="0">
      <li>item 1.0</li>
      <li>item 1.1</li>
    </ol>
  </li>
  <li>item 2</li>
</ul>

加 2 个 space 在前面 (术语叫 indentation,用 Material Design 话术叫 inset 也可以) 表示要 nested。

<pre>, <code>

` 表示一行代表

`const firstName = 'Derrick';`

<!-- parse to-->

<p><code>const firstName = &#39;Derrick&#39;;</code></p>

``` 表示多行代码 (注:这个是 extended syntax)

```
  const firstName = 'Derrick';
  const lastName = 'Yam';
```

<!-- parse to-->

<pre>
  <code>
    const firstName = 'Derrick';
    const lastName = 'Yam';
  </code>
</pre>

此外,它还可以声明语言

```js
  const firstName = 'Derrick';
  const lastName = 'Yam';
```

<!-- parse to-->

<pre>
  <code class="language-js">
    const firstName = 'Derrick';
    const lastName = 'Yam';
  </code>
</pre>

会多一个 language class,CSS 可以针对不同语言做不同样式处理。

<img>

![image alt](/yangmi.jpg)

<!-- parse to-->

<p><img src="/yangmi.jpg" alt="image alt" /></p>

可以声明 alt 和 src。

src 可以是相对路径或绝对路径 URL 都可以。

要加 title 也可以

![image alt](/yangmi.jpg "Yang Mi")

<!-- parse to-->

<p><img src="/yangmi.jpg" alt="image alt" title="Yang Mi" /></p>

<hr>

使用 triple underscrore ___ 表示 <hr>

line1
___
line2

<!-- parse to-->

<p>line1</p>
<hr />
<p>line2</p>

除了 underscore,triple asterisk *** 和 triple dash --- 也是可以。

line1
***
line2

或者

line1

---
line2

不过 triple dash --- 要多一个 new line,不然它会变成 <h2>,因为 triple dash --- 和 triple equal === 也可以用作 <h2> 和 <h1>。

Title
===

<!-- parse to-->

<h1>Title</h1>


Title
---

<!-- parse to-->

<h2>Title</h2>

我个人的习惯是 heading 统一用 #,<hr> 统一用 underscore ___

<a>

[click to about page](/about)

<!-- parse to-->

<p><a href="/about">click to about page</a></p>

语法和 <img> 雷同,只是前面少了 ! 惊叹号。

和 img 一样也可以额外添加 title

[click to about page](/about "About page")

<!-- parse to-->

<p><a href="/about" title="About page">click to about page</a></p>

但很遗憾,它不支持 target="_blank"。

有一种写法是这样

[link](url){:target="_blank"}
or
[link](url){target="_blank"}

但不是正规的,大部分 parser 都不支持。

另外,我们也可以用 email text 或者 URL 直接作为 <a>

<hengkeat87@gmail.com>

<https://www.stooges.com.my>

<!-- parse to-->

<p><a href="mailto:hengkeat87@gmail.com">hengkeat87@gmail.com</a></p>
<p><a href="https://www.stooges.com.my">https://www.stooges.com.my</a></p>

wrap 一个 <> 符号到 email 和 URL 上就可以了。

HTML tags

不是所有的 HTML 都有对应的 Markdown。

比如 underline <u> 和 <ins> 就没有。

但是,Markdown Library 通常允许我们直接写 HTML tags 来弥补它的不足,比如

<u>underline</u>

<ins>underline</ins>

<!-- parse to-->

<p><u>underline</u></p>
<p><ins>underline</ins></p>

直接写,它是可以 parse 出来的 (注:也不是所有的 HTML 都可以 parse 啦)。

常见的有 <sub>、<sup>、<mark>、<del>,这几个有些 extended syntax 支持,有些不一定支持。

 

Extended Syntax

上一 part 是 basic,这一 part 是 extended syntax。

注:不是所有的 Library 都支持 parse extended syntax 哦,使用时记得先检查清楚。

<table>

| Header1 | Header2 |
| ------- | ------- |
| Cell 2A | Cell 2B |
| Cell 3A | Cell 3B |

<!-- parse to-->

<table>
  <thead>
    <tr>
      <th>Header1</th>
      <th>Header2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cell 2A</td>
      <td>Cell 2B</td>
    </tr>
    <tr>
      <td>Cell 3A</td>
      <td>Cell 3B</td>
    </tr>
  </tbody>
</table>

利用 pipe | 和 dash --- 做分割。

pipe 和 triple dash 不需要对齐,像下面这样也是可以的。

| Header1 | Header2 |
| --- | --- |
| Value1 | Value2 |

要 align 也可以,加上分号:

| Header1 | Header2 | Header3 |
| ------: | :-----: | :------ |
| Value1 | Value2 | Value3 |
| Value4 | Value5 | Value6 |

<!-- parse to-->

<table>
  <thead>
    <tr>
      <th align="right">Header1</th>
      <th align="center">Header2</th>
      <th align="left">Header3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td align="right">Value1</td>
      <td align="center">Value2</td>
      <td align="left">Value3</td>
    </tr>
    <tr>
      <td align="right">Value4</td>
      <td align="center">Value5</td>
      <td align="left">Value6</td>
    </tr>
  </tbody>
</table>

不过它只能批量,不能针对特定 cell align。

另外,有些 Markdown Library 会 parse to style,像这样

<th style="text-align: right;">Header1</th>
<th style="text-align: center;">Header2</th>
<th style="text-align: left;">Header3</th>

比如说 JS --marked 是 parse to align="center",而 .NET -- markdig 则是 style="text-align: center;"

<h1 - h6> with [id]

# My Great Heading {#custom-id}

<!-- parse to-->

<h1 id="custom-id">My Great Heading</h1>

link to Heading with [id]

[Heading IDs](#custom-id)

<!-- parse to-->

<p><a href="#custom-id">Heading IDs</a></p>

<dl>, <dt>, <dd>

First Term
: This is the definition of the first term.

Second Term
: This is one definition of the second term.
: This is another definition of the second term.

<!-- parse to-->

<dl>
  <dt>First Term</dt>
  <dd>This is the definition of the first term.</dd>
  <dt>Second Term</dt>
  <dd>This is one definition of the second term.</dd>
  <dd>This is another definition of the second term.</dd>
</dl>

<del>

~~delete~~

<!-- parse to-->

<p><del>delete</del></p>

<mark>

==mark==

<!-- parse to-->

<p><mark>mark</mark></p>

总结

basic 和 extended syntax 大概就是这些,我没有 100% 的列出,只是写了我有接触到的,想看完整的 list 可以看这三篇:Basic SyntaxExtended SyntaxHacks

 

JavaScript Library for Markdown – marked

JS 有好几个 Markdown Library 可选,比较火的是 marked

get started

安装

yarn add marked

它源码是用 TypeScript 写的,直接支持类型了。

调用

import { marked } from 'marked';

const html = marked.parse(`# Hello World`); // <h1>Hello World</h1>

import + pares 就可以了,非常简单。

configuration

marked 可以做一些简单的配置。

我们先看看如何 set config。

const html = await marked.parse(`# Hello World`, { async: true }); // set config on each call

在调用 parse 时,传入 config,这是其中一个 set config 的方式。

每一次 parse 都 set 很麻烦,所以可以使用 use 来配置全局

marked.use({ async: true }); // set global config
const html = await marked.parse(`# Hello World`);

其实它也不是全局啦,marked 是对象,它的范围就是这个对象内,如果有需要,我们也可以实例化多个对象来做管理。

import { marked, Marked } from 'marked';

const marked1 = new Marked();
marked1.use({ async: true });

const marked2 = new Marked();
marked2.use({ async: false });

const html1 = await marked.parse(`# Hello World`); // 有 async 
const html2 = marked.parse(`# Hello World`);       // 没 async

常见 config

所有的 config list 看这篇 -- Options

这里只点出我常用的。

import { marked } from 'marked';

marked.use({ async: true, breaks: true });

const html = await marked.parse(`
paragraph1
paragraph2
`);

console.log(html); // <p>paragraph1<br>paragraph2</p>

两个点:

  1. parse 默认是同步 sync 的,如果想要 async 可以设置 async: true

  2. 本来 <br> 需要在结尾加两个 space,开启 breaks: true 之后就不需要了。

另外,markded 默认 config gfm: true,gfm stand for GitHub Flavored Markdown,就是 Github 采用的 Markdown 标准 (里头除了 basic 和 一些 extended syntax)。

其中一个特色是 auto convert link

const html1 = marked.parse('hengkeat87@gmail.com');                 // <p><a href="mailto:hengkeat87@gmail.com">hengkeat87@gmail.com</a></p>
const html2 = marked.parse('hengkeat87@gmail.com', { gfm: false }); // <p>hengkeat87@gmail.com</p>

gfm: true 的情况下,email text 会直接 parse 成 <a>,原版的 Markdown 需要 wrap 一层 <> 符号 <hengkeat87@gmail.com> 才会 parse to <a>。

其它操作

这里列出一些我日常用过的操作

parseInline

const html1 = marked.parse('hengkeat87@gmail.com');       // <p><a href="mailto:hengkeat87@gmail.com">hengkeat87@gmail.com</a></p>
const html2 = marked.parseInline('hengkeat87@gmail.com'); // <a href="mailto:hengkeat87@gmail.com">hengkeat87@gmail.com</a>

主要区别是有没有 wrap 一层 <p>,inline 就没有。

Extensions

marked 支持插件扩展 (比如 Markdown extended syntax 甚至是超出这个范围),这里介绍一些常用的 extensions。(注:想看完整的 list -- 这篇 Known Extensions

marked-custom-heading-id

要支持 Markdown extended syntax -- heading with [id],需要额外的插件

安装

yarn add marked-custom-heading-id

使用 marked.use() 来 register plugin。 

import { marked } from 'marked';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import customHeadingId from 'marked-custom-heading-id';

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
marked.use(customHeadingId());

const html = marked.parse('# heading {#custom-id}'); // <h1 id="custom-id">heading</h1>

由于这个插件不是 TypeScript 写的,也没有 .d.ts 文档,所以我使用了 @ts-ignore 来 by pass。(注:大多数的 plugin 是支持类型的,只有少数是不支持的)

注:如果不想使用插件,自己写一个 override render 也是可以实现,下一 part 会教。

marked-emoji

安装

yarn add marked-emoji
yarn add @octokit/rest

@octokit/rest 用来获取 Github 的 emojis (你想用其它库也可以),marked-emoji 是插件。

import { Octokit } from '@octokit/rest';
import { marked } from 'marked';
import { markedEmoji, MarkedEmojiOptions } from 'marked-emoji';

const octokit = new Octokit();
const response = await octokit.rest.emojis.get(); // get all emojis available to use on GitHub.
const emojis = response.data;

const options: MarkedEmojiOptions = {
  emojis,
  renderer: token => `<img alt="${token.name}" src="${token.emoji}" class="marked-emoji-img">`,
};

marked.use(markedEmoji(options));

const html = marked.parse(':smile:');
// <p><img alt="smile" src="https://github.githubassets.com/images/icons/emoji/unicode/1f604.png?v8" class="marked-emoji-img"></p>

marked-extended-tables

Markdown extended syntax 支持 <table>,但不支持 colspan,rowspan,multiple header row。

而这个插件弥补了这些缺失。

安装

yarn add marked-extended-tables

调用

import { marked } from 'marked';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import extendedTables from 'marked-extended-tables';

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
marked.use(extendedTables());
const html = marked.parse(`
| H1      | H2      | H3      |
|---------|---------|---------|
| This cell spans 3 columns |||  
`);

/*
  <table>
    <thead>
      <tr>
        <th>H1</th>
        <th>H2</th>
        <th>H3</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td colspan=3>This cell spans 3 columns</td>
      </tr>
    </tbody>
  </table>
*/

上面是 colspan 的 markdown 写法,再看看 rowspan 和 multiple header row 的写法

multiple header rows

效果

还有 column width 的写法

override default renderer & tokenizer

如果我们想拦截 marked parse 的过程,然后动点手脚的话,可以 override default renderer。

下面这个是让 marked 支持 heading + id property (默认是不支持的,上一 part 有提到如何使用插件实现,这里给一个不依赖插件的做法)

import { marked, Tokens } from 'marked';

// 一个带 id 的 heading
const text = `
  # Hello World {#my-id}
`;

// 创建一个 default renderer
const customRenderer = new marked.Renderer();

// override default renderer 的 heading 方法
customRenderer.heading = function ({ tokens, depth }: Tokens.Heading) {
  // 使用 original 手法 parse heading
  // 这里会得到 string 'Hello World {#my-id}'
  const headingText = this.parser.parseInline(tokens);

  // id 的正则表达
  const idRegex = / {#([a-z]+[a-z\-0-9]+)}$/;
  const matchResults = headingText.match(idRegex);

  if (matchResults) {
    // 尝试 match id
    const id = matchResults[1];
    const headlineTextWithoutId = headingText.replace(idRegex, '');

    // 如果有 match 到 id 就加进去,同时把 id 从 heading 里删除,最终返回
    return `<h${depth} id="${id}">${headlineTextWithoutId}</h${depth}>\n`;
  }

  // 如果没有 match 到 id 直接返回就可以了。
  return `<h${depth}>${headingText}</h${depth}>\n`;

  // 源码 renderer.heading 长这样:
  // heading({ tokens, depth }: Tokens.Heading): string {
  //   return `<h${depth}>${this.parser.parseInline(tokens)}</h${depth}>\n`;
  // }
};

// 把 custom renderer 配置进去 marked
marked.use({ renderer: customRenderer });

// parse markdown syntax
console.log(marked.parse(text)); // result: <h1 id="my-id">Hello World</h1>

如果我们想支持 anchor target _blank 难度会比较高。

先看看它的源码如何实现 renderer

用到了一些 internal(没用公开)的方法:cleanUrl、escape。

只有 href, title 资料,它们来自 tokens。

最后靠拼接 string 形成 <a href title>。

从这几个面向来看,完全没用 target _blank 的影子,因此我们不只是要 override renderer,还要 override tokenizer 才有办法做到。

最终的调用

const html = marked.parse(
  `
    # Hello World

    I am the [king](/king "king"){target="_blank" aria-label="test"} of the world
  `
    .split('\n')
    .map(v => v.trim())
    .join('\n'),
);

在此之前,我们需要给 marked 添加自定义的 tokenizer 和 renderer

interface IAttributes {
  attributes?: string;
}

// 自定义一个 tokenizer
const tokenizer: TokenizerObject = {
  // 我们要覆盖原本的 link 方法
  link(src) {
    // 调用原生的 link 方法
    const result = marked.Tokenizer.prototype.link.call(this, src);

    // 如果是 undefined,代表不是 anchor,不需要处理
    if (result) {
      // 此时的 src 是 [king](/king "king"){target="_blank" aria-label="test"} of the world
      // restOfSrc 就是 {target="_blank" aria-label="test"} of the world
      const restOfSrc = src.substring(result.raw.length);

      // 我们要的就是括弧内的 attributes
      if (restOfSrc.length > 0 && restOfSrc.charAt(0) === '{') {
        const indexOfEndBracket = restOfSrc.indexOf('}', 1);
        if (indexOfEndBracket !== -1) {
          // attributes 就是 target="_blank" aria-label="test"
          const attributes = restOfSrc.substring(1, indexOfEndBracket);
          // 存起来
          (result as IAttributes).attributes = attributes;
          // result.raw 就是这一次处理的字符串:[king](/king "king"){target="_blank" aria-label="test"}
          // 剩余的 ' of the world' 会继续被其它 tokenizer 解析
          result.raw += '{' + attributes + '}';
        }
      }
    }

    return result;
  },
};

const renderer = new marked.Renderer();
renderer.link = function (linkTokenInfo: Tokens.Link & IAttributes): string {
  // 调用原生的 link 方法,会拿到 anchor raw html '<a href="/king" title="king">king</a>'
  const linkRawHtml = marked.Renderer.prototype.link.call(this, linkTokenInfo) as string;
  if (linkTokenInfo.attributes === undefined) return linkRawHtml;
  // 把我们的 attributes replace 进去
  // 形成 <a target="_blank" aria-label="test" href="/king" title="king">king</a>
  return linkRawHtml.replace(/<a(\s|>)/, `<a ${linkTokenInfo.attributes}$1`);
};

marked.use({ tokenizer, renderer });

实现手法比较粗糙,但勉强能用。

总结

我目前有用到的大致上就是这些了,更多功能以后有用到才来补上。

 

.NET Library for Markdown – markdig

.NET 有好几个 Markdown Library 可选,比较火的是 markdig

它的文档比较简陋,我懒得深入研究了。

基本上 basic / extended syntax 都支持。

get started

安装

dotnet add package Markdig

Program.cs

using Markdig;

var result = Markdown.ToHtml("# Hello World");

Console.WriteLine(result); // <h1>Hello World</h1>

extended syntax

要支持 extended syntax 就添加 pipeline。

using Markdig;

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();

var result = Markdown.ToHtml(@"
  | Header1 | Header2 |
  | ------- | ------- |
  | Cell 2A | Cell 2B |
", pipeline);

Console.WriteLine(result);

/*
<table>
  <thead>
    <tr>
      <th>Header1</th>
      <th>Header2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Cell 2A</td>
      <td>Cell 2B</td>
    </tr>
  </tbody>
</table>
*/

上面这个是全部都开启,如果只想针对 table extended syntax,那可以这样写。

var pipeline = new MarkdownPipelineBuilder().UsePipeTables().Build();

注:这个 table syntax 不支持 colspan 哦,想支持 colspan 要用 Grid Table 插件。

var pipeline = new MarkdownPipelineBuilder().UseGridTables().Build();

Markdown 语法也不同

+---------+---------+---------+
| Col1    | Col2    | Col3    |
| Col1a   | Col2a   | Col3a   |
| Col1b             | Col3b   |
| Col1c                       |

<!-- parse to-->

<table>
  <col style="width:33.33%" />
  <col style="width:33.33%" />
  <col style="width:33.33%" />
  <tbody>
    <tr>
      <td>Col1
        Col1a</td>
      <td>Col2
        Col2a</td>
      <td>Col3
        Col3a</td>
    </tr>
    <tr>
      <td colspan="2">Col1b</td>
      <td>Col3b</td>
    </tr>
    <tr>
      <td colspan="3">Col1c</td>
    </tr>
  </tbody>
</table>

注意事项

开启所有的 extended syntax (UseAdvancedExtensions) 是挺危险的,一不小心可能会有意想不到的 parse 结果哦,比如

自动加 id

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var result = Markdown.ToHtml(@"# Hello World", pipeline); // <h1 id="hello-world">Hello World</h1>

auto convert link, but not email

var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var result = Markdown.ToHtml(@"
  website: https://www.stooges.com.my
  
  email: hengkeat87@gmail.com
", pipeline);

// <p>website: <a href="https://www.stooges.com.my">https://www.stooges.com.my</a></p>
// <p>email: hengkeat87@gmail.com</p>

所以最好还是挨个挨个添加 extensions

var pipeline = new MarkdownPipelineBuilder()
  .UseAutoIdentifiers() // auto add id 
  .UseAutoLinks() // auto convert link 
  .Build();

如果真的太多了,那就只能反过来,添加 all 然后再 remove 特定的 extension。

var pipelineBuilder = new MarkdownPipelineBuilder()
  .UseAdvancedExtensions();

pipelineBuilder.Extensions.Remove(pipelineBuilder.Extensions.Find<AutoIdentifierExtension>()!); // remove auto add id
pipelineBuilder.Extensions.Remove(pipelineBuilder.Extensions.Find<AutoLinkExtension>()!); // remove auto convert link

var pipeline = pipelineBuilder.Build();

anchor target _blank and heading id

有 built-in 支持

var pipeline = new MarkdownPipelineBuilder()
  .UseGenericAttributes()
  .Build();

var markdown = """
  [click to about page](/about "About page"){target="_blank"}

  # My Great Heading {#custom-id}
""";

Console.WriteLine(Markdown.ToHtml(markdown, pipeline));

/*
  <p><a href="/about" target="_blank" title="About page">click to about page</a></p>
  <h1 id="custom-id">My Great Heading</h1>
*/

它不只是 target="_blank" 和 id,任何 attribute 都可以添加。

只要在后续写上 { attributeName="attributeValue" } 就可以了(注:支持 multiple attribute,用空格 space 做分割)。

parseInline

marked 有 parseInline 的功能,但 markdig 却没有。

相关 Issue – Setting ImplicitParagraph has no effect

目前的 workaround 是这样

using Markdig;
using Markdig.Renderers;

var pipelineBuilder = new MarkdownPipelineBuilder();
var pipeline = pipelineBuilder.Build();

var markdown = "**Hello World**";

Console.WriteLine(Markdown.ToHtml(markdown, pipeline)); // <p><strong>Hello World</strong></p> 会有 paragraph


using var writer = new StringWriter();
var renderer = new HtmlRenderer(writer)
{
  ImplicitParagraph = true
};
pipeline.Setup(renderer);

var document = Markdown.Parse(markdown, pipeline, null);
renderer.Render(document);
Console.WriteLine(writer.ToString()); // <strong>Hello World</strong> 没有 paragraph

使用底层的 HtmlRenderer 做 parse。

markdown to plain text

有时候我们想把 markdown 转成 plain text 而不是 raw html(比如:想生成 JSON-LD)。

markdig 没用 built-in 这个功能,我目前的方案是把 markdown 转成 raw html,接着在用 AngleSharp(类似 browser 的 DOM API)解析 HTML 取出 plain text。

string markdown = @"
**How often should I service my aircon?**

We recommend servicing your aircon every 3-6 months to maintain optimal performance.

[Learn more](https://example.com)
";

string html = Markdown.ToHtml(markdown);
Console.WriteLine("HTML:\n" + html);
/*
  <p><strong>How often should I service my aircon?</strong></p>
  <p>We recommend servicing your aircon every 3-6 months to maintain optimal performance.</p>
  <p><a href="https://example.com">Learn more</a></p>
*/

var parser = new HtmlParser();
var document = parser.ParseDocument(html);

string plainText = document.Body!.TextContent.Trim();
Console.WriteLine("\nPlain Text:\n" + plainText);
/*
  How often should I service my aircon?
  We recommend servicing your aircon every 3-6 months to maintain optimal performance.
  Learn more
*/

 

总结

本篇简单介绍了 Markdown 语法和两个 Markdown Library。

一个是 JS 的 marked

另一个是 .NET 的 markdig

 

posted @ 2025-01-08 15:15  兴杰  阅读(183)  评论(0)    收藏  举报