开发者 Markdown 扩展指南

Discourse 使用一个名为 Markdown-it 的 Markdown 引擎。

以下是一些开发笔记,它们将帮助您修复核心错误或创建新的插件。

基础知识

Discourse 在引擎之上只包含一些辅助函数,因此需要学习的大部分内容是理解 Markdown It。

docs 目录 包含当前的文档。

我强烈建议阅读:

  • 架构文档,以从顶层了解引擎的工作原理。

  • 开发,了解基本开发指南。

  • API 文档,获取非常详细的参考。

  • 最后是源代码,它有很好的文档记录且清晰易懂。

当我对引擎进行扩展开发时,我通常会打开第二个编辑器,查看现有的规则。引擎由一长串规则组成,每条规则都在一个专用的文件中,相当容易理解。

如果我正在处理一个行内规则,我会考虑哪个现有的行内规则与它或多或少相似,并以此为基础进行工作。

请记住,有时您可以通过更改渲染器来实现所需的功能,这通常要容易得多。

如何构建一个扩展?

当 Markdown 引擎初始化时,它会搜索所有模块。

如果任何模块的名称匹配 /discourse-markdown\\/|markdown-it\\//(意味着它位于 discourse-markdown 或 markdown-it 目录中),它将成为初始化的候选对象。

如果模块 导出 了名为 setup 的方法,引擎将在初始化期间调用它。

setup 协议

/my-plugins/assets/javascripts/discourse-markdown/awesome-extension.js

export function setup(helper) {
  // ... 您的代码放在这里
}

setup 方法会获得一个可以用于初始化的 helper 对象。它包含以下方法和变量:

  • bool markdownIt:当使用新的引擎时,此属性设置为 true。为了正确向后兼容,您需要检查它。

  • registerOptions(cb(opts,siteSettings,state)):提供的函数在 Markdown 引擎初始化之前被调用,您可以使用它来决定启用或禁用引擎。

  • allowList([spec, ...]):此方法用于将 HTML 列入白名单,以便我们的清理程序处理。

  • registerPlugin(func(md)):此方法用于注册一个 Markdown It 插件

整合所有内容

function amazingMarkdownItInline(state, silent) {
   // 标准的 markdown it 行内扩展放在这里。
   return false;
}

export function setup(helper) {
   if(!helper.markdownIt) { return; }

   helper.registerOptions((opts,siteSettings)=>{
      opts.features.['my_extension'] = !!siteSettings.my_extension_enabled;
   });

   helper.allowList(['span.amazing', 'div.amazing']);

   helper.registerPlugin(md=>{
      md.inline.push('amazing', amazingMarkdownItInline);
   });
}

Discourse 特定的扩展

BBCode

Discourse 包含 2 个您可以用于自定义 BBCode 标签的规则器。一个行内和一个块级规则器。

行内 bbcode 规则是存在于行内段落中的,例如 [b]bold[/b]

块级规则适用于多行文本,例如:

[poll]
- option 1

- options 2
[/poll]

md.inline.bbcode.ruler 保存按顺序应用的行内规则列表。

md.block.bbcode.ruler 保存块级规则列表。

行内规则的许多示例位于:bbcode-inline.js

引用 和投票是 bbcode 块规则的好例子。

行内 BBCode 规则

行内 BBCode 规则是一个包含如何处理标签信息的对象。

例如:

md.inline.bbcode.ruler.push("underline", {
  tag: "u",
  wrap: "span.bbcode-u",
});

将导致

test [u]test[/u]

被转换为:

test <span>test</span>

行内规则可以包裹或替换文本。当包裹时,您也可以传入一个函数以获得额外的灵活性。

md.inline.bbcode.ruler.push("url", {
  tag: "url",
  wrap: function (startToken, endToken, tagInfo, content) {
    const url = (tagInfo.attrs["_default"] || content).trim();

    if (simpleUrlRegex.test(url)) {
      startToken.type = "link_open";
      startToken.tag = "a";
      startToken.attrs = [
        ["href", url],
        ["data-bbcode", "true"],
      ];
      startToken.content = "";
      startToken.nesting = 1;

      endToken.type = "link_close";
      endToken.tag = "a";
      endToken.content = "";
      endToken.nesting = -1;
    } else {
      // 只移除 bbcode 标签
      endToken.content = "";
      startToken.content = "";

      // 边缘情况,我们不希望它在自动链接时被检测为 onebox
      // 这确保它不会被剥离
      startToken.type = "html_inline";
    }

    return false;
  },
});

包裹函数提供了对以下内容的访问:

  • tagInfo,它是通过 bbcode 指定的键/值字典。

    [test=testing]{_default: "testing"}
    [test a=1]{a: "1"}

  • 开始行内的 token

  • 结束行内的 token

  • bbcode 行内内容的 content

利用这些信息,您可以处理所有类型的包裹需求。

偶尔您可能想替换整个 BBCode 块,为此您可以使用 replace

md.inline.bbcode.ruler.push("code", {
  tag: "code",
  replace: function (state, tagInfo, content) {
    let token;
    token = state.push("code_inline", "code", 0);
    token.content = content;
    return true;
  },
});

在这种情况下,我们将整个 [code]code block[code] 替换为单个 code_inline token。

块级 BBCode 规则

块级 bbcode 规则允许您替换整个块。对于简单情况,块 API 相同:

md.block.bbcode.ruler.push("happy", {
  tag: "happy",
  wrap: "div.happy",
});
[happy]
hello
[/happy]

将变为

<div class="happy">hello</div>

函数包装器具有略微不同的 API,因为没有包裹 token。

md.block.bbcode.ruler.push("money", {
  tag: "money",
  wrap: function (token, tagInfo) {
    token.attrs = [["data-money", tagInfo.attrs["_default"]]];
    return true;
  },
});
[money=100]
**test**
[/money]

将变为

<div data-money="100">
  <b>test</b>
</div>

您可以通过 beforeafter 规则获得对块渲染的完全控制,这允许您执行诸如双重嵌套标签之类的事情。

md.block.bbcode.ruler.push("ddiv", {
  tag: "ddiv",
  before: function (state, tagInfo) {
    state.push("div_open", "div", 1);
    state.push("div_open", "div", 1);
  },
  after: function (state) {
    state.push("div_close", "div", -1);
    state.push("div_close", "div", -1);
  },
});
[ddiv]
test
[/ddiv]

将变为

<div>
  <div>test</div>
</div>

处理文本替换

Discourse 附带一个额外的特殊核心规则,用于将正则表达式应用于文本。

md.core.textPostProcess.ruler

使用方法:

md.core.textPostProcess.ruler.push("onlyfastcars", {
  matcher: /(car)|(bus)/, //不支持正则表达式标志
  onMatch: function (buffer, matches, state) {
    let token = new state.Token("text", "", 0);
    token.content = "fast " + matches[0];
    buffer.push(token);
  },
});
I like cars and buses

将变为

<p>I like fast cars and fast buses</p>

此文档是版本控制的 - 在 github 上建议更改。

35 个赞