在 Markdown 元素之间转换

Convert between Markdown elements

有哪些选项可以解析 Markdown 文档并处理其元素以输出另一个 Markdown 文档?

说吧

```
# unaffected #
```

# H1 #

H1
==

## H2 ##

H2
--

### H3 ###

应转换为

```
# unaffected #
```

## H1 ##

H1
--

### H2 ###

### H2 ###

#### H3 ####

在节点环境中。目标元素可能会有所不同(例如 #### 可能会转换为 **)。

文档可能包含其他不受影响的标记元素。

如何获得?显然,不使用正则表达式(使用正则表达式而不是成熟的词法分析器会影响 # unaffected #)。我希望使用 marked 但它似乎只能输出 HTML,而不是 Markdown。

您必须使用正则表达式。 marked 本身使用 Regexp 来解析文档。你为什么不呢?

这是您需要的一些正则表达式,来自 marked.js source code on github:

var block = {
  newline: /^\n+/,
  code: /^( {4}[^\n]+\n*)+/,
  fences: noop,
  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
  nptable: noop,
  lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
  blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
  list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!bull )\n*|\s*$)/,
  html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
  table: noop,
  paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
  text: /^[^\n]+/
};

如果你真的不想使用正则表达式,你可以 fork marked 对象。并覆盖 Renderer 对象。

github 上的标记拆分为两个部分。一个用于解析,一个用于渲染。您可以轻松地将渲染更改为您自己的渲染。 (编译器)

Example of one function in Render.js:

Renderer.prototype.blockquote = function(quote) {
  return '<blockquote>\n' + quote + '</blockquote>\n';
};)

这是一个带有外部降价解析器的解决方案,pandoc。它允许 custom filters in haskell or python to modify the input (there also is a node.js port)。这是一个 python 过滤器,它每 header 增加一个级别。让我们将其保存为 header_increase.py.

from pandocfilters import toJSONFilter, Header

def header_increase(key, value, format, meta):
    if key == 'Header' and value[0] < 7:
        value[0] = value[0] + 1
        return Header(value[0], value[1], value[2])

if __name__ == "__main__":
    toJSONFilter(header_increase)

不会影响代码块。但是,它可能会将 h1 和 h2 元素的 setex-style headers(使用 ===---)转换为 atx-style headers(使用 #),和 vice-versa.

要使用该脚本,可以从命令行调用 pandoc:

pandoc input.md --filter header_increase.py -o output.md -t markdown

使用 node.js,你可以使用 pdc 来调用 pandoc。

var pdc = require('pdc');
pdc(input_md, 'markdown', 'markdown', [ '--filter', './header_increase.py' ], function(err, result) {
  if (err)
    throw err;

  console.log(result);
});

尽管 Markdown 看起来很简单,但实际上解析起来有些复杂。每个部分都建立在下一个部分的基础上,因此即使您只想处理文档的一部分,也要涵盖所有边缘情况,您需要一个完整的解析器。

例如,各种类型的块级元素可以嵌套在其他块级元素(列表、块引用等)中。大多数实现依赖于解析器中事件的不同特定顺序来确保正确解析整个文档。如果您移除其中一个较早的部分,则许多较晚的部分将会破裂。例如,代码块内的 Markdown 标记不会被解析为 Markdown,因为第一步是查找和识别代码块,以便后面的解析步骤永远不会看到代码块。

因此,为了实现您的目标并涵盖所有可能的边缘情况,您需要一个完整的 Markdown 解析器。但是,由于您不想输出 HTML,您的选择有些受限,您需要做一些工作才能获得有效的解决方案。

基本上有三种 Markdown 解析器风格(我在这里概括一下):

  1. 使用正则表达式字符串替换将 Markdown 标记替换为源文档中的 HTML 标记。
  2. 使用解析器在解析文档输出新文档时(在每个步骤中)调用的呈现器。
  3. 生成树对象或标记列表(具体因实现而异),在后续步骤中呈现(转换为字符串)到新文档。

原始参考实现(markdown.pl)是第一种类型,可能对您没有用。为了完整起见,我只是提到它。

Marked 是第二种,虽然可以使用它,但您需要编写自己的渲染器并让渲染器在渲染文档的同时修改文档。虽然通常是一种性能解决方案,但当您需要修改文档时,它并不总是最好的方法,尤其是当您需要文档中其他地方的上下文时。但是,您应该能够让它发挥作用。

例如适配一个example in the docs, you might do something like this (multiplyString borrowed from here):

function multiplyString (str, num) {
    return num ? Array(num + 1).join(str) : "";
}

renderer.heading = function (text, level) {
    return multiplyString("#", level+1) + " " + text;
}

当然,您还需要为其他所有输出 Markdown 语法的 block level renderer methods and inline level renderer methods 创建渲染器。请参阅下面关于一般渲染器的评论。

Markdown-JS is of the third variety (as it turns out Marked also provides a lower level API with access to the tokens so it could be used this way as well). As stated in its README:

Intermediate Representation

Internally the process to convert a chunk of Markdown into a chunk of HTML has three steps:

  1. Parse the Markdown into a JsonML tree. Any references found in the parsing are stored in the attribute hash of the root node under the key references.
  2. Convert the Markdown tree into an HTML tree. Rename any nodes that need it (bulletlist to ul for example) and lookup any references used by links or images. Remove the references attribute once done.
  3. Stringify the HTML tree being careful not to wreck whitespace where whitespace is important (surrounding inline elements for example).

Each step of this process can be called individually if you need to do some processing or modification of the data at an intermediate stage.

您可以在步骤 1 或步骤 2 中获取树对象并进行修改。但是,我会推荐步骤 1,因为 JsonML 树将更接近实际的 Markdown 文档,因为步骤 2 中的 HTML 树是要输出的 HTML 的表示。请注意,HTML 将在任何实现中丢失有关原始 Markdown 的一些信息。例如,星号或下划线是否用于强调(*foo*_foo_),或者星号、破折号(连字符)或加号是否用作列表项目符号?我不确定 JsonML 树包含多少细节(我个人没有使用过),但它肯定比第 2 步中的 HTML 树更多。

一旦您对 JsonML 树进行了修改(可能使用 tools listed here 之一,那么您可能希望跳过第 2 步并实现您自己的第 3 步以呈现(字符串化)JsonML 树回到 Markdown 文档。

这就是困难的部分。 Markdown 解析器很少输出 Markdown。事实上,Markdown 解析器很少输出 HTML 以外的任何内容。最受欢迎的例外是 Pandoc,它是多种输入和输出格式的文档转换器。但是,如果希望继续使用 JavaScript 解决方案,您选择的任何库都将要求您编写自己的渲染器,该渲染器将输出 Markdown(除非搜索结果显示由其他第三方构建的渲染器)。当然,一旦你这样做了,如果你提供它,其他人将来可能会从中受益。不幸的是,构建 Markdown 渲染器超出了这个答案的范围。

构建渲染器时一个可能的捷径是,如果您使用的 Markdown 库恰好将位置信息存储在其标记列表中(或者以某种其他方式让您可以访问每个元素的原始原始 Markdown ),您可以在渲染器中使用该信息来简单地复制和输出原始 Markdown 文本,除非您需要更改它。例如,markdown-it lib offers that data on the Token.map and/or Token.markup 属性。您仍然需要创建自己的渲染器,但让 Markdown 看起来更像原始渲染器应该更容易。

最后,我没有亲身使用过,也不推荐上面提到的具体Markdown解析器。它们只是各种类型解析器的流行示例,用于演示如何创建解决方案。您可能会找到更适合您需求的不同实现。一个冗长但不完整的 list is here.

您是否考虑过使用 HTML 作为中间格式?一旦进入 HTML,header 类型之间的差异将无法区分,因此 Markdown -> HTML 转换将有效地为您标准化它们。有很多 markdown -> HTML 转换器,还有一些 HTML -> markdown.

我使用这两个包组合了一个示例:

我不知道你在这里是否有任何性能要求(阅读:这很慢......)但这是一个非常低投资的解决方案。看看:

var md = require('markdown-it')(),
    h2m = require('h2m');

var mdContent = `
\`\`\`
# unaffected #
\`\`\`

# H1 #

H1
==

## H2 ##

H2
--

### H3 ###
`;

var htmlContent = md.render(mdContent);
var newMdContent = h2m(htmlContent, {converter: 'MarkdownExtra'});
console.log(newMdContent);

您可能需要混合使用各种组件才能获得正确的方言支持等等。我尝试了一堆,但无法完全匹配您的输出。我想也许 -- 有不同的解释?这是输出,我会让你决定它是否足够好:

```
# unaffected #

```

# H1 #

# H1 #

## H2 ##

## H2 ##

### H3 ###

也许答案不完整。 将不受影响的复制到其他文件中。

然后全部替换

  1. #space##space

  2. space#space##