Docker フォーラムで、折り返しとスクロールの両方に対する行番号の実験を行っています。まだライブではありませんが、何かご意見があれば教えていただきたいです。まず、いくつかのスクリーンショットをご覧ください:
折り返しされたコンテンツ
上記のように、行番号によってどこで一行が終わり、次の行が始まるかを識別しやすくなります。もちろん、他の表示方法もありますが、このトピックは行番号に焦点を当てています。
スクロールされたコンテンツ
隠れているコンテンツの側(複数可)にパディングを制限し、スクロールすることでさらに表示されることを示す方法:
または、水平方向に隠れているコンテンツの位置を示すためにシャドウを使用する方法:
一部からのフィードバックでは、小さなシャドウがスクロールバーと混同される可能性があるため、異なるスタイルが望ましいとの意見がありました。
クライアント側での行番号の追加
水平スクロールを使用する場合、コンテンツの各行は画面上一行を占有します。したがって、理論的には JavaScript を使用して垂直方向の <div> を注入し、行番号の範囲を表示し、スクロールするコンテンツの隣に配置することが可能です。しかし、折り返しの場合は、単一の行番号が複数のコンテンツ行に適用されるため、CSS の span::before と content: 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>