Guia do desenvolvedor para extensões de Markdown

O Discourse usa um motor de 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.

Os Fundamentos

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

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

Recomendo fortemente a leitura de:

Enquanto desenvolvo extensões para o motor, geralmente abro um segundo editor olhando para 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 da mesma forma e baseio meu trabalho nela.

Lembre-se, às vezes você pode se sair bem apenas alterando um renderer para obter a funcionalidade desejada, o que geralmente é muito mais fácil.

Como estruturar uma extensão?

Quando o motor de markdown é inicializado, ele percorre todos os módulos.

Se qualquer módulo for chamado /discourse-markdown\/|markdown-it\// (ou seja, estiver em um diretório discourse-markdown ou markdown-it), ele será um candidato para inicialização.

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

O protocolo de setup

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

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

Um método setup tem acesso a um objeto helper que pode ser usado para inicialização. Ele 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 reversa adequada, você deseja verificá-la.

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

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

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

Juntando tudo

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

Gerando referências de hashtag em Ruby

Use HashtagAutocompleteService#hashtags_for para construir referências para o Markdown gerado:

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

Se uma categoria visível tiver o slug bug, o resultado será #bug::tag #support.
Passar nomes já filtrados para a visibilidade do espectador. A ordem e a caixa (maiúsculas/minúsculas) são
preservadas, as colisões são verificadas contra tipos de maior prioridade, e um nome que a
regra de hashtag não consegue corresponder volta para texto simples.

BBCode

O Discourse contém 2 rulers que você pode usar para tags BBCode personalizadas. Um ruler de nível inline e um de nível de bloco.

Regras bbcode inline são aquelas que ficam 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ções 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

Há muitos exemplos de regras inline em: bbcode-inline.js

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

Regras de BBCode inline

Regras de 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",
});

Isso causará

test [u]test[/u]

A ser convertido em:

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

Regras inline podem envolver ou substituir texto. Ao envolver, você também pode passar uma função para ganhar 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 remover a tag bbcode
      endToken.content = "";
      startToken.content = "";

      // caso de borda, não queremos que isso seja detectado como um onebox se for auto linkado
      // 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 chaves/valores especificados 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 bbcode inline

Usando essas informações, você pode atender a 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 [code]bloco de código[code] inteiro por um único token code_inline.

Regras de BBCode de bloco

Regras de 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]
hello
[/happy]

se tornará

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

A função de envolvimento tem uma API ligeiramente diferente porque 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]
**test**
[/money]

Se tornará

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

Você pode ganhar controle total sobre a renderização de blocos com as regras before e after, isso permite que você faça coisas como aninhar duplamente uma tag 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]
test
[/ddiv]

se tornará

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

Lidando com substituições de texto

O Discourse vem com uma regra especial extra do núcleo 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);
  },
});
I like cars and buses

Se tornará

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

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

36 Curtiram

Isso pode/será usado em um Componente de Tema? Ou é exclusivo para plugins?