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

您好,我使用 Markdown(主要是 Typora 软件来创建并导出为 PDF)编写文档。我使用以下代码在代码块中显示行号:

{.language .numberLines}

示例代码:

结果:

此外,我还使用论坛发布公告,并希望能在论坛帖子中分享一些代码。但似乎这个技巧在 Discourse 中不起作用。请问有什么方法可以在论坛的代码块中显示行号吗?

我们使用 highlight.js 来处理代码块,据我所知,它不支持行号……其文档中对此有相关说明:Line numbers — highlight.js 11.9.0 documentation

行号确实很有参考价值,例如“您可以更改第 3 行的字体大小”……因此,如果我们未来考虑更换 highlight.js,这一点值得纳入考量。

你好,Kris,非常感谢你的参考!

我尝试通过主题自定义应用 一个插件,但未成功。或许有人熟悉如何在主题中使用自定义 JS/CSS,可以提供帮助 :slight_smile:

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

我最近在 Docker 论坛上尝试为换行和滚动内容添加行号。目前功能尚未上线,但我想听听大家的意见。先附上一些截图:

换行内容

如上所示,行号有助于识别一行何时结束、下一行何时开始。当然,还有其他渲染方式,但本主题聚焦于行号。:nerd_face:

滚动内容

在内容被隐藏的一侧或两侧设置较小的内边距,以提示滚动后可显示更多内容:

或使用阴影来指示水平方向上被隐藏的内容:

收到的部分反馈指出,较小的阴影可能与滚动条混淆,因此可能需要不同的样式设计。

在客户端添加行号

当使用水平滚动时,每一行内容在屏幕上都会占据一行。理论上,可以使用 JavaScript 注入一个包含行号范围的垂直 <div>,并将其定位在滚动内容旁边。但在换行模式下,单个行号可能对应多行内容,此时使用 CSS 的 span::before 配合 content: counter(...) 似乎更简便。本文采用的就是这种方法。

要使用 ::before,预格式化代码块中的每一行首先需要一个父元素,例如通过 api.decorateCookedElement 将其包裹在 <span> 中。这需要该函数在 highlight.js 运行之前执行,因为有时 highlight.js 不会在同一行内闭合它打开的标签。例如,当它认为某段代码是 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>

当然,这可能是一个临时性 Bug,我或许应该上报。但无论如何,谨慎为上。好消息是:目前看来,api.decorateCookedElement(...) 确实会在 highlight.js 之前执行。

接下来的 JavaScript 代码会将每一行包裹在 <span> 中,并添加 CSS 类 lines-scrolllines-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(' ');

      // 假设行上没有事件监听器,
      // 并修剪以隐藏末尾的空行
      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:
      // 前两个覆盖层随内容滚动,因此在无法滚动时出现在阴影前方。
      // 这些覆盖层前 25% 与背景色匹配,随后渐变为完全透明
      //(假设覆盖层宽度是阴影宽度的 4 倍)。
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // 其后是两个固定位置的阴影,用于指示可滚动但被隐藏的内容(若未被覆盖)。
      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:
      // 将覆盖层设置为实际阴影宽度的 4 倍,以匹配覆盖层渐变中的 25% 停止点
      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);
    }
  }
}

开发

开发时我使用了以下代码,允许设置 URL 查询参数,例如 ?avbPreview 以启用 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('===== 重写;源\n' + elem.innerHTML.trim());
        console.log('===== 重写;结果\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // 假设行上没有事件监听器,
      // 并修剪以隐藏末尾的空行
      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 主题组件字体颜色修复(更深)
// 原始 <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:

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

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