Руководство разработчика по расширениям Markdown

Discourse использует движок Markdown под названием Markdown-it.

Ниже приведены некоторые замечания для разработчиков, которые помогут вам исправлять ошибки в ядре или создавать новые плагины.

Основы

Discourse содержит лишь несколько вспомогательных функций поверх движка, поэтому основная часть работы по изучению материала заключается в понимании Markdown It.

Каталог с документацией содержит актуальную документацию.

Я настоятельно рекомендую прочитать:

Во время разработки расширений для движка я обычно открываю второй редактор, чтобы посмотреть на существующие правила. Движок состоит из длинного списка правил, и каждое правило находится в отдельном файле, который довольно легко читать.

Если я работаю над inline-правилом, я думаю о том, какое из существующих inline-правил работает примерно так же, и использую его в качестве основы для своей работы.

Имейте в виду, что иногда можно обойтись простой заменой рендерера, чтобы получить желаемую функциональность, что обычно гораздо проще.

Как структурировать расширение?

При инициализации движка 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) {
   // стандартное inline-расширение 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 есть два «правителя» (rulers), которые вы можете использовать для пользовательских тегов BBCode. Один для inline-уровня, другой для блочного уровня.

Inline-правила bbcode — это те, что находятся внутри inline-абзаца, например [b]bold[/b]

Блочные правила применяются к нескольким строкам текста, например:

[poll]
- option 1

- options 2
[/poll]

md.inline.bbcode.ruler содержит список inline-правил, которые применяются последовательно.

md.block.bbcode.ruler содержит список блочных правил.

Множество примеров inline-правил можно найти здесь: bbcode-inline.js

Цитаты и опросы — хорошие примеры блочных правил bbcode.

Inline-правила BBCode

Inline-правила BBCode — это объект, содержащий информацию о том, как обрабатывать тег.

Например:

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

Это приведет к тому, что

test [u]test[/u]

будет преобразовано в:

test <span class="bbcode-u">test</span>

Inline-правила могут либо оборачивать, либо заменять текст. При оборачивании вы также можете передать функцию, чтобы получить дополнительную гибкость.

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

  • Токену, начинающему inline-фрагмент

  • Токену, завершающему inline-фрагмент

  • Содержимому inline-фрагмента 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.

Блочные правила BBCode

Блочные правила bbcode позволяют заменить весь блок. API для блоков такие же, как и для простых случаев:

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

станет

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

У функции-обертки немного другой API, так как там нет токенов оборачивания.

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>

Вы можете получить полный контроль над рендерингом блоков с помощью правил before и after, что позволяет делать такие вещи, как двойное вложение тегов и т. д.

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 лайков