将 markdown-it 扩展重新打包为 Discourse 插件

markdown.it Discourse 使用的 CommonMark 引擎有一个广泛的插件

标题锚点、定义列表、智能箭头等等不胜枚举。

首先是一个警告

:warning: CommonMark 旨在保持……通用。你偏离规范越远,你的 Markdown 就越不通用。这可能会使移植到其他解决方案变得更加困难,并且如果不小心,可能会导致解析中的内部不一致。在重新打包任何东西之前,请务必回答“我真的想重新打包这个吗?”这个问题。

我刚刚完成了对 Discourse Footnote 的重新打包,并分享一些关于如何正确做这件事的经验教训。

懒人步骤

如果你很懒,只想开始,最简单的方法是分叉脚注插件,然后交换文件和变量名。我非常仔细地确保它遵循了最佳实践,所以你应该有一个可靠的例子。

开场白,最小化重新打包

据我所知,大多数 markdown.it 插件都作为纯 JavaScript 文件分发。在许多情况下,插件只是作为一个单独的 JS 文件分发,例如:markdown-it-mark.js

理想情况下,你想保持原始文件不变,这意味着你可以简单地将更新后的文件版本复制到你的插件中,而无需修改现有插件。

你会遇到的第一个问题是,你必须教你的插件在服务器上加载这个 JavaScript,因为 Markdown 引擎也在服务器上运行。要做到这一点,你可以简单地将文件原封不动地复制到 assets/javascripts/vendor/original-plugin.js,然后在你的 plugin.rb 文件中添加:

# 这会教我们的 markdown 引擎加载你的 vanilla js 文件
register_asset "javascripts/vendor/original-plugin.js", :vendored_pretty_text

一旦你包含了插件的实际主体,你需要教我们的管道如何加载和初始化它:

创建一个名为 assets/javascripts/lib/discourse-markdown/your-extension.js 的文件

这个文件将被自动加载,因为它以 .js 结尾并且位于 discourse-markdown 目录中。

一个简单的例子可以是:

export function setup(helper) {
  // 这只允许你在启用站点设置时加载你的扩展
  helper.registerOptions((opts, siteSettings) => {
    opts.features["your-extension"] = !!siteSettings.enable_my_plugin;
  });

  // 白名单任何你需要支持的属性,
  // 否则我们的清理程序会将其剥离
  helper.whiteList(["div.amazingness"]);

  // 你也可以做一些花哨的事情,比如这样
  helper.whiteList({
    custom(tag, name, value) {
      if ((tag === "a" || tag === "li") && name === "id") {
        return !!value.match(/^fn(ref)?\\d+$/);
      }
    },
  });

  // 最后,这是你在我们的管道中注册扩展所使用的魔法
  // whateverGlobal 是插件暴露的全局变量的名称
  // 它接收一个单一的 (md) 变量,然后用于修改管道
  helper.registerPlugin(window.whateverGlobal);
}

始终进行测试

Discourse 的 bin/rake autospec 了解插件 :innocent:

这意味着当你添加文件 spec/pretty_text_spec.rb 时,每次保存它时,插件测试文件都会运行。

我大量使用它,因为它使工作速度快得多。

假设你添加了一个插件,将帖子中的所有数字都更改为 8 个圆圈,你可以称之为 discourse-magic-8-ball。

这是我将如何构建测试:

require "rails_helper"

describe PrettyText do
  it "can be disabled" do
    SiteSetting.enable_magic_8_ball = false

    markdown = <<-MD
      1 thing
    MD

    html = <<-HTML
      <p>1 thing</p>
    HTML

    cooked = PrettyText.cook markdown.strip
    expect(cooked).to eq(html.strip)
  end

  it "supports magic 8 ball" do
    markdown = <<-MD
      1 thing
    MD

    html = <<-HTML
      <p>8 circle thing</p>
    HTML

    cooked = PrettyText.cook markdown.strip
    expect(cooked).to eq(html.strip)
  end
end

你可能需要“装饰帖子”

在某些情况下,插件在向帖子添加额外的“动态”功能时效果最佳。例如 poll 插件或 footnotes 插件,它会添加一个“…”来动态显示工具提示。

如果你需要“装饰”帖子,请添加 assets/javascripts/api-initializers/your-initializer.js

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer((api) => {
  const siteSettings = api.container.lookup("service:site-settings");
  if (!siteSettings.enable_magic_8_ball) {
    return;
  }

  api.decorateCookedElement((elem) => {
    // 你惊人的魔法就在这里
  });
});

你可能需要“后处理”帖子

在某些情况下,你可能需要“后处理”帖子,默认情况下,Markdown 渲染引擎不了解某些信息,例如 post_id。在某些情况下,你可能希望在服务器端访问帖子和“烹饪”后的 HTML,这可以让你执行诸如触发后台作业、同步自定义字段或“更正”自动生成的 HTML 等操作。

对于脚注,我需要为每个脚注提供一个不同的 id,这意味着我需要访问 post_id,因此我被迫在后处理器(在 sidekiq 中运行)中更改 HTML。

要挂入,你需要在 plugin.rb 文件中添加以下内容:

DiscourseEvent.on(:before_post_process_cooked) do |doc, post|
  doc.css("a.always-bing").each do |a|
    # 这应该总是导向 bing
    a["href"] = "https://bing.com"
  end
end

你可能需要一些自定义 CSS

如果你想添加自定义 CSS,请确保在 plugin.rb 中注册该文件。

将你的 CSS 添加到 assets/stylesheets/magic.scss,然后运行

register_asset "stylesheets/magic.scss"

请记住,我们“自动重新加载”更改,因此你可以修改插件 CSS 并在开发中实时看到更改。

祝你的重新打包冒险好运 :four_leaf_clover:


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

29 个赞

我看到一些扩展 Markdown 的 Superset 工具,例如 Quarkdown 似乎非常强大。

有没有办法用 Quarkdown 等工具替换 markdown-it 扩展?