É possível mostrar números de linha no bloco de código?

Olá, eu uso Markdown (principalmente, uso o software Typora para criar e exportar para PDF) para criar documentação. Uso o seguinte código para mostrar números de linha em blocos de código:

{.language .numberLines}

Exemplo de código:

resultado:

Também uso um fórum para publicar anúncios e gostaria de compartilhar algum código nas postagens do fórum. Mas parece que esse truque não funciona no Discourse :(. Existe alguma maneira de mostrar números de linha em blocos de código do fórum?

Usamos o highlight.js para nossos blocos de código e, ao que me consta, eles não suportam numeração de linhas… há uma justificativa na documentação deles aqui: Line numbers — highlight.js 11.9.0 documentation

Eles podem ser realmente úteis para referência, por exemplo: “você pode alterar o tamanho da fonte na linha 3”… então, se algum dia decidirmos migrar do highlight.js, isso seria um ponto a considerar.

Olá Kris, muito obrigado pela referência!

Tentei aplicar um plugin por meio da personalização do tema, mas sem sucesso. Provavelmente alguém que saiba bem como usar JS/CSS personalizado no tema poderia ajudar :slight_smile:.

Sim. Além disso, em instalações onde as linhas realmente quebram (em vez de acionar a rolagem horizontal), ver os números das linhas deixa claro que linhas longas foram quebradas.

Tenho experimentado com números de linha tanto para quebra quanto para rolagem no fórum do Docker. Isso ainda não está ao vivo, mas gostaria de saber se alguém tem algum comentário. Então, primeiro algumas capturas de tela:

Conteúdo quebrado

Acima, os números de linha ajudam a identificar quando uma linha termina e outra começa. Claro, existem outras maneiras de renderizar isso, mas este tópico trata dos números de linha. :nerd_face:

Conteúdo rolado

Com preenchimento limitado no lado(s) onde o conteúdo está oculto, para indicar que a rolagem revelará mais:

Ou com sombras para indicar onde o conteúdo está oculto horizontalmente:

Algum feedback que recebi mostrou que as pequenas sombras podem ser confundidas com barras de rolagem, então um estilo diferente pode ser ótimo.

Adicionando os números de linha no lado do cliente

Ao usar rolagem horizontal, cada linha de conteúdo ocupará uma linha na tela. Então, em teoria, poderíamos usar JavaScript para injetar um <div> vertical com uma faixa de números de linha e posicioná-lo ao lado do conteúdo com rolagem. Mas, ao quebrar, um único número de linha pode se aplicar a várias linhas de conteúdo, para o que usar span::before em CSS junto com content: counter(...) parece mais fácil. É isso que é usado aqui.

Para usar ::before, cada linha no bloco de código pré-formatado primeiro precisa de algum elemento pai, como ser envolvido em um <span> usando api.decorateCookedElement. Isso requer que o último seja executado antes do highlight.js, pois às vezes o highlight.js não fecha as tags na mesma linha em que as abriu. Por exemplo, quando ele acha que algo é Mustache/Handlebars:

docker image ls --format '{{slice (printf "%s:%s" .Repository .Tag) 0 11}}'

O acima resulta na tag de fechamento </span> na próxima linha:

<code class="lang-handlebars hljs"><span class="xml">docker image ls --format '</span><span class="hljs-template-variable">{{<span class="hljs-name">slice</span> (<span class="hljs-name">printf</span> <span class="hljs-string">"%s:%s"</span> .Repository .Tag) <span class="hljs-number">0</span> <span class="hljs-number">11</span>}}</span><span class="xml">'
</span></code>

Claro, isso pode ser um bug temporário que provavelmente devo relatar. Mas, independentemente disso, é melhor prevenir. E a boa notícia: parece que, hoje, api.decorateCookedElement(...) realmente roda antes do highlight.js.

O próximo JavaScript envolve cada linha em um <span> e adiciona a classe CSS lines-scroll ou lines-wrap para indicar qual estilo é desejado (poderia até deixar o usuário alternar isso). Também adiciona lines-shadow para habilitar as sombras de rolagem. E adiciona algumas classes CSS ao <pre> e <code> pai para suportar estilização e garantir que as coisas sejam decoradas apenas uma vez.

Decorador JavaScript do tema

O seguinte também habilita números de linha e a outra estilização na visualização do editor. Altere a última linha para incluir onlyStream: true para desabilitar isso:

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines', onlyStream: true});

Adicione tudo o seguinte ao “Comum, Cabeçalho” do seu componente em /admin/customize/themes/:

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // Combinações de 'number', 'wrap', 'scroll' e/ou 'shadow'
    const classes = ['decorator', 'number', 'scroll', 'shadow'].map(c => `lines-${c}`);

    const split = /^(.*)$/mg;
    const elems = post.querySelectorAll('pre code:not(.lines-decorator)');
    elems.forEach(elem => {
      const count = elem.innerHTML.trim().match(split).length;

      // O Discourse usa `<aside>` para citações de outras postagens do fórum
      const quote = elem.closest('aside') ? ['lines-in-quote'] : []

      elem.parentElement.classList.add(`lines-count-${count}`, ...classes, ...quote);
      elem.classList.add(`lines-count-${count}`, ...classes, ...quote);
      const lineClass = ['lines-line', ...quote].join(' ');

      // Suponha que não tenhamos nenhum ouvinte de evento na linha,
      // e trim para ocultar linhas em branco finais
      elem.innerHTML = elem.innerHTML.trim().replace(/^(.*)$/mg, `<span class="${lineClass}">$1</span>`);
    });
  } catch (e) {
    console.error(e);
  }
}

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines'});
</script>

Estilização Sass SCSS

O seguinte suporta quebra com e sem numeração de linhas, e rolagem com ou sem numeração de linhas e/ou sombras. Também suprime a numeração de linhas quando há apenas uma linha.

Isso não adiciona números de linha a citações de outras postagens, pois, se tal citação for apenas uma citação parcial, ela ainda começará a numerar do 1. Para adicionar numeração a citações, remova o :not(.lines-in-quote) da linha abaixo (que ocorre várias vezes no SCSS):

&.lines-number:not(.lines-count-1):not(.lines-in-quote) {

Acima é também onde a numeração para blocos de código de uma única linha está sendo suprimida.

Estilização Sass do tema

Adicione isso ao “Comum, CSS” do seu componente em /admin/customize/themes/:

pre.lines-decorator code.lines-decorator span.lines-line {
  &::before {
    box-sizing: content-box;
  }
}

pre.lines-decorator {
  --lines-bgcolor: var(--hljs-bg);
  --lines-number-width: 2em;
  --lines-number-padding-to-right-border: .5em;
  --lines-shadow-width: .8em;
  --lines-shadow-color: rgba(0, 0, 0, .15);
  // Sem preenchimento para o PRE externo:
  // - barras de rolagem não cobrindo o CODE mas próximas às bordas mais externas
  // - texto próximo às bordas quando há conteúdo oculto
  // - da mesma forma para as sombras
  padding: 0;
  // #f5f5f5 em `code.less`
  background-color: var(--lines-bgcolor);
}

code.lines-decorator {
  padding: 1.5em;

  &.lines-wrap {
    white-space: pre-wrap;

    &:not(.lines-count-1) {
      span.lines-line {
        display: inline-block;
        position: relative;
        padding-left: 3em;
      }
    }
  }

  &.lines-scroll {
    white-space: pre;

    span.lines-line {
      padding-right: 1.5em;

      &::before {
        position: sticky;
        left: 0;
      }
    }
  }

  &.lines-wrap span.lines-line {
    // Altura mínima para obter borda e fundo para linhas vazias (apenas
    // Firefox precisa disso para o ::before, mas Chrome também para o span)
    min-height: 1.5em;

    &::before {
      position: absolute;
      left: 0;
    }
  }

  &.lines-shadow {
    background-image:
      // As duas primeiras coberturas que rolam com o conteúdo, portanto aparecem
      // na frente das sombras quando não há rolagem possível. Essas
      // correspondem à cor de fundo para os primeiros 25%, e depois um gradiente
      // até transparência total (assumindo que as coberturas são 4 vezes a largura
      // das sombras).
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // E atrás disso duas sombras em posições fixas, para indicar
      // conteúdo rolagem, oculto se não coberto.
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // Ou:
      // radial-gradient(farthest-side at 0px 50%, var(--lines-shadow-color), transparent),
      // radial-gradient(farthest-side at 100% 50%, var(--lines-shadow-color), transparent);

    background-size:
      // Defina a cobertura para 4 vezes a largura da sombra real, para corresponder
      // ao ponto de parada de 25% no gradiente da cobertura
      calc(4 * var(--lines-shadow-width)) 100%,
      calc(4 * var(--lines-shadow-width)) 100%,
      var(--lines-shadow-width) 100%,
      var(--lines-shadow-width) 100%;

    --shadow-left: 0;

    &.lines-number:not(.lines-count-1):not(.lines-in-quote) {
      // Se os números de linha estiverem visíveis, precisamos mover a sombra
      --shadow-left: calc(var(--lines-number-width) + var(--lines-number-padding-to-right-border));
    }

    background-position: var(--shadow-left) center, right center, var(--shadow-left) center, right center;

    background-attachment: local, local, scroll, scroll;
    background-repeat: no-repeat;
  }

  &.lines-number:not(.lines-count-1):not(.lines-in-quote) {
    padding-left: 0;
    counter-reset: line-numbering;

    span.lines-line::before {
      display: inline-block;
      content: counter(line-numbering);
      counter-increment: line-numbering;
      border-right: 1px solid #ccc;
      width: var(--lines-number-width);
      padding-right: var(--lines-number-padding-to-right-border);
      margin-right: .5em;
      height: 100%;
      text-align: right;
      font-family: var(--font-family);
      color: var(--primary-medium);
      background-color: var(--lines-bgcolor);
    }
  }
}

Desenvolvimento

Para desenvolvimento, usei o próximo código, permitindo definir parâmetros de consulta de URL, como ?avbPreview para habilitar o gancho do decorador JavaScript, e coisas como ?avbPreview&avbClasses=number,scroll para testar números de linha com rolagem mas sem sombras. Como todo o CSS é escopado, isso não deve afetar outros usuários durante os testes.

Versão de desenvolvimento/debug
<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    const params = new URL(document.location.href).searchParams;
    const preview = params.has('avbPreview');
    if (!preview) {
      return;
    }
    const classes = ['decorator'].concat(
      (params.get('avbClasses') || 'number,scroll,shadow').split(',')).map(c => `lines-${c}`);
    const debug = params.has('avbDebug');
    const split = /^(.*)$/mg;
    const elems = post.querySelectorAll('pre code:not(.lines-decorator)');
    elems.forEach(elem => {
      const count = elem.innerHTML.trim().match(split).length;
      // O Discourse usa `<aside>` para citações de outras postagens do fórum
      const quote = elem.closest('aside') ? ['lines-in-quote'] : []
      elem.parentElement.classList.add(`lines-count-${count}`, ...classes, ...quote);
      elem.classList.add(`lines-count-${count}`, ...classes, ...quote);
      const lineClass = ['lines-line', ...quote].join(' ');

      if (debug) {
        console.log('===== Reescrevendo; fonte\n' + elem.innerHTML.trim());
        console.log('===== Reescrevendo; resultado\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // Suponha que não tenhamos nenhum ouvinte de evento na linha,
      // e trim para ocultar linhas em branco finais
      elem.innerHTML = elem.innerHTML.trim().replace(/^(.*)$/mg, `<span class="${lineClass}">$1</span>`);
    });
  } catch (e) {
    console.error(e);
  }
}

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines'});
</script>

Trabalho fantástico foi feito! :clap: :clap: :clap:

Muito obrigado, Arjan! Tudo funciona tanto no tema claro padrão quanto no escuro.


изображение

Compartilhe minha configuração:
css.txt (5.7 KB)
head.txt (1.2 KB)

Customizações CSS:

// Cores inline do código
// Polegar verde para cima
// Ícone de atribuído verde
// Cabeçalhos cinza ou azuis
// Centralizar imagens
// Quebra de linha em blocos de código + conteúdo completo
// Corrigir margens enormes de tópicos
// Cor de fonte do componente de tema do Gitlab (mais escuro)
// Sabores de Markdown do Gitlab original <kbd> sobre componente de tema
// Numeração de linha em blocos de código por @Arjan

Componentes instalados:

https://github.com/discourse/discourse-search-banner
https://github.com/keegangeorge/discourse-markdown-flavors.git
https://github.com/pacharanero/discourse-topic-width-desktop.git

Legal, foi rápido! Acho que você vai querer usar --lines-bgcolor: white; para se livrar de alguns cinzas. :sunglasses:

Editei minha postagem para um ajuste no SCSS, para aplicar corretamente as sombras. Isso agora permite definir --lines-shadow-width para qualquer tamanho, mantendo o fade in/fade out corretos.

Também inclui um exemplo para radial-gradient agora. Mas isso não mudará muito para blocos de código altos: