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
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 = 'Derrick';</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>
 <!-- parse to--> <p><img src="/yangmi.jpg" alt="image alt" /></p>
可以声明 alt 和 src。
src 可以是相对路径或绝对路径 URL 都可以。
要加 title 也可以
 <!-- 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 Syntax, Extended Syntax, Hacks
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>
两个点:
-
parse 默认是同步 sync 的,如果想要 async 可以设置 async: true
-
本来 <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。