all repos

mugit @ 9718c0f

馃惍 git server that your cow will love
8 files changed, 206 insertions(+), 173 deletions(-)
refactor: markdown renderer 
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-03-01 23:47:04 +0200
Authored at: 2026-02-28 19:53:55 +0200
Change ID: xnmzwkwturptswnkzyztruzsmutoxyxr
Parent: 365b06c
M flake.nix
路路路
        15
        15
                     pname = "mugit";

      
        16
        16
                     version = version;

      
        17
        17
                     src = ./.;

      
        18
        
        -            vendorHash = "sha256-xF8IRS0Ne1zp4u6uolKFpKEZObSM6VhV95JUj2krXPY=";

      
        
        18
        +            vendorHash = "sha256-TmCwI6axUTGZEVTXyBxQbO4mbK8Dn9vaTU+/Y+K1oxM=";

      
        19
        19
                     ldflags = [ "-s" "-w" "-X main.version=${version}" ];

      
        20
        20
                     meta = with pkgs.lib; {

      
        21
        21
                       homepage = "https://git.olexsmir.xyz/mugit";

      
M go.mod
路路路
        11
        11
         	github.com/urfave/cli/v3 v3.6.2

      
        12
        12
         	github.com/yuin/goldmark v1.7.16

      
        13
        13
         	github.com/yuin/goldmark-emoji v1.0.6

      
        
        14
        +	gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab

      
        14
        15
         	golang.org/x/crypto v0.47.0

      
        15
        16
         	golang.org/x/sync v0.19.0

      
        16
        17
         	gopkg.in/yaml.v2 v2.4.0

      
M go.sum
路路路
        77
        77
         github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=

      
        78
        78
         github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=

      
        79
        79
         github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=

      
        
        80
        +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=

      
        
        81
        +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c=

      
        80
        82
         golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=

      
        81
        83
         golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=

      
        82
        84
         golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=

      
M internal/handlers/repo.go
路路路
        15
        15
         	"strings"

      
        16
        16
         	"time"

      
        17
        17
         

      
        18
        
        -	"github.com/yuin/goldmark"

      
        19
        
        -	emoji "github.com/yuin/goldmark-emoji"

      
        20
        
        -	"github.com/yuin/goldmark/extension"

      
        21
        
        -	"github.com/yuin/goldmark/renderer/html"

      
        22
        18
         	"olexsmir.xyz/mugit/internal/git"

      
        23
        
        -	"olexsmir.xyz/mugit/internal/mdx"

      
        
        19
        +	"olexsmir.xyz/mugit/internal/markdown"

      
        24
        20
         )

      
        25
        21
         

      
        26
        22
         type Meta struct {

      路路路
        459
        455
         	return diff, nil

      
        460
        456
         }

      
        461
        457
         

      
        462
        
        -var markdown = goldmark.New(

      
        463
        
        -	goldmark.WithRendererOptions(html.WithUnsafe()),

      
        464
        
        -	goldmark.WithExtensions(

      
        465
        
        -		extension.GFM,

      
        466
        
        -		extension.Linkify,

      
        467
        
        -		emoji.Emoji,

      
        468
        
        -		mdx.RelativeLink,

      
        469
        
        -	))

      
        470
        
        -

      
        471
        458
         func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) {

      
        472
        459
         	name := r.Name()

      
        473
        460
         	cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)

      路路路
        492
        479
         		if len(content) > 0 {

      
        493
        480
         			switch ext {

      
        494
        481
         			case ".md", ".markdown", ".mkd":

      
        495
        
        -				var buf bytes.Buffer

      
        496
        
        -				if cerr := markdown.Convert([]byte(content), &buf,

      
        497
        
        -					mdx.NewRelativeLinkCtx(name, fullPath)); cerr != nil {

      
        498
        
        -					return "", cerr

      
        
        482
        +				readme, err := markdown.Render(name, ref, fullPath, content)

      
        
        483
        +				if err != nil {

      
        
        484
        +					return "", err

      
        499
        485
         				}

      
        500
        
        -				readmeContents = template.HTML(buf.String())

      
        
        486
        +				return template.HTML(readme), nil

      
        
        487
        +

      
        501
        488
         			default:

      
        502
        489
         				readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content))

      
        503
        490
         			}

      
A internal/markdown/markdown.go
路路路
        
        1
        +package markdown

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"bytes"

      
        
        5
        +	"path/filepath"

      
        
        6
        +

      
        
        7
        +	"github.com/yuin/goldmark"

      
        
        8
        +	emoji "github.com/yuin/goldmark-emoji"

      
        
        9
        +	"github.com/yuin/goldmark/extension"

      
        
        10
        +	"github.com/yuin/goldmark/parser"

      
        
        11
        +	"github.com/yuin/goldmark/renderer/html"

      
        
        12
        +	callout "gitlab.com/staticnoise/goldmark-callout"

      
        
        13
        +)

      
        
        14
        +

      
        
        15
        +var (

      
        
        16
        +	repoNameKey = parser.NewContextKey()

      
        
        17
        +	repoRefKey  = parser.NewContextKey()

      
        
        18
        +	baseDirKey  = parser.NewContextKey()

      
        
        19
        +

      
        
        20
        +	markdown = goldmark.New(

      
        
        21
        +		goldmark.WithRendererOptions(html.WithUnsafe()),

      
        
        22
        +		goldmark.WithParserOptions(parser.WithAutoHeadingID()),

      
        
        23
        +		goldmark.WithExtensions(

      
        
        24
        +			extension.GFM,

      
        
        25
        +			emoji.Emoji,

      
        
        26
        +			callout.CalloutExtention,

      
        
        27
        +			&relativeLink{},

      
        
        28
        +		))

      
        
        29
        +)

      
        
        30
        +

      
        
        31
        +func Render(repoName, repoRef, readmePath, readmeSource string) (string, error) {

      
        
        32
        +	ctx := parser.NewContext()

      
        
        33
        +	ctx.Set(repoNameKey, repoName)

      
        
        34
        +	ctx.Set(repoRefKey, repoRef)

      
        
        35
        +	ctx.Set(baseDirKey, filepath.Dir(readmePath))

      
        
        36
        +	parserOpts := parser.WithContext(ctx)

      
        
        37
        +

      
        
        38
        +	var buf bytes.Buffer

      
        
        39
        +	if err := markdown.Convert([]byte(readmeSource), &buf, parserOpts); err != nil {

      
        
        40
        +		return "", err

      
        
        41
        +	}

      
        
        42
        +	return buf.String(), nil

      
        
        43
        +}

      
A internal/markdown/relink.go
路路路
        
        1
        +package markdown

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"bytes"

      
        
        5
        +	"net/url"

      
        
        6
        +	"path"

      
        
        7
        +	"regexp"

      
        
        8
        +	"strings"

      
        
        9
        +

      
        
        10
        +	"github.com/yuin/goldmark"

      
        
        11
        +	"github.com/yuin/goldmark/ast"

      
        
        12
        +	"github.com/yuin/goldmark/parser"

      
        
        13
        +	"github.com/yuin/goldmark/renderer"

      
        
        14
        +	"github.com/yuin/goldmark/text"

      
        
        15
        +	"github.com/yuin/goldmark/util"

      
        
        16
        +)

      
        
        17
        +

      
        
        18
        +type relativeLink struct{}

      
        
        19
        +

      
        
        20
        +func (e *relativeLink) Extend(m goldmark.Markdown) {

      
        
        21
        +	m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&relLinkTransformer{}, 99)))

      
        
        22
        +	m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)))

      
        
        23
        +}

      
        
        24
        +

      
        
        25
        +type relLinkTransformer struct {

      
        
        26
        +	repoName string

      
        
        27
        +	repoRef  string

      
        
        28
        +	baseDir  string

      
        
        29
        +}

      
        
        30
        +

      
        
        31
        +func (m *relLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {

      
        
        32
        +	m.repoName, _ = pc.Get(repoNameKey).(string)

      
        
        33
        +	m.repoRef, _ = pc.Get(repoRefKey).(string)

      
        
        34
        +	m.baseDir, _ = pc.Get(baseDirKey).(string)

      
        
        35
        +

      
        
        36
        +	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {

      
        
        37
        +		if !entering {

      
        
        38
        +			return ast.WalkContinue, nil

      
        
        39
        +		}

      
        
        40
        +

      
        
        41
        +		switch n := n.(type) {

      
        
        42
        +		case *ast.Link:

      
        
        43
        +			m.relativeLinkTransformer(n)

      
        
        44
        +

      
        
        45
        +		case *ast.Image:

      
        
        46
        +			m.imageFromRepoTransformer(n)

      
        
        47
        +

      
        
        48
        +		case *ast.RawHTML:

      
        
        49
        +			buf := m.segmentsToBytes(n.Segments, reader)

      
        
        50
        +			updated := m.rewriteSrc(buf)

      
        
        51
        +			if !bytes.Equal(updated, buf) {

      
        
        52
        +				s := ast.NewString(updated)

      
        
        53
        +				s.SetRaw(true)

      
        
        54
        +				n.Parent().ReplaceChild(n.Parent(), n, s)

      
        
        55
        +			}

      
        
        56
        +

      
        
        57
        +		case *ast.HTMLBlock:

      
        
        58
        +			buf := m.segmentsToBytes(n.Lines(), reader)

      
        
        59
        +			updated := m.rewriteSrc(buf)

      
        
        60
        +			if !bytes.Equal(updated, buf) {

      
        
        61
        +				n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated})

      
        
        62
        +			}

      
        
        63
        +		}

      
        
        64
        +

      
        
        65
        +		return ast.WalkContinue, nil

      
        
        66
        +	})

      
        
        67
        +}

      
        
        68
        +

      
        
        69
        +func (m *relLinkTransformer) relativeLinkTransformer(link *ast.Link) {

      
        
        70
        +	dst := string(link.Destination)

      
        
        71
        +	if isAbsoluteURL(dst) {

      
        
        72
        +		return

      
        
        73
        +	}

      
        
        74
        +

      
        
        75
        +	act := m.path(dst)

      
        
        76
        +	link.Destination = []byte(path.Join("/", m.repoName, "tree", m.repoRef, act))

      
        
        77
        +}

      
        
        78
        +

      
        
        79
        +func (m *relLinkTransformer) imageFromRepoTransformer(img *ast.Image) {

      
        
        80
        +	img.Destination = []byte(m.imageFromRepo(

      
        
        81
        +		string(img.Destination)))

      
        
        82
        +}

      
        
        83
        +

      
        
        84
        +func (m *relLinkTransformer) imageFromRepo(dst string) string {

      
        
        85
        +	if isAbsoluteURL(dst) {

      
        
        86
        +		return dst

      
        
        87
        +	}

      
        
        88
        +

      
        
        89
        +	absPath := m.path(dst)

      
        
        90
        +	return path.Join("/", url.PathEscape(m.repoName), "blob", m.repoRef, absPath) +

      
        
        91
        +		"?raw=true"

      
        
        92
        +}

      
        
        93
        +

      
        
        94
        +func (m *relLinkTransformer) path(dst string) string {

      
        
        95
        +	if path.IsAbs(dst) {

      
        
        96
        +		return dst

      
        
        97
        +	}

      
        
        98
        +	return path.Join(m.baseDir, dst)

      
        
        99
        +}

      
        
        100
        +

      
        
        101
        +var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`)

      
        
        102
        +

      
        
        103
        +func (m *relLinkTransformer) rewriteSrc(buf []byte) []byte {

      
        
        104
        +	return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte {

      
        
        105
        +		start := bytes.IndexByte(match, '"') + 1

      
        
        106
        +		end := bytes.LastIndexByte(match, '"')

      
        
        107
        +		src := string(match[start:end])

      
        
        108
        +		return []byte(`src="` + m.imageFromRepo(src) + `"`)

      
        
        109
        +	})

      
        
        110
        +}

      
        
        111
        +

      
        
        112
        +func (m *relLinkTransformer) segmentsToBytes(segs *text.Segments, reader text.Reader) []byte {

      
        
        113
        +	var buf []byte

      
        
        114
        +	for i := 0; i < segs.Len(); i++ {

      
        
        115
        +		buf = append(buf, reader.Value(segs.At(i))...)

      
        
        116
        +	}

      
        
        117
        +	return buf

      
        
        118
        +}

      
        
        119
        +

      
        
        120
        +func isAbsoluteURL(link string) bool {

      
        
        121
        +	if strings.HasPrefix(link, "#") {

      
        
        122
        +		return true

      
        
        123
        +	}

      
        
        124
        +	u, err := url.Parse(link)

      
        
        125
        +	return err == nil && (u.Scheme != "" || strings.HasPrefix(link, "//"))

      
        
        126
        +}

      
        
        127
        +

      
        
        128
        +// row block

      
        
        129
        +

      
        
        130
        +type rawBlock struct {

      
        
        131
        +	ast.BaseBlock

      
        
        132
        +	data []byte

      
        
        133
        +}

      
        
        134
        +

      
        
        135
        +var rawBlockKind = ast.NewNodeKind("RawBlock")

      
        
        136
        +

      
        
        137
        +func (r *rawBlock) Kind() ast.NodeKind   { return rawBlockKind }

      
        
        138
        +func (r *rawBlock) Dump(_ []byte, _ int) {}

      
        
        139
        +

      
        
        140
        +type rawBlockRenderer struct{}

      
        
        141
        +

      
        
        142
        +func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {

      
        
        143
        +	reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {

      
        
        144
        +		if entering {

      
        
        145
        +			w.Write(node.(*rawBlock).data)

      
        
        146
        +		}

      
        
        147
        +		return ast.WalkContinue, nil

      
        
        148
        +	})

      
        
        149
        +}

      
D internal/mdx/relative_link.go
路路路
        1
        
        -package mdx

      
        2
        
        -

      
        3
        
        -import (

      
        4
        
        -	"bytes"

      
        5
        
        -	"fmt"

      
        6
        
        -	"net/url"

      
        7
        
        -	"path"

      
        8
        
        -	"path/filepath"

      
        9
        
        -	"regexp"

      
        10
        
        -	"strings"

      
        11
        
        -

      
        12
        
        -	"github.com/yuin/goldmark"

      
        13
        
        -	"github.com/yuin/goldmark/ast"

      
        14
        
        -	"github.com/yuin/goldmark/parser"

      
        15
        
        -	"github.com/yuin/goldmark/renderer"

      
        16
        
        -	"github.com/yuin/goldmark/text"

      
        17
        
        -	"github.com/yuin/goldmark/util"

      
        18
        
        -)

      
        19
        
        -

      
        20
        
        -var (

      
        21
        
        -	repoNameKey = parser.NewContextKey()

      
        22
        
        -	baseDirKey  = parser.NewContextKey()

      
        23
        
        -)

      
        24
        
        -

      
        25
        
        -func NewRelativeLinkCtx(repoName, readmePath string) parser.ParseOption {

      
        26
        
        -	ctx := parser.NewContext()

      
        27
        
        -	ctx.Set(repoNameKey, repoName)

      
        28
        
        -	ctx.Set(baseDirKey, filepath.Dir(readmePath))

      
        29
        
        -	return parser.WithContext(ctx)

      
        30
        
        -}

      
        31
        
        -

      
        32
        
        -var RelativeLink = &relativeLink{}

      
        33
        
        -

      
        34
        
        -type relativeLink struct{}

      
        35
        
        -

      
        36
        
        -func (e *relativeLink) Extend(m goldmark.Markdown) {

      
        37
        
        -	m.Parser().AddOptions(

      
        38
        
        -		parser.WithASTTransformers(util.Prioritized(&relinkTransformer{}, 100)),

      
        39
        
        -	)

      
        40
        
        -	m.Renderer().AddOptions(

      
        41
        
        -		renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)),

      
        42
        
        -	)

      
        43
        
        -}

      
        44
        
        -

      
        45
        
        -type rawBlock struct {

      
        46
        
        -	ast.BaseBlock

      
        47
        
        -	data []byte

      
        48
        
        -}

      
        49
        
        -

      
        50
        
        -var rawBlockKind = ast.NewNodeKind("RawBlock")

      
        51
        
        -

      
        52
        
        -func (r *rawBlock) Kind() ast.NodeKind   { return rawBlockKind }

      
        53
        
        -func (r *rawBlock) Dump(_ []byte, _ int) {}

      
        54
        
        -

      
        55
        
        -type rawBlockRenderer struct{}

      
        56
        
        -

      
        57
        
        -func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {

      
        58
        
        -	reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {

      
        59
        
        -		if entering {

      
        60
        
        -			w.Write(node.(*rawBlock).data)

      
        61
        
        -		}

      
        62
        
        -		return ast.WalkContinue, nil

      
        63
        
        -	})

      
        64
        
        -}

      
        65
        
        -

      
        66
        
        -type relinkTransformer struct{}

      
        67
        
        -

      
        68
        
        -func (t *relinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {

      
        69
        
        -	repoName, _ := pc.Get(repoNameKey).(string)

      
        70
        
        -	baseDir, _ := pc.Get(baseDirKey).(string)

      
        71
        
        -

      
        72
        
        -	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {

      
        73
        
        -		if !entering {

      
        74
        
        -			return ast.WalkContinue, nil

      
        75
        
        -		}

      
        76
        
        -

      
        77
        
        -		switch v := n.(type) {

      
        78
        
        -		case *ast.Image:

      
        79
        
        -			if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil {

      
        80
        
        -				v.Destination = rewritten

      
        81
        
        -			}

      
        82
        
        -		case *ast.Link:

      
        83
        
        -			if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil {

      
        84
        
        -				v.Destination = rewritten

      
        85
        
        -			}

      
        86
        
        -		case *ast.RawHTML:

      
        87
        
        -			var buf []byte

      
        88
        
        -			for i := 0; i < v.Segments.Len(); i++ {

      
        89
        
        -				buf = append(buf, reader.Value(v.Segments.At(i))...)

      
        90
        
        -			}

      
        91
        
        -			updated := t.rewriteSrc(buf, repoName, baseDir)

      
        92
        
        -			if !bytes.Equal(updated, buf) {

      
        93
        
        -				str := ast.NewString(updated)

      
        94
        
        -				str.SetRaw(true)

      
        95
        
        -				n.Parent().ReplaceChild(n.Parent(), n, str)

      
        96
        
        -			}

      
        97
        
        -		case *ast.HTMLBlock:

      
        98
        
        -			var buf []byte

      
        99
        
        -			for i := 0; i < v.Lines().Len(); i++ {

      
        100
        
        -				buf = append(buf, reader.Value(v.Lines().At(i))...)

      
        101
        
        -			}

      
        102
        
        -			updated := t.rewriteSrc(buf, repoName, baseDir)

      
        103
        
        -			if !bytes.Equal(updated, buf) {

      
        104
        
        -				n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated})

      
        105
        
        -			}

      
        106
        
        -		}

      
        107
        
        -

      
        108
        
        -		return ast.WalkContinue, nil

      
        109
        
        -	})

      
        110
        
        -}

      
        111
        
        -

      
        112
        
        -func (t *relinkTransformer) rewriteDest(dest []byte, repoName, baseDir string) []byte {

      
        113
        
        -	urlStr := string(dest)

      
        114
        
        -	if strings.HasPrefix(urlStr, "http://") ||

      
        115
        
        -		strings.HasPrefix(urlStr, "https://") ||

      
        116
        
        -		strings.HasPrefix(urlStr, "//") ||

      
        117
        
        -		strings.HasPrefix(urlStr, "#") ||

      
        118
        
        -		strings.HasPrefix(urlStr, "mailto:") ||

      
        119
        
        -		strings.HasPrefix(urlStr, "data:") {

      
        120
        
        -		return nil

      
        121
        
        -	}

      
        122
        
        -	urlStr = strings.TrimPrefix(urlStr, "./")

      
        123
        
        -

      
        124
        
        -	var absPath string

      
        125
        
        -	if after, ok := strings.CutPrefix(urlStr, "/"); ok {

      
        126
        
        -		absPath = after

      
        127
        
        -	} else {

      
        128
        
        -		if baseDir == "" || baseDir == "." {

      
        129
        
        -			absPath = urlStr

      
        130
        
        -		} else {

      
        131
        
        -			absPath = path.Join(baseDir, urlStr)

      
        132
        
        -		}

      
        133
        
        -	}

      
        134
        
        -

      
        135
        
        -	absPath = strings.TrimPrefix(path.Clean(absPath), "/")

      
        136
        
        -

      
        137
        
        -	// FIXME: hardcoded ref and link

      
        138
        
        -	return fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", url.PathEscape(repoName), absPath)

      
        139
        
        -}

      
        140
        
        -

      
        141
        
        -var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`)

      
        142
        
        -

      
        143
        
        -func (t *relinkTransformer) rewriteSrc(buf []byte, repoName, baseDir string) []byte {

      
        144
        
        -	return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte {

      
        145
        
        -		src := imgSrcRe.FindSubmatch(match)[1]

      
        146
        
        -		rewritten := t.rewriteDest(src, repoName, baseDir)

      
        147
        
        -		if rewritten == nil {

      
        148
        
        -			return match

      
        149
        
        -		}

      
        150
        
        -		return append([]byte(`src="`), append(rewritten, '"')...)

      
        151
        
        -	})

      
        152
        
        -}

      
M web/static/style.css
路路路
        370
        370
         

      
        371
        371
         /* readme */

      
        372
        372
         .readme { padding: 0.6rem 0; }

      
        373
        
        -.readme pre { white-space: pre-wrap; }

      
        374
        373
         .readme ul  { padding: revert; }

      
        375
        374
         .readme img { max-width: 100%; }

      
        
        375
        +.readme pre {

      
        
        376
        +  background: var(--light-gray);

      
        
        377
        +  padding: 0.6rem;

      
        
        378
        +}

      
        376
        379
         

      
        377
        380
         @media (max-width: 600px) {

      
        378
        381
           .repo-index {