Add support for rendering terminal output with colors (#19497)

This commit is contained in:
Lauris BH 2022-06-09 00:46:39 +03:00 committed by GitHub
parent 8fee7c46c1
commit f92b7a6331
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 466 additions and 0 deletions

View file

@ -0,0 +1,95 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package console
import (
"bytes"
"io"
"path/filepath"
"regexp"
"strings"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
trend "github.com/buildkite/terminal-to-html/v3"
"github.com/go-enry/go-enry/v2"
)
// MarkupName describes markup's name
var MarkupName = "console"
func init() {
markup.RegisterRenderer(Renderer{})
}
// Renderer implements markup.Renderer
type Renderer struct{}
// Name implements markup.Renderer
func (Renderer) Name() string {
return MarkupName
}
// NeedPostProcess implements markup.Renderer
func (Renderer) NeedPostProcess() bool { return false }
// Extensions implements markup.Renderer
func (Renderer) Extensions() []string {
return []string{".sh-session"}
}
// SanitizerRules implements markup.Renderer
func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
return []setting.MarkupSanitizerRule{
{Element: "span", AllowAttr: "class", Regexp: regexp.MustCompile(`^term-((fg[ix]?|bg)\d+|container)$`)},
}
}
// SanitizerDisabled disabled sanitize if return true
func (Renderer) SanitizerDisabled() bool {
return false
}
// CanRender implements markup.RendererContentDetector
func (Renderer) CanRender(filename string, input io.Reader) bool {
buf, err := io.ReadAll(input)
if err != nil {
return false
}
if enry.GetLanguage(filepath.Base(filename), buf) != enry.OtherLanguage {
return false
}
return bytes.ContainsRune(buf, '\x1b')
}
// Render renders terminal colors to HTML with all specific handling stuff.
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
buf, err := io.ReadAll(input)
if err != nil {
return err
}
buf = trend.Render(buf)
buf = bytes.ReplaceAll(buf, []byte("\n"), []byte(`<br>`))
_, err = output.Write(buf)
return err
}
// Render renders terminal colors to HTML with all specific handling stuff.
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
if ctx.Type == "" {
ctx.Type = MarkupName
}
return markup.Render(ctx, input, output)
}
// RenderString renders terminal colors in string to HTML with all specific handling stuff and return string
func RenderString(ctx *markup.RenderContext, content string) (string, error) {
var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
return "", err
}
return buf.String(), nil
}

View file

@ -0,0 +1,31 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package console
import (
"strings"
"testing"
"code.gitea.io/gitea/modules/markup"
"github.com/stretchr/testify/assert"
)
func TestRenderConsole(t *testing.T) {
var render Renderer
kases := map[string]string{
"\x1b[37m\x1b[40mnpm\x1b[0m \x1b[0m\x1b[32minfo\x1b[0m \x1b[0m\x1b[35mit worked if it ends with\x1b[0m ok": "<span class=\"term-fg37 term-bg40\">npm</span> <span class=\"term-fg32\">info</span> <span class=\"term-fg35\">it worked if it ends with</span> ok",
}
for k, v := range kases {
var buf strings.Builder
canRender := render.CanRender("test", strings.NewReader(k))
assert.True(t, canRender)
err := render.Render(&markup.RenderContext{}, strings.NewReader(k), &buf)
assert.NoError(t, err)
assert.EqualValues(t, v, buf.String())
}
}

View file

@ -5,6 +5,7 @@
package markup
import (
"bytes"
"context"
"errors"
"fmt"
@ -93,6 +94,12 @@ type Renderer interface {
Render(ctx *RenderContext, input io.Reader, output io.Writer) error
}
// RendererContentDetector detects if the content can be rendered
// by specified renderer
type RendererContentDetector interface {
CanRender(filename string, input io.Reader) bool
}
var (
extRenderers = make(map[string]Renderer)
renderers = make(map[string]Renderer)
@ -117,6 +124,20 @@ func GetRendererByType(tp string) Renderer {
return renderers[tp]
}
// DetectRendererType detects the markup type of the content
func DetectRendererType(filename string, input io.Reader) string {
buf, err := io.ReadAll(input)
if err != nil {
return ""
}
for _, renderer := range renderers {
if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) {
return renderer.Name()
}
}
return ""
}
// Render renders markup file to HTML with all specific handling stuff.
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
if ctx.Type != "" {