Guia do desenvolvedor para extensões Markdown

O Discourse usa um motor Markdown chamado Markdown-it.

Aqui estão algumas notas de desenvolvimento que ajudarão você a corrigir bugs no núcleo ou a criar seus novos plugins.

O Básico

O Discourse contém apenas alguns auxiliares sobre o motor, então a grande maioria do aprendizado necessário é entender o Markdown It.

O diretório docs contém a documentação atual.

Eu recomendo fortemente a leitura:

Enquanto desenvolvo extensões para o motor, geralmente abro um segundo editor olhando as regras existentes. O motor consiste em uma longa lista de regras e cada regra está em um arquivo dedicado que é razoavelmente fácil de seguir.

Se estou trabalhando em uma regra inline, penso em qual regra inline existente funciona mais ou menos como ela e baseio meu trabalho nela.

Tenha em mente que, às vezes, você pode se safar apenas alterando um renderizador para obter a funcionalidade desejada, o que geralmente é muito mais fácil.

Como estruturar uma extensão?

Quando o motor Markdown é inicializado, ele procura por todos os módulos.

Se algum módulo for chamado /discourse-markdown\\/|markdown-it\\// (o que significa que ele reside em um diretório discourse-markdown ou markdown-it), ele será um candidato à inicialização.

Se o módulo exportar um método chamado setup, ele será chamado pelo motor durante a inicialização.

O protocolo de configuração (setup)

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

export function setup(helper) {
  // ... seu código vai aqui
}

Um método setup obtém acesso a um objeto auxiliar que pode ser usado para inicialização. Este contém os seguintes métodos e variáveis:

  • bool markdownIt : esta propriedade é definida como true quando o novo motor está em uso. Para uma compatibilidade com versões anteriores adequada, você deve verificar isso.

  • registerOptions(cb(opts,siteSettings,state)) : a função fornecida é chamada antes que o motor Markdown seja inicializado, você pode usá-la para determinar se deve habilitar ou desabilitar o motor.

  • allowList([spec, ...]): este método é usado para permitir explicitamente (allowlist) HTML com nosso sanitizador.

  • registerPlugin(func(md)): este método é usado para registrar um plugin Markdown It.

Juntando tudo

function amazingMarkdownItInline(state, silent) {
   // extensão inline markdown it padrão vai aqui.
   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);
   });
}

Extensões específicas do Discourse

BBCode

O Discourse contém 2 rulers (regras) que você pode usar para tags BBCode personalizadas. Uma regra de nível inline e outra de nível de bloco.

Regras bbcode inline são aquelas que residem em um parágrafo inline como [b]negrito[/b]

Regras de nível de bloco se aplicam a múltiplas linhas de texto como:

[poll]
- opção 1

- opção 2
[/poll]

md.inline.bbcode.ruler contém uma lista de regras inline que são aplicadas em ordem.

md.block.bbcode.ruler contém uma lista de regras de nível de bloco

Existem muitos exemplos para regras inline em: bbcode-inline.js

Citações e enquetes são bons exemplos de regras de bloco bbcode.

Regras BBCode Inline

Regras BBCode inline são um objeto contendo informações sobre como lidar com uma tag.

Por exemplo:

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

Causará

teste [u]teste[/u]

Para ser convertido em:

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

Regras inline podem envolver (wrap) ou substituir (replace) texto. Ao envolver, você também pode passar uma função para obter flexibilidade extra.

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 {
      // apenas remove a tag bbcode
      endToken.content = "";
      startToken.content = "";

      // caso extremo, não queremos que isso seja detectado como um onebox se for linkado automaticamente
      // isso garante que não seja removido
      startToken.type = "html_inline";
    }

    return false;
  },
});

A função de envolvimento fornece acesso a:

  • O tagInfo, que é um dicionário de chave/valor especificado via bbcode.

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

  • O token que inicia o inline

  • O token que finaliza o inline

  • O conteúdo do inline bbcode

Usando essas informações, você pode lidar com todos os tipos de necessidades de envolvimento.

Ocasionalmente, você pode querer substituir todo o bloco BBCode; para isso, você pode usar 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;
  },
});

Neste caso, estamos substituindo um bloco inteiro [code]bloco de código[code] por um único token code_inline.

Regras BBCode de Bloco

Regras bbcode de bloco permitem que você substitua um bloco inteiro. As APIs de bloco são as mesmas para casos simples:

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

se tornará

<div class="happy">olá</div>

O wrapper de função tem uma API ligeiramente diferente, pois não há tokens de envolvimento.

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

Se tornará

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

Você pode obter controle total sobre a renderização de blocos com as regras before e after, isso permite que você faça coisas como aninhar uma tag duas vezes e assim por diante.

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]
teste
[/ddiv]

se tornará

<div>
  <div>teste</div>
</div>

Lidando com substituições de texto

O Discourse envia uma regra principal especial extra para aplicar expressões regulares ao texto.

md.core.textPostProcess.ruler

Para usar:

md.core.textPostProcess.ruler.push("onlyfastcars", {
  matcher: /(car)|(bus)/, //flags de regex NÃO são suportadas
  onMatch: function (buffer, matches, state) {
    let token = new state.Token("text", "", 0);
    token.content = "fast " + matches[0];
    buffer.push(token);
  },
});
Eu gosto de carros e ônibus

Se tornará

<p>Eu gosto de fast carros e fast ônibus</p>

Este documento é controlado por versão - sugira alterações no github.

35 curtidas