代码块中可以显示行号吗?

Hi, I use markdown (mostly, this is Typora software to create and export to PDF) to create documentation. I use the following code to show line numbers in code blocks:

{.language .numberLines}

Example code:

изображение

result:

изображение

Also I use a forum to publish announcements and I would like to share some code in forum posts. But seems this trick doesn’t work in Discourse :(. Is there any way to show line numbers in code blocks of the forum?

9 个赞

We use highlight.js for our codeblocks, and I don’t believe they support line numbers… there’s some rationale in their documentation here: Line numbers — highlight.js 11.2.0 documentation

They can definitely be useful for reference, e.g., “you can change the font size on line 3”… so if we’re ever looking to move away from highlight.js, that would be one thing to consider.

10 个赞

Hi Kris, thank you very much for the reference!

I tried to apply a plugin via theme customization but without success. Probably somebody who knows well how to use custom JS/CSS in theme could help :slight_smile: .

1 个赞

是的。另外,在实际换行的安装中(而不是触发水平滚动),显示行号可以清楚地表明长行已换行。

I’ve been experimenting with line numbers for both wrapping and scrolling on the Docker forum. This is not live yet, but I’d like to know if someone has any comments. So, some screenshots first:

Wrapped content

Above, line numbers help identifying when one line stops and another starts. Of course, there are other ways to render that, but this topic is about line numbers. :nerd_face:

Scrolled content

With limited padding on the side(s) where content is hidden, to indicate that scrolling will reveal more:

Or with shadows to indicate where content is hidden horizontally:

Some feedback I got showed that the small shadows may be confused with scrollbars, so different styling may be great.

Adding the line numbers client-side

When using horizontal scrolling, each line of content will use one line on the screen. So, in theory one could use JavaScript to inject a vertical <div> with a range of line numbers, and position that next to the scrolling content. But when wrapping, a single line number may apply to multiple lines of content, for which using CSS span::before along with content: counter(...) seems easier. That is what is used here.

To use ::before, each line in the preformatted code block first needs some parent element, like being wrapped in a <span> using api.decorateCookedElement. This needs the latter to run before highlight.js runs, as sometimes highlight.js does not close tags on the same line it opened them. For example, when it thinks something is Mustache/Handlebars:

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

The above yields the closing </span> on the next line:

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

Of course, this may be a temporary bug which I probably should report. But regardless, better be safe. And the good news: it seems, today, api.decorateCookedElement(...) indeed runs prior to highlight.js.

The next JavaScript wraps each line in a <span> and adds CSS class lines-scroll or lines-wrap to indicate what style is wanted (one could even let the user toggle that). It also adds lines-shadow to enable the scroll shadows. And it adds some CSS classes to the parent <pre> and <code> to support styling, and to ensure things are only ever decorated once.

Theme JavaScript decorator

The following also enables line numbers and the other styling in the editor preview. Change the last line to include onlyStream: true to disable that:

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

Add all of the following to your component’s “Common, Head” in /admin/customize/themes/:

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // Combinations of 'number', 'wrap', 'scroll' and/or '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 uses `<aside>` for quotes of other forum posts
      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(' ');

      // Assume we don't have any event listeners on the line,
      // and trim to hide trailing blank lines
      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 styling

The following supports wrapping with and without line numbering, and scrolling with or without line numbering and/or shadows. It also suppresses line numbering when there is just a single line.

This does not add line numbers to quotes of other posts, as if such quote is just a partial quote, it will still start numbering with 1. To add numbering to quotes, remove the :not(.lines-in-quote) from the line below (which occurs multiple times in the SCSS):

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

The above is also where the numbering for single-line code blocks is being suppressed.

Theme Sass styling

Add this to your component’s “Common, CSS” in /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);
  // No padding for outer PRE:
  // - scrollbars not covering CODE but close to outermost edges
  // - text close to the edges when there is hidden content
  // - likewise for the shadows
  padding: 0;
  // #f5f5f5 in `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 {
    // Minimum height to get border and background for empty lines (Firefox
    // only needs this for the ::before, but Chrome also for the span)
    min-height: 1.5em;

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

  &.lines-shadow {
    background-image:
      // First two covers that scroll with the content, hence appearing
      // in front of the shadows when no scrolling is possible. These
      // match the background color for the first 25%, and next a gradient
      // to full transparency (hence assuming the covers are 4 times the
      // width of the shadows).
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // And behind that two shadows at fixed positions, to indicate
      // scrollable, hidden content if not covered.
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // Or:
      // 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:
      // Set the cover to 4 times the width of the actual shadow, to match
      // the 25% stop in the gradient of the cover
      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) {
      // If line numbers are visible, we need to move the shadow
      --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);
    }
  }
}

Development

For development I used the next code, allowing to set URL query parameters such as ?avbPreview to enable the JavaScript decorator hook, and things like ?avbPreview&avbClasses=number,scroll to test line numbers with scrolling but no shadows. As all CSS is scoped, this should not affect other users during testing.

Development/debug version
<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 uses `<aside>` for quotes of other forum posts
      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>`));
      }

      // Assume we don't have any event listeners on the line,
      // and trim to hide trailing blank lines
      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>
7 个赞

做得太棒了! :clap: :clap: :clap:

非常感谢 Arjan!在默认的浅色和深色主题下都能正常工作。


изображение

分享我的配置:
css.txt (5.7 KB)
head.txt (1.2 KB)

CSS 自定义:

// 行内代码颜色
// 绿色的点赞
// 绿色的分配人图标
// 灰色或蓝色的标题
// 居中图片
// 代码块中的文字换行 + 全部内容
// 修复过大的主题边距
// Gitlab 主题组件字体颜色修复(更深)
// 原始 <kbd> 覆盖 Gitlab Markdown 风格主题组件
// @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:

1 个赞

我已经编辑了我的帖子,在 SCSS 中进行了修复,以正确应用阴影覆盖。现在,这允许将 --lines-shadow-width 设置为任何大小,同时仍然能够正确地淡入/淡出。

它现在还包括一个 radial-gradient 的示例。但这对于高代码块不会有太大改变:

1 个赞

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.