コードブロックで行番号を表示することは可能ですか?

こんにちは、私は Markdown(主に Typora ソフトウェアを使用して作成し、PDF としてエクスポートしています)を使ってドキュメントを作成しています。コードブロックに行番号を表示するために、以下のコードを使用しています。

{.language .numberLines}

例:

結果:

また、お知らせを投稿するためにフォーラムも利用しており、フォーラムの投稿でもコードを共有したいと考えています。しかし、このテクニックは Discourse では機能しないようです。フォーラムのコードブロックに行番号を表示する方法はありますか?

コードブロックには highlight.js を使用しており、行番号のサポートはないと考えています。その理由については、こちらのドキュメントに記載されています:Line numbers — highlight.js 11.9.0 documentation

行番号は「3 行目のフォントサイズを変更できます」のように参照として非常に役立ちます。そのため、将来的に highlight.js から移行を検討する際は、その点を考慮すべきでしょう。

こんにちは、クリスさん。ご参照ありがとうございます!

テーマのカスタマイズを通じて このプラグイン を適用しようと試みたのですが、うまくいきませんでした。おそらく、テーマでのカスタム JS/CSS の使い方に詳しい方ならお力になれるかもしれませんね :slight_smile:

はい。また、横スクロールをトリガーするのではなく、実際に改行されるインストールでは、行番号が表示されることで、長い行が折り返されていることが明確になります。

Docker フォーラムで、折り返しとスクロールの両方に対する行番号の実験を行っています。まだライブではありませんが、何かご意見があれば教えていただきたいです。まず、いくつかのスクリーンショットをご覧ください:

折り返しされたコンテンツ

上記のように、行番号によってどこで一行が終わり、次の行が始まるかを識別しやすくなります。もちろん、他の表示方法もありますが、このトピックは行番号に焦点を当てています。:nerd_face:

スクロールされたコンテンツ

隠れているコンテンツの側(複数可)にパディングを制限し、スクロールすることでさらに表示されることを示す方法:

または、水平方向に隠れているコンテンツの位置を示すためにシャドウを使用する方法:

一部からのフィードバックでは、小さなシャドウがスクロールバーと混同される可能性があるため、異なるスタイルが望ましいとの意見がありました。

クライアント側での行番号の追加

水平スクロールを使用する場合、コンテンツの各行は画面上一行を占有します。したがって、理論的には JavaScript を使用して垂直方向の <div> を注入し、行番号の範囲を表示し、スクロールするコンテンツの隣に配置することが可能です。しかし、折り返しの場合は、単一の行番号が複数のコンテンツ行に適用されるため、CSS の span::beforecontent: counter(...) を併用する方が簡単です。これがここで採用されている方法です。

::before を使用するには、プレフォーマットされたコードブロック内の各行が、まず api.decorateCookedElement を使用して <span> で囲まれるなど、何らかの親要素を持つ必要があります。これは、highlight.js が実行される前に api.decorateCookedElement が実行される必要があるためです。なぜなら、highlight.js は Sometimes 同じ行で開いたタグを閉じないことがあるからです。例えば、Mustache/Handlebars とみなされる場合:

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

上記の結果、閉じタグ </span> が次の行に出力されます:

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

もちろん、これは一時的なバグであり、報告すべきかもしれませんが、とにかく安全策を講じる方が良いでしょう。そして良いニュースは、現在では api.decorateCookedElement(...) が highlight.js より先に実行されているようです。

次の JavaScript は、各行を <span> で囲み、必要なスタイルを示すために CSS クラス lines-scroll または lines-wrap を追加します(ユーザーが切り替えられるようにすることも可能です)。また、スクロールシャドウを有効にするために lines-shadow も追加します。さらに、スタイリングをサポートし、装飾が一度だけ行われるようにするために、親 <pre><code> にいくつかの CSS クラスを追加します。

テーマの JavaScript デコレーター

以下のコードにより、エディタのプレビューでも行番号と他のスタイリングが有効になります。最後の行を変更して onlyStream: true を含めると、それを無効にできます:

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

以下のすべてを /admin/customize/themes/ 内のコンポーネントの「Common, Head」に追加してください:

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // 'number', 'wrap', 'scroll' および/または '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;

      // Discourse は他のフォーラム投稿の引用に `<aside>` を使用します
      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(' ');

      // 行にイベントリスナーがないと仮定し、末尾の空白行を隠すために trim します
      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>

Sass SCSS スタイリング

以下のコードは、行番号の有無にかかわらず折り返し、行番号および/またはシャドウの有無にかかわらずスクロールをサポートします。また、単一行のコードブロックでは行番号を抑制します。

これは、他の投稿の引用には行番号を追加しません。なぜなら、そのような引用が部分的な引用であっても、番号付けは 1 から始まるからです。引用に行番号を追加するには、以下の行(SCSS に複数回登場します)から :not(.lines-in-quote) を削除してください:

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

上記は、単一行のコードブロックの番号付けを抑制する場所でもあります。

テーマの Sass スタイリング

以下のコードを /admin/customize/themes/ 内のコンポーネントの「Common, CSS」に追加してください:

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);
  // 外部 PRE にはパディングなし:
  // - スクロールバーが CODE を覆わず、最も外側の端に近い
  // - 隠れたコンテンツがある場合、テキストが端に近い
  // - シャドウについても同様
  padding: 0;
  // `code.less` 内の #f5f5f5
  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 {
    // 空の行の境界と背景を取得するための最小高さ(Firefox は `::before` のみ必要ですが、Chrome も span に対して必要です)
    min-height: 1.5em;

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

  &.lines-shadow {
    background-image:
      // コンテンツと一緒にスクロールする最初の 2 つのカバー。これにより、スクロール不可能な場合にシャドウの手前に表示されます。
      // これらは最初の 25% で背景色と一致し、次にフル透過へのグラデーションになります(カバーがシャドウの幅の 4 倍であると仮定)。
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // その背後に、スクロール可能な隠れたコンテンツを示すために固定位置に 2 つのシャドウ。
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // または:
      // 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:
      // カバーのグラデーションの 25% の停止点と一致するように、カバーを実際のシャドウの幅の 4 倍に設定
      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) {
      // 行番号が表示される場合、シャドウを移動する必要があります
      --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);
    }
  }
}

開発

開発では、次のコードを使用しました。これにより、?avbPreview のような URL クエリパラメータを設定して JavaScript デコレーターフックを有効にしたり、?avbPreview&avbClasses=number,scroll のように行番号とスクロール(シャドウなし)をテストしたりできます。すべての CSS はスコープされているため、テスト中も他のユーザーには影響しません。

開発/デバッグバージョン
<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;
      // Discourse は他のフォーラム投稿の引用に `<aside>` を使用します
      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('===== Rewriting; source\n' + elem.innerHTML.trim());
        console.log('===== Rewriting; result\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // 行にイベントリスナーがないと仮定し、末尾の空白行を隠すために trim します
      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>

素晴らしい仕事でした!:clap: :clap: :clap:

ありがとうございます、Arjanさん!ライトテーマとダークテーマの両方で問題なく動作しています。


изображение

私の設定を共有します:
css.txt (5.7 KB)
head.txt (1.2 KB)

CSSのカスタマイズ:

// インラインコードの色
// 緑色のサムズアップ
// 緑色の担当者アイコン
// グレーまたは青色のヘッダー
// 画像の中央揃え
// コードブロックのワードラップ + 全コンテンツ
// 大きすぎるトピックのマージンを修正
// Gitlabテーマコンポーネントのフォントカラー修正(より暗く)
// Gitlab Markdown flavorsテーマコンポーネントの元の<kbd>を上書き
// @Arjanによるコードブロックの行番号

インストールされたコンポーネント:

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

素晴らしい、早かったですね!灰色をなくすために --lines-bgcolor: white; を使いたいのだと思います。:sunglasses:

SCSS のシャドウカバーの適用方法を修正するため、投稿を編集しました。これにより、--lines-shadow-width を任意のサイズに設定しても、フェードイン/フェードアウトが適切に行われるようになります。

また、radial-gradient の例も追加されました。ただし、これは縦長のコードブロックではあまり大きな変更はありません。