코드 블록에 줄 번호를 표시할 수 있나요?

안녕하세요, 저는 마크다운(주로 Typora 소프트웨어를 사용하여 작성하고 PDF로 내보내는 방식)을 사용하여 문서를 작성합니다. 코드 블록에 줄 번호를 표시하기 위해 다음 코드를 사용합니다:

{.language .numberLines}

예시 코드:

결과:

또한 저는 포럼을 통해 공지사항을 게시하며, 포럼 게시물에 일부 코드를 공유하고 싶습니다. 하지만 이 트릭이 Discourse에서는 작동하지 않는 것 같습니다. :(. 포럼의 코드 블록에 줄 번호를 표시할 수 있는 방법이 있을까요?

우리는 코드 블록에 highlight.js를 사용하고 있으며, 줄 번호를 지원하지 않는다고 생각합니다… 해당 문서에 관련 근거가 있습니다: Line numbers — highlight.js 11.9.0 documentation

줄 번호는 "3번 줄에서 글꼴 크기를 변경할 수 있습니다"와 같은 참조 목적으로 유용할 수 있으므로, 향후 highlight.js 사용을 중단하는 것을 고려하게 된다면 이를 한 가지 고려 사항으로 삼아야 합니다.

크리스님, 추천해 주셔서 정말 감사합니다!

테마 커스터마이징을 통해 플러그인을 적용해 보았지만 성공하지 못했습니다. 테마에서 커스텀 JS/CSS를 어떻게 사용하는지 잘 아시는 분의 도움이 필요할 것 같습니다 :slight_smile: .

네. 또한 실제로 줄이 줄바꿈되는 설치 환경(수평 스크롤이 트리거되지 않는 경우)에서는 줄 번호를 보면 긴 줄이 줄바꿈되었는지 명확히 알 수 있습니다.

Docker 포럼에서 줄 번호를 줄바꿈(wrapping)과 스크롤링에 모두 적용하는 실험을 하고 있습니다. 아직 라이브로 배포되지는 않았지만, 혹시 의견을 주실 분이 계신지 궁금합니다. 먼저 몇 가지 스크린샷을 보여드리겠습니다:

줄바꿈된 콘텐츠

위 이미지에서 줄 번호는 한 줄이 끝나고 다른 줄이 시작되는 위치를 식별하는 데 도움이 됩니다. 물론 이를 렌더링하는 다른 방법들도 있지만, 이 주제의 핵심은 줄 번호입니다. :nerd_face:

스크롤된 콘텐츠

숨겨진 콘텐츠가 있는 쪽의 패딩을 제한하여, 스크롤하면 더 많은 내용이 보임을 나타내는 방식:

또는 가로 방향으로 콘텐츠가 숨겨진 위치를 나타내기 위해 그림자를 사용하는 방식:

받은 피드백 중에는 작은 그림자가 스크롤바와 혼동될 수 있다는 의견이 있어, 다른 스타일링을 적용하는 것이 좋겠다고 하였습니다.

클라이언트 측에서 줄 번호 추가하기

가로 스크롤링을 사용할 때, 콘텐츠의 각 줄은 화면에서 한 줄을 차지합니다. 따라서 이론적으로는 JavaScript를 사용하여 줄 번호 범위를 가진 세로 <div>를 삽입하고 이를 스크롤되는 콘텐츠 옆에 배치할 수 있습니다. 하지만 줄바꿈(wrapping)을 사용할 경우, 하나의 줄 번호가 여러 줄의 콘텐츠에 적용될 수 있으며, 이 경우 CSS span::before와 content: counter(...)를 함께 사용하는 것이 더 쉬운 것 같습니다. 여기서는 이 방식을 사용하고 있습니다.

::before를 사용하려면, 사전 형식화된 코드 블록의 각 줄에 <span>으로 감싸는 등 부모 요소가 먼저 필요합니다. api.decorateCookedElement를 사용하여 이를 수행해야 합니다. 이는 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>

물론 이는 제가 보고해야 할 일시적인 버그일 수 있습니다. 하지만 어쨌든 안전을 기하는 것이 좋습니다. 그리고 좋은 소식은, 현재 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부터 시작되기 때문입니다. 인용에 번호 매김을 추가하려면, 아래 줄에서 :not(.lines-in-quote)를 제거하세요 (SCSS에서 여러 번 나타납니다):

&.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);
    }
  }
}

개발

개발을 위해 다음 코드를 사용하여, ?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 마크다운 플레버 테마 구성 요소에 대한 원래 <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 수정 사항을 반영하기 위해 게시물을 편집했습니다. 이제 그림자 덮개(shadow covers)가 올바르게 적용되며, --lines-shadow-width를 어떤 크기로 설정하더라도 올바르게 페이드 인/아웃됩니다.

또한 이제 radial-gradient 예제도 포함되어 있습니다. 하지만 이는 높은 코드 블록에 대해 크게 변하지 않을 것입니다: