From 410d556431ed1d3f5ff4f24429d0cd19238c0816 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Sun, 12 Jul 2020 12:31:24 -0700 Subject: [PATCH] bug(renderer): source highlighter should get unadulterated string Ths fix for this involved some refactoring of the highlighting work to separate out the calloouts (we embed them using reserved Unicode so that we can find them again and replace them inline). Chroma automatically escapes the code for HTML safetly already, so we defer to it's rules, when we use it. We still have to do this ourselves when not using Chroma. Fixes #721 --- LIMITATIONS.adoc | 4 + pkg/renderer/sgml/delimited_block.go | 98 +++++++++++++++++-- pkg/renderer/sgml/html5/callout_list.go | 3 + .../sgml/html5/delimited_block_test.go | 30 ++++++ pkg/renderer/sgml/html5/templates.go | 1 + pkg/renderer/sgml/sgml_renderer.go | 2 + pkg/renderer/sgml/templates.go | 1 + .../sgml/xhtml5/delimited_block_test.go | 30 ++++++ 8 files changed, 162 insertions(+), 7 deletions(-) diff --git a/LIMITATIONS.adoc b/LIMITATIONS.adoc index 207a4b1d..eb734bf5 100644 --- a/LIMITATIONS.adoc +++ b/LIMITATIONS.adoc @@ -136,6 +136,10 @@ is treated as an alias. Chroma supports all the standard pygments styles, as we majority of the same source code languages. However some more esoteric languages might not be supported. See https://github.com/alecthomas/chroma#supported-languages[Chroma's documentation] for details. +== Callouts + +Libasciidoc does not support font or image based callouts yet. + == Math MathML and equations (`[stem]` blocks) are not supported yet. diff --git a/pkg/renderer/sgml/delimited_block.go b/pkg/renderer/sgml/delimited_block.go index 24fbac15..82d3a127 100644 --- a/pkg/renderer/sgml/delimited_block.go +++ b/pkg/renderer/sgml/delimited_block.go @@ -2,6 +2,7 @@ package sgml import ( "bytes" + "strconv" "strings" "github.com/alecthomas/chroma" @@ -107,6 +108,71 @@ func (r *sgmlRenderer) renderListingBlock(ctx *renderer.Context, b types.Delimit return result.String(), err } +func (r *sgmlRenderer) renderSourceLine(w *strings.Builder, item interface{}) { + switch item := item.(type) { + case types.VerbatimLine: + w.WriteString(item.Content) + for _, co := range item.Callouts { + // We inject an escaped sequence for now, which we can replace later with + // a fully rendered version of the callout. We use two non-characters + // (reserved for this kind of use by Unicode) to bracket callouts, allowing + // us to find them again in post-processing.. + w.WriteString("\ufdd0") + w.WriteString(strconv.Itoa(co.Ref)) + w.WriteString("\ufdd1") + } + case types.StringElement: + w.WriteString(item.Content) + case []interface{}: + for _, sub := range item { + r.renderSourceLine(w, sub) + } + } +} + +func (r *sgmlRenderer) renderCalloutRef(co types.Callout) (string, error) { + result := &strings.Builder{} + err := r.calloutRef.Execute(result, co) + if err != nil { + return "", errors.Wrap(err, "unable to render callout number") + } + return result.String(), nil +} + +func (r *sgmlRenderer) renderSourceCallouts(source string) (string, error) { + result := &strings.Builder{} + num := 0 + co := false + for _, ch := range source { + if co { + if ch >= '0' && ch <= '9' { + num *= 10 + num += int(ch - '0') + continue + } + if ch == '\ufdd1' { + s, err := r.renderCalloutRef(types.Callout{Ref: num}) + if err != nil { + return "", errors.Wrap(err, "unable to render source block") + } + result.WriteString(s) + co = false + continue + } + // unexpected character - just copy it to output. + result.WriteRune(ch) + } + if ch == '\ufdd0' { + // start of integer + num = 0 + co = true + continue + } + result.WriteRune(ch) + } + return result.String(), nil +} + func (r *sgmlRenderer) renderSourceBlock(ctx *renderer.Context, b types.DelimitedBlock) (string, error) { previousWithinDelimitedBlock := ctx.WithinDelimitedBlock previousIncludeBlankLine := ctx.IncludeBlankLine @@ -119,15 +185,20 @@ func (r *sgmlRenderer) renderSourceBlock(ctx *renderer.Context, b types.Delimite // first, render the content elements := discardTrailingBlankLines(b.Elements) - content, err := r.renderElements(ctx, elements) - - if err != nil { - return "", err - } highlighter, _ := ctx.Attributes.GetAsString(types.AttrSyntaxHighlighter) language, found := b.Attributes.GetAsString(types.AttrLanguage) + content := "" if found && (highlighter == "chroma" || highlighter == "pygments") { + + source := &strings.Builder{} + for i, line := range elements { + if i > 0 { + source.WriteRune('\n') + } + r.renderSourceLine(source, line) + } + // using github.com/alecthomas/chroma to highlight the content contentBuf := &strings.Builder{} lexer := lexers.Get(language) @@ -140,7 +211,8 @@ func (r *sgmlRenderer) renderSourceBlock(ctx *renderer.Context, b types.Delimite if s, found := ctx.Attributes.GetAsString(highlighter + "-style"); found { style = styles.Get(s) } - iterator, err := lexer.Tokenise(nil, content) + // iterator, err := lexer.Tokenise(nil, content) + iterator, err := lexer.Tokenise(nil, source.String()) if err != nil { return "", err } @@ -163,10 +235,21 @@ func (r *sgmlRenderer) renderSourceBlock(ctx *renderer.Context, b types.Delimite return "", err } content = contentBuf.String() + content, err = r.renderSourceCallouts(content) + if err != nil { + return "", err + } + } else { + elements := discardTrailingBlankLines(b.Elements) + if s, err := r.renderElements(ctx, elements); err != nil { + return "", err + } else { + content = s + } } result := &bytes.Buffer{} - err = r.sourceBlock.Execute(result, struct { + err := r.sourceBlock.Execute(result, struct { ID sanitized Title sanitized Roles sanitized @@ -181,6 +264,7 @@ func (r *sgmlRenderer) renderSourceBlock(ctx *renderer.Context, b types.Delimite Language: language, Content: content, }) + return result.String(), err } diff --git a/pkg/renderer/sgml/html5/callout_list.go b/pkg/renderer/sgml/html5/callout_list.go index d62fbfd0..16db2d40 100644 --- a/pkg/renderer/sgml/html5/callout_list.go +++ b/pkg/renderer/sgml/html5/callout_list.go @@ -12,4 +12,7 @@ const ( // NB: The items are numbered sequentially. calloutListItemTmpl = "
  • \n{{ .Content }}
  • \n" + + // This should probably have been a , but for compatibility we use + calloutRefTmpl = "({{ .Ref }})" ) diff --git a/pkg/renderer/sgml/html5/delimited_block_test.go b/pkg/renderer/sgml/html5/delimited_block_test.go index 03b104be..9cda4da1 100644 --- a/pkg/renderer/sgml/html5/delimited_block_test.go +++ b/pkg/renderer/sgml/html5/delimited_block_test.go @@ -357,6 +357,36 @@ end Expect(RenderHTML(source)).To(MatchHTML(expected)) }) + It("with C content, and callout", func() { + source := `:source-highlighter: chroma +[source, c] +---- +#include + +printf("Hello world!\n"); // <1> +link +---- +<1> A greeting +` + expected := `
    +
    +
    #include <stdio.h>
    +
    +printf("Hello world!\n"); // (1)
    +<a>link</a>
    +
    +
    +
    +
      +
    1. +

      A greeting

      +
    2. +
    +
    +` + Expect(RenderHTML(source)).To(MatchHTML(expected)) + }) + It("with other content", func() { source := `---- a< + +printf("Hello world!\n"); // <1> +link +---- +<1> A greeting +` + expected := `
    +
    +
    #include <stdio.h>
    +
    +printf("Hello world!\n"); // (1)
    +<a>link</a>
    +
    +
    +
    +
      +
    1. +

      A greeting

      +
    2. +
    +
    +` + Expect(RenderXHTML(source)).To(MatchHTML(expected)) + }) + It("with other content", func() { source := `---- a<