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"}

但不是正规的,大部分 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

如果我们想拦截 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>
复制代码

另外,本来还想给多一个支持 <a target="_blank" > 的例子,但看了源码后发现不太容易

基本上就是拼接而成的,而且用到了许多内部函数,比如 cleanUrl, escape 这些都没有公开。

for 这个 case 倘若我们要动手脚,也只能拿它的 return string 做 modify,感觉这样非常不顺风水,还是算了呗。

总结

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

 

.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();

 

总结

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

一个是 JS 的 marked

另一个是 .NET 的 markdig

 

posted @   兴杰  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· 【全网最全教程】使用最强DeepSeekR1+联网的火山引擎,没有生成长度限制,DeepSeek本体
历史上的今天:
2024-01-08 Angular 18+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)
2023-01-08 BOM – Cookie 和 LocalStorage
2019-01-08 Asp.net core 学习笔记 SignalR
点击右上角即可分享
微信分享提示