Ist es möglich, Zeilennummern im Codeblock anzuzeigen?

Hallo, ich verwende Markdown (meistens mit der Software Typora zum Erstellen und Exportieren nach PDF), um Dokumentation zu erstellen. Ich verwende den folgenden Code, um in Code-Blöcken Zeilennummern anzuzeigen:

{.language .numberLines}

Beispielcode:

Ergebnis:

Außerdem verwende ich ein Forum, um Ankündigungen zu veröffentlichen, und ich möchte dort auch Code in Forumsposts teilen. Aber dieser Trick funktioniert anscheinend nicht in Discourse :(. Gibt es eine Möglichkeit, Zeilennummern in Code-Blöcken des Forums anzuzeigen?

Wir verwenden highlight.js für unsere Codeblöcke, und ich bin mir nicht sicher, ob sie Zeilennummern unterstützen. In ihrer Dokumentation gibt es dazu einige Erläuterungen: Line numbers — highlight.js 11.9.0 documentation

Zeilennummern können definitiv nützlich sein, zum Beispiel für Hinweise wie „Sie können die Schriftgröße in Zeile 3 ändern". Falls wir also jemals planen, von highlight.js wegzuziehen, wäre dies ein Punkt, den wir berücksichtigen sollten.

Hallo Kris, vielen Dank für den Hinweis!

Ich habe versucht, ein Plugin über die Theme-Anpassung zu installieren, aber leider ohne Erfolg. Vielleicht kann jemand helfen, der sich gut mit benutzerdefiniertem JS/CSS in Themes auskennt :slight_smile: .

Ja. Auch bei Installationen, bei denen Zeilen tatsächlich umbrechen (anstatt einen horizontalen Bildlauf auszulösen), machen Zeilennummern deutlich, dass lange Zeilen umgebrochen wurden.

Ich habe auf dem Docker-Forum mit Zeilennummern sowohl für den Umbruch als auch für das Scrollen experimentiert. Dies ist noch nicht live geschaltet, aber ich würde gerne wissen, ob jemand Kommentare hat. Zuerst also einige Screenshots:

Umgebrochener Inhalt

Oben helfen Zeilennummern dabei, zu erkennen, wann eine Zeile aufhört und eine andere beginnt. Natürlich gibt es andere Möglichkeiten, dies darzustellen, aber dieses Thema dreht sich um Zeilennummern. :nerd_face:

Gescrollter Inhalt

Mit begrenztem Padding an der (den) Seite(n), wo Inhalt verborgen ist, um anzudeuten, dass durch Scrollen mehr angezeigt wird:

Oder mit Schatten, um anzuzeigen, wo der Inhalt horizontal verborgen ist:

Einige Rückmeldungen zeigten, dass die kleinen Schatten mit Scrollbalken verwechselt werden könnten, daher wäre ein anderes Styling vielleicht besser.

Hinzufügen der Zeilennummern auf Client-Seite

Bei der Verwendung von horizontalem Scrollen verwendet jede Zeile des Inhalts eine Zeile auf dem Bildschirm. Theoretisch könnte man also JavaScript verwenden, um ein vertikales <div> mit einer Reihe von Zeilennummern einzufügen und dieses neben den gescrollten Inhalt zu positionieren. Beim Umbruch kann sich jedoch eine einzelne Zeilennummer auf mehrere Zeilen des Inhalts beziehen, wofür die Verwendung von CSS span::before zusammen mit content: counter(...) einfacher erscheint. Das wird hier verwendet.

Um ::before zu verwenden, benötigt jede Zeile im vorformatierten Code-Block zunächst ein übergeordnetes Element, wie z. B. das Umhüllen in einem <span> mit api.decorateCookedElement. Dies erfordert, dass Letzteres vor dem Ausführen von highlight.js läuft, da highlight.js manchmal Tags nicht in derselben Zeile schließt, in der sie geöffnet wurden. Zum Beispiel, wenn es annimmt, dass etwas Mustache/Handlebars ist:

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

Das oben genannte Ergebnis ist das schließende </span> in der nächsten Zeile:

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

Natürlich könnte dies ein vorübergehender Fehler sein, den ich wahrscheinlich melden sollte. Aber egal, besser auf Nummer sicher gehen. Und die gute Nachricht: Es scheint, dass api.decorateCookedElement(...) heute tatsächlich vor highlight.js ausgeführt wird.

Das nächste JavaScript umhüllt jede Zeile in einem <span> und fügt die CSS-Klasse lines-scroll oder lines-wrap hinzu, um anzugeben, welcher Stil gewünscht ist (man könnte dem Benutzer sogar erlauben, dies umzuschalten). Es fügt auch lines-shadow hinzu, um die Scroll-Schatten zu aktivieren. Außerdem werden einige CSS-Klassen an das übergeordnete <pre> und <code> hinzugefügt, um das Styling zu unterstützen und sicherzustellen, dass Dinge nur einmal dekoriert werden.

Theme JavaScript Decorator

Das Folgende aktiviert auch Zeilennummern und das andere Styling in der Editor-Vorschau. Ändern Sie die letzte Zeile, um onlyStream: true einzuschließen, um dies zu deaktivieren:

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

Fügen Sie alles Folgende zum „Common, Head“ Ihrer Komponente in /admin/customize/themes/ hinzu:

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // Kombinationen aus 'number', 'wrap', 'scroll' und/oder '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 verwendet `<aside>` für Zitate anderer Forumsposts
      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(' ');

      // Nehmen wir an, es gibt keine Event-Listener auf der Zeile,
      // und trimmen, um abschließende leere Zeilen zu verstecken
      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

Das Folgende unterstützt Umbruch mit und ohne Zeilennummerierung sowie Scrollen mit oder ohne Zeilennummerierung und/oder Schatten. Es unterdrückt auch die Zeilennummerierung, wenn es nur eine einzelne Zeile gibt.

Dies fügt Zitate anderer Beiträge keine Zeilennummern hinzu, da ein solches Zitat, wenn es nur ein Teilzitat ist, immer noch mit 1 beginnt. Um die Nummerierung für Zitate hinzuzufügen, entfernen Sie :not(.lines-in-quote) von der Zeile unten (die mehrfach im SCSS vorkommt):

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

Das oben Genannte ist auch der Ort, an dem die Nummerierung für einzeilige Code-Blöcke unterdrückt wird.

Theme Sass Styling

Fügen Sie dies zum „Common, CSS“ Ihrer Komponente in /admin/customize/themes/ hinzu:

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);
  // Kein Padding für das äußere PRE:
  // - Scrollbalken bedecken CODE nicht, sondern liegen nahe an den äußersten Rändern
  // - Text nahe an den Rändern, wenn verborgener Inhalt vorhanden ist
  // - ebenso für die Schatten
  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 {
    // Mindesthöhe, um Rahmen und Hintergrund für leere Zeilen zu erhalten (Firefox
    // benötigt dies nur für das ::before, aber Chrome auch für das span)
    min-height: 1.5em;

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

  &.lines-shadow {
    background-image:
      // Die ersten beiden Überdeckungen, die mit dem Inhalt scrollen und daher
      // vor den Schatten erscheinen, wenn kein Scrollen möglich ist. Diese
      // entsprechen der Hintergrundfarbe für die ersten 25 %, gefolgt von einem Verlauf
      // bis zur vollen Transparenz (unter der Annahme, dass die Überdeckungen viermal so breit
      // sind wie die Schatten).
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // Und dahinter zwei Schatten an festen Positionen, um scrollbaren,
      // verborgenen Inhalt anzuzeigen, falls nicht überdeckt.
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // Oder:
      // 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:
      // Setzen Sie die Überdeckung auf das Vierfache der Breite des tatsächlichen Schattens, um mit
      // dem 25 %-Stop im Verlauf der Überdeckung übereinzustimmen
      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) {
      // Wenn Zeilennummern sichtbar sind, müssen wir den Schatten verschieben
      --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);
    }
  }
}

Entwicklung

Für die Entwicklung habe ich den folgenden Code verwendet, der es ermöglicht, URL-Abfrageparameter wie ?avbPreview festzulegen, um den JavaScript-Decorator-Hook zu aktivieren, und Dinge wie ?avbPreview&avbClasses=number,scroll, um Zeilennummern mit Scrollen aber ohne Schatten zu testen. Da alle CSS-Bereiche abgegrenzt sind, sollte dies andere Benutzer während des Tests nicht beeinflussen.

Entwicklungs-/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 verwendet `<aside>` für Zitate anderer Forumsposts
      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('===== Umschreiben; Quelle\n' + elem.innerHTML.trim());
        console.log('===== Umschreiben; Ergebnis\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // Nehmen wir an, es gibt keine Event-Listener auf der Zeile,
      // und trimmen, um abschließende leere Zeilen zu verstecken
      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>

Fantastische Arbeit wurde geleistet! :clap: :clap: :clap:

Vielen Dank, Arjan! Alles funktioniert sowohl im hellen als auch im dunklen Standarddesign.


изображение

Teile meine Konfiguration:
css.txt (5.7 KB)
head.txt (1.2 KB)

CSS-Anpassungen:

// Inline-Codefarben
// Grüner Daumen hoch
// Grünes Zuweisungssymbol
// Graue oder blaue Header
// Bilder zentrieren
// Zeilenumbruch im Codeblock + vollständiger Inhalt
// Riesige Themenränder beheben
// Gitlab-Theme-Komponenten-Schriftfarbe Korrektur (dunkler)
// Original <code> über Gitlab Markdown Flavors Theme-Komponente
// Zeilennummerierung in Codeblöcken von @Arjan

Installierte Komponenten:

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

Schön, das ging schnell! Ich schätze, Sie möchten --lines-bgcolor: white; verwenden, um einige Grautöne zu entfernen. :sunglasses:

Ich habe meinen Beitrag für eine Korrektur im SCSS bearbeitet, um die Schattenabdeckungen richtig anzuwenden. Dies ermöglicht nun das Einstellen von --lines-shadow-width auf jede beliebige Größe, während das Ein- und Ausblenden weiterhin korrekt funktioniert.

Es enthält jetzt auch ein Beispiel für radial-gradient. Aber das wird für hohe Codeblöcke nicht viel ändern: