开发者 Markdown 扩展指南

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

以下是一些开发笔记,可以帮助你修复核心代码中的 bug 或创建新的插件。

基础

Discourse 仅在引擎之上提供了一些辅助功能,因此需要学习的大部分内容都是理解 Markdown It。

文档目录 包含当前的文档。

我强烈建议阅读以下内容:

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

如果我在处理内联(inline)规则,我会思考哪些现有的内联规则与之类似,并基于它们来开展工作。

请记住,有时你只需更改渲染器(renderer)即可获得所需的功能,这通常要简单得多。

如何构建扩展?

当 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, ...]): 此方法用于通过我们的净化器(sanitizer)允许特定的 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 特定扩展

在 Ruby 中生成标签引用

使用 HashtagAutocompleteService#hashtags_for 来为生成的 Markdown 构建引用:

HashtagAutocompleteService.new(user.guardian).hashtags_for("tag", ["bug", "support"]).join(" ")

如果一个可见类别的 slug 是 bug,结果将是 #bug::tag #support
传入已经针对查看者可见性过滤过的名称。顺序和大小写保持不变,冲突会与更高优先级的类型进行检查,如果标签规则无法匹配某个名称,则回退为纯文本。

BBCode

Discourse 包含两个你可以用于自定义 BBCode 标签的 ruler(规则器)。一个是内联级别,另一个是块级(block level)。

内联 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 class="bbcode-u">test</span>

内联规则可以包裹(wrap)或替换(replace)文本。在包裹时,你还可以传入一个函数以获得额外的灵活性。

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 内联的内容

利用这些信息,你可以处理各种包裹需求。

偶尔你可能想替换整个 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 上建议更改。

36 个赞

这能否在主题组件中使用?还是仅限插件使用?