mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-25 14:35:40 +00:00
Render inline file permalinks
This commit is contained in:
parent
d1e808f803
commit
1d3240887c
|
@ -10,10 +10,12 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -61,6 +63,9 @@ var (
|
|||
|
||||
validLinksPattern = regexp.MustCompile(`^[a-z][\w-]+://`)
|
||||
|
||||
// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
|
||||
filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{7,64})/(\S+)#(L\d+(?:-L\d+)?)`)
|
||||
|
||||
// While this email regex is definitely not perfect and I'm sure you can come up
|
||||
// with edge cases, it is still accepted by the CommonMark specification, as
|
||||
// well as the HTML5 spec:
|
||||
|
@ -171,6 +176,7 @@ type processor func(ctx *RenderContext, node *html.Node)
|
|||
var defaultProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
filePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
|
@ -1054,6 +1060,267 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
return
|
||||
}
|
||||
if DefaultProcessorHelper.GetRepoFileContent == nil || DefaultProcessorHelper.GetLocale == nil {
|
||||
return
|
||||
}
|
||||
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
m := filePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that every group (m[0]...m[9]) has a match
|
||||
for i := 0; i < 10; i++ {
|
||||
if m[i] == -1 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
urlFull := node.Data[m[0]:m[1]]
|
||||
|
||||
// Ensure that we only use links to local repositories
|
||||
if !strings.HasPrefix(urlFull, setting.AppURL+setting.AppSubURL) {
|
||||
return
|
||||
}
|
||||
|
||||
projPath := node.Data[m[2]:m[3]]
|
||||
projPath = strings.TrimSuffix(projPath, "/")
|
||||
|
||||
commitSha := node.Data[m[4]:m[5]]
|
||||
filePath := node.Data[m[6]:m[7]]
|
||||
hash := node.Data[m[8]:m[9]]
|
||||
|
||||
start := m[0]
|
||||
end := m[1]
|
||||
|
||||
// If url ends in '.', it's very likely that it is not part of the
|
||||
// actual url but used to finish a sentence.
|
||||
if strings.HasSuffix(urlFull, ".") {
|
||||
end--
|
||||
urlFull = urlFull[:len(urlFull)-1]
|
||||
hash = hash[:len(hash)-1]
|
||||
}
|
||||
|
||||
projPathSegments := strings.Split(projPath, "/")
|
||||
fileContent, err := DefaultProcessorHelper.GetRepoFileContent(
|
||||
ctx.Ctx,
|
||||
projPathSegments[len(projPathSegments)-2],
|
||||
projPathSegments[len(projPathSegments)-1],
|
||||
commitSha, filePath,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lineSpecs := strings.Split(hash, "-")
|
||||
lineCount := len(fileContent)
|
||||
|
||||
var subTitle string
|
||||
var lineOffset int
|
||||
|
||||
if len(lineSpecs) == 1 {
|
||||
line, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
|
||||
if line < 1 || line > lineCount {
|
||||
return
|
||||
}
|
||||
|
||||
fileContent = fileContent[line-1 : line]
|
||||
subTitle = "Line " + strconv.Itoa(line)
|
||||
|
||||
lineOffset = line - 1
|
||||
} else {
|
||||
startLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
|
||||
endLine, _ := strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L"))
|
||||
|
||||
if startLine < 1 || endLine < 1 || startLine > lineCount || endLine > lineCount || endLine < startLine {
|
||||
return
|
||||
}
|
||||
|
||||
fileContent = fileContent[startLine-1 : endLine]
|
||||
subTitle = "Lines " + strconv.Itoa(startLine) + " to " + strconv.Itoa(endLine)
|
||||
|
||||
lineOffset = startLine - 1
|
||||
}
|
||||
|
||||
table := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Table.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "file-preview"}},
|
||||
}
|
||||
tbody := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Tbody.String(),
|
||||
}
|
||||
|
||||
locale, err := DefaultProcessorHelper.GetLocale(ctx.Ctx)
|
||||
if err != nil {
|
||||
log.Error("Unable to get locale. Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
status := &charset.EscapeStatus{}
|
||||
statuses := make([]*charset.EscapeStatus, len(fileContent))
|
||||
for i, line := range fileContent {
|
||||
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext)
|
||||
status = status.Or(statuses[i])
|
||||
}
|
||||
|
||||
for idx, code := range fileContent {
|
||||
tr := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Tr.String(),
|
||||
}
|
||||
|
||||
lineNum := strconv.Itoa(lineOffset + idx + 1)
|
||||
|
||||
tdLinesnum := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "id", Val: "L" + lineNum},
|
||||
{Key: "class", Val: "lines-num"},
|
||||
},
|
||||
}
|
||||
spanLinesNum := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "id", Val: "L" + lineNum},
|
||||
{Key: "data-line-number", Val: lineNum},
|
||||
},
|
||||
}
|
||||
tdLinesnum.AppendChild(spanLinesNum)
|
||||
tr.AppendChild(tdLinesnum)
|
||||
|
||||
if status.Escaped {
|
||||
tdLinesEscape := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "lines-escape"},
|
||||
},
|
||||
}
|
||||
|
||||
if statuses[idx].Escaped {
|
||||
btnTitle := ""
|
||||
if statuses[idx].HasInvisible {
|
||||
btnTitle += locale.TrString("repo.invisible_runes_line") + " "
|
||||
}
|
||||
if statuses[idx].HasAmbiguous {
|
||||
btnTitle += locale.TrString("repo.ambiguous_runes_line")
|
||||
}
|
||||
|
||||
escapeBtn := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Button.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "toggle-escape-button btn interact-bg"},
|
||||
{Key: "title", Val: btnTitle},
|
||||
},
|
||||
}
|
||||
tdLinesEscape.AppendChild(escapeBtn)
|
||||
}
|
||||
|
||||
tr.AppendChild(tdLinesEscape)
|
||||
}
|
||||
|
||||
tdCode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "rel", Val: "L" + lineNum},
|
||||
{Key: "class", Val: "lines-code chroma"},
|
||||
},
|
||||
}
|
||||
codeInner := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Code.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "code-inner"}},
|
||||
}
|
||||
codeText := &html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: string(code),
|
||||
}
|
||||
codeInner.AppendChild(codeText)
|
||||
tdCode.AppendChild(codeInner)
|
||||
tr.AppendChild(tdCode)
|
||||
|
||||
tbody.AppendChild(tr)
|
||||
}
|
||||
|
||||
table.AppendChild(tbody)
|
||||
|
||||
twrapper := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "ui table"}},
|
||||
}
|
||||
twrapper.AppendChild(table)
|
||||
|
||||
header := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "header"}},
|
||||
}
|
||||
afilepath := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.A.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "href", Val: urlFull},
|
||||
{Key: "class", Val: "muted"},
|
||||
},
|
||||
}
|
||||
afilepath.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: filePath,
|
||||
})
|
||||
header.AppendChild(afilepath)
|
||||
|
||||
psubtitle := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "text small grey"}},
|
||||
}
|
||||
psubtitle.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: subTitle + " in ",
|
||||
})
|
||||
psubtitle.AppendChild(createLink(urlFull[m[0]:m[5]], commitSha[0:7], "text black"))
|
||||
header.AppendChild(psubtitle)
|
||||
|
||||
preview := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}},
|
||||
}
|
||||
preview.AppendChild(header)
|
||||
preview.AppendChild(twrapper)
|
||||
|
||||
// Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
|
||||
before := node.Data[:start]
|
||||
after := node.Data[end:]
|
||||
node.Data = before
|
||||
nextSibling := node.NextSibling
|
||||
node.Parent.InsertBefore(&html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: "</p>",
|
||||
}, nextSibling)
|
||||
node.Parent.InsertBefore(preview, nextSibling)
|
||||
node.Parent.InsertBefore(&html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: "<p>" + after,
|
||||
}, nextSibling)
|
||||
|
||||
node = node.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
// emojiShortCodeProcessor for rendering text like :smile: into emoji
|
||||
func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
||||
start := 0
|
||||
|
|
|
@ -5,6 +5,7 @@ package markup_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
@ -13,10 +14,12 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -673,3 +676,57 @@ func TestIssue18471(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
|
||||
}
|
||||
|
||||
func TestRender_FilePreview(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) {
|
||||
buf := []byte("A\nB\nC\nD\n")
|
||||
return highlight.PlainText(buf), nil
|
||||
},
|
||||
GetLocale: func(ctx context.Context) (translation.Locale, error) {
|
||||
return translation.NewLocale("en-US"), nil
|
||||
},
|
||||
})
|
||||
|
||||
sha := "b6dd6210eaebc915fd5be5579c58cce4da2e2579"
|
||||
commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L1-L2"
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
test(
|
||||
commitFilePreview,
|
||||
`<p></p>`+
|
||||
`<div class="file-preview-box">`+
|
||||
`<div class="header">`+
|
||||
`<a href="http://localhost:3000/gogits/gogs/src/commit/b6dd6210eaebc915fd5be5579c58cce4da2e2579/path/to/file.go#L1-L2" class="muted" rel="nofollow">path/to/file.go</a>`+
|
||||
`<span class="text small grey">`+
|
||||
`Lines 1 to 2 in <a href="http://localhost:3000/gogits/gogs/src/commit/b6dd6210eaebc915fd5be5579c58cce4da2e2579" class="text black" rel="nofollow">b6dd621</a>`+
|
||||
`</span>`+
|
||||
`</div>`+
|
||||
`<div class="ui table">`+
|
||||
`<table class="file-preview">`+
|
||||
`<tbody>`+
|
||||
`<tr>`+
|
||||
`<td id="user-content-L1" class="lines-num"><span id="user-content-L1" data-line-number="1"></span></td>`+
|
||||
`<td rel="L1" class="lines-code chroma"><code class="code-inner">A`+"\n"+`</code></td>`+
|
||||
`</tr>`+
|
||||
`<tr>`+
|
||||
`<td id="user-content-L2" class="lines-num"><span id="user-content-L2" data-line-number="2"></span></td>`+
|
||||
`<td rel="L2" class="lines-code chroma"><code class="code-inner">B`+"\n"+`</code></td>`+
|
||||
`</tr>`+
|
||||
`</tbody>`+
|
||||
`</table>`+
|
||||
`</div>`+
|
||||
`</div>`+
|
||||
`<p></p>`,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
|
@ -31,6 +33,8 @@ const (
|
|||
|
||||
type ProcessorHelper struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
GetRepoFileContent func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error)
|
||||
GetLocale func(ctx context.Context) (translation.Locale, error)
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
}
|
||||
|
|
|
@ -120,6 +120,23 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow classes for file preview links...
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div")
|
||||
policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span")
|
||||
policy.AllowAttrs("rel").Matching(regexp.MustCompile("^L[0-9]+$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button")
|
||||
policy.AllowAttrs("title").OnElements("button")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
|
|
|
@ -5,10 +5,21 @@ package markup
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
file_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func ProcessorHelper() *markup.ProcessorHelper {
|
||||
|
@ -29,5 +40,75 @@ func ProcessorHelper() *markup.ProcessorHelper {
|
|||
// when using gitea context (web context), use user's visibility and user's permission to check
|
||||
return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer)
|
||||
},
|
||||
GetRepoFileContent: func(ctx context.Context, ownerName, repoName, commitSha, filePath string) ([]template.HTML, error) {
|
||||
repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *user.User
|
||||
|
||||
giteaCtx, ok := ctx.(*gitea_context.Context)
|
||||
if ok {
|
||||
user = giteaCtx.Doer
|
||||
}
|
||||
|
||||
perms, err := access.GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !perms.CanRead(unit.TypeCode) {
|
||||
return nil, fmt.Errorf("cannot access repository code")
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commit, err := gitRepo.GetCommit(commitSha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
language, err := file_service.TryGetContentLanguage(gitRepo, commitSha, filePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err)
|
||||
}
|
||||
|
||||
blob, err := commit.GetBlobByPath(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataRc, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
buf, _ := io.ReadAll(dataRc)
|
||||
|
||||
fileContent, _, err := highlight.File(blob.Name(), language, buf)
|
||||
if err != nil {
|
||||
log.Error("highlight.File failed, fallback to plain text: %v", err)
|
||||
fileContent = highlight.PlainText(buf)
|
||||
}
|
||||
|
||||
return fileContent, nil
|
||||
},
|
||||
GetLocale: func(ctx context.Context) (translation.Locale, error) {
|
||||
giteaCtx, ok := ctx.(*gitea_context.Context)
|
||||
if ok {
|
||||
return giteaCtx.Locale, nil
|
||||
}
|
||||
|
||||
giteaBaseCtx, ok := ctx.(*gitea_context.Base)
|
||||
if ok {
|
||||
return giteaBaseCtx.Locale, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not retrieve locale from context")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
@import "./markup/content.css";
|
||||
@import "./markup/codecopy.css";
|
||||
@import "./markup/asciicast.css";
|
||||
@import "./markup/filepreview.css";
|
||||
|
||||
@import "./chroma/base.css";
|
||||
@import "./codemirror/base.css";
|
||||
|
|
|
@ -451,7 +451,8 @@
|
|||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.markup pre > code {
|
||||
.markup pre > code,
|
||||
.markup .file-preview code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
|
|
35
web_src/css/markup/filepreview.css
Normal file
35
web_src/css/markup/filepreview.css
Normal file
|
@ -0,0 +1,35 @@
|
|||
.markup table.file-preview {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markup table.file-preview td {
|
||||
padding: 0 10px !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.markup table.file-preview tr {
|
||||
border-top: none;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.markup .file-preview-box {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markup .file-preview-box .header {
|
||||
padding: .5rem;
|
||||
padding-left: 1rem;
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-bottom: none;
|
||||
border-radius: 0.28571429rem 0.28571429rem 0 0;
|
||||
background: var(--color-box-header);
|
||||
}
|
||||
|
||||
.markup .file-preview-box .header > a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markup .file-preview-box .table {
|
||||
margin-top: 0;
|
||||
border-radius: 0 0 0.28571429rem 0.28571429rem;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
.code-view .lines-num:hover {
|
||||
.code-view .lines-num:hover,
|
||||
.file-preview .lines-num:hover {
|
||||
color: var(--color-text-dark) !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ export function initUnicodeEscapeButton() {
|
|||
|
||||
e.preventDefault();
|
||||
|
||||
const fileContent = btn.closest('.file-content, .non-diff-file-content');
|
||||
const fileView = fileContent?.querySelectorAll('.file-code, .file-view');
|
||||
const fileContent = btn.closest('.file-content, .non-diff-file-content, .file-preview-box');
|
||||
const fileView = fileContent?.querySelectorAll('.file-code, .file-view, .file-preview');
|
||||
if (btn.matches('.escape-button')) {
|
||||
for (const el of fileView) el.classList.add('unicode-escaped');
|
||||
hideElem(btn);
|
||||
|
|
Loading…
Reference in a new issue