mirror of
https://github.com/go-gitea/gitea.git
synced 2025-05-05 15:32:53 +00:00
Fix markdown render behaviors (#34122)
* Fix #27645 * Add config options `MATH_CODE_BLOCK_DETECTION`, problematic syntaxes are disabled by default * Fix #33639 * Add config options `RENDER_OPTIONS_*`, old behaviors are kept
This commit is contained in:
parent
ee6929d96b
commit
e1c2d05bde
@ -1413,14 +1413,14 @@ LEVEL = Info
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;; Render soft line breaks as hard line breaks, which means a single newline character between
|
;; Customize render options for different contexts. Set to "none" to disable the defaults, or use comma separated list:
|
||||||
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
|
;; * short-issue-pattern: recognized "#123" issue reference and render it as a link to the issue
|
||||||
;; necessary to force a line break.
|
;; * new-line-hard-break: render soft line breaks as hard line breaks, which means a single newline character between
|
||||||
;; Render soft line breaks as hard line breaks for comments
|
;; paragraphs will cause a line break and adding trailing whitespace to paragraphs is not
|
||||||
;ENABLE_HARD_LINE_BREAK_IN_COMMENTS = true
|
;; necessary to force a line break.
|
||||||
;;
|
;RENDER_OPTIONS_COMMENT = short-issue-pattern, new-line-hard-break
|
||||||
;; Render soft line breaks as hard line breaks for markdown documents
|
;RENDER_OPTIONS_WIKI = short-issue-pattern
|
||||||
;ENABLE_HARD_LINE_BREAK_IN_DOCUMENTS = false
|
;RENDER_OPTIONS_REPO_FILE =
|
||||||
;;
|
;;
|
||||||
;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
|
;; Comma separated list of custom URL-Schemes that are allowed as links when rendering Markdown
|
||||||
;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
|
;; for example git,magnet,ftp (more at https://en.wikipedia.org/wiki/List_of_URI_schemes)
|
||||||
@ -1434,6 +1434,12 @@ LEVEL = Info
|
|||||||
;;
|
;;
|
||||||
;; Enables math inline and block detection
|
;; Enables math inline and block detection
|
||||||
;ENABLE_MATH = true
|
;ENABLE_MATH = true
|
||||||
|
;;
|
||||||
|
;; Enable delimiters for math code block detection. Set to "none" to disable all,
|
||||||
|
;; or use comma separated list: inline-dollar, inline-parentheses, block-dollar, block-square-brackets
|
||||||
|
;; Defaults to "inline-dollar,block-dollar" to follow GitHub's behavior.
|
||||||
|
;MATH_CODE_BLOCK_DETECTION =
|
||||||
|
;;
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -56,7 +56,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
|
|||||||
if repo != nil {
|
if repo != nil {
|
||||||
helper.repoLink = repo.Link()
|
helper.repoLink = repo.Link()
|
||||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||||
rctx = rctx.WithMetas(repo.ComposeMetas(ctx))
|
rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
|
||||||
} else {
|
} else {
|
||||||
// this is almost dead code, only to pass the incorrect tests
|
// this is almost dead code, only to pass the incorrect tests
|
||||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||||
@ -64,7 +64,7 @@ func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repositor
|
|||||||
"user": helper.opts.DeprecatedOwnerName,
|
"user": helper.opts.DeprecatedOwnerName,
|
||||||
"repo": helper.opts.DeprecatedRepoName,
|
"repo": helper.opts.DeprecatedRepoName,
|
||||||
|
|
||||||
"markdownLineBreakStyle": "comment",
|
"markdownNewLineHardBreak": "true",
|
||||||
"markupAllowShortIssuePattern": "true",
|
"markupAllowShortIssuePattern": "true",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -61,15 +61,13 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository,
|
|||||||
if repo != nil {
|
if repo != nil {
|
||||||
helper.repoLink = repo.Link()
|
helper.repoLink = repo.Link()
|
||||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||||
rctx = rctx.WithMetas(repo.ComposeDocumentMetas(ctx))
|
rctx = rctx.WithMetas(repo.ComposeRepoFileMetas(ctx))
|
||||||
} else {
|
} else {
|
||||||
// this is almost dead code, only to pass the incorrect tests
|
// this is almost dead code, only to pass the incorrect tests
|
||||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||||
rctx = rctx.WithMetas(map[string]string{
|
rctx = rctx.WithMetas(map[string]string{
|
||||||
"user": helper.opts.DeprecatedOwnerName,
|
"user": helper.opts.DeprecatedOwnerName,
|
||||||
"repo": helper.opts.DeprecatedRepoName,
|
"repo": helper.opts.DeprecatedRepoName,
|
||||||
|
|
||||||
"markdownLineBreakStyle": "document",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
rctx = rctx.WithHelper(helper)
|
rctx = rctx.WithHelper(helper)
|
||||||
|
@ -68,7 +68,6 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository,
|
|||||||
"user": helper.opts.DeprecatedOwnerName,
|
"user": helper.opts.DeprecatedOwnerName,
|
||||||
"repo": helper.opts.DeprecatedRepoName,
|
"repo": helper.opts.DeprecatedRepoName,
|
||||||
|
|
||||||
"markdownLineBreakStyle": "document",
|
|
||||||
"markupAllowShortIssuePattern": "true",
|
"markupAllowShortIssuePattern": "true",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -512,15 +512,15 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
|
|||||||
"repo": repo.Name,
|
"repo": repo.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
unitExternalTracker, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
|
metas["format"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerFormat
|
||||||
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
|
switch unitExternalTracker.ExternalTrackerConfig().ExternalTrackerStyle {
|
||||||
case markup.IssueNameStyleAlphanumeric:
|
case markup.IssueNameStyleAlphanumeric:
|
||||||
metas["style"] = markup.IssueNameStyleAlphanumeric
|
metas["style"] = markup.IssueNameStyleAlphanumeric
|
||||||
case markup.IssueNameStyleRegexp:
|
case markup.IssueNameStyleRegexp:
|
||||||
metas["style"] = markup.IssueNameStyleRegexp
|
metas["style"] = markup.IssueNameStyleRegexp
|
||||||
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
|
metas["regexp"] = unitExternalTracker.ExternalTrackerConfig().ExternalTrackerRegexpPattern
|
||||||
default:
|
default:
|
||||||
metas["style"] = markup.IssueNameStyleNumeric
|
metas["style"] = markup.IssueNameStyleNumeric
|
||||||
}
|
}
|
||||||
@ -544,11 +544,11 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
|
|||||||
return repo.commonRenderingMetas
|
return repo.commonRenderingMetas
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
|
// ComposeCommentMetas composes a map of metas for properly rendering comments or comment-like contents (commit message)
|
||||||
func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
func (repo *Repository) ComposeCommentMetas(ctx context.Context) map[string]string {
|
||||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
metas["markdownLineBreakStyle"] = "comment"
|
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.NewLineHardBreak)
|
||||||
metas["markupAllowShortIssuePattern"] = "true"
|
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsComment.ShortIssuePattern)
|
||||||
return metas
|
return metas
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -556,16 +556,17 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string {
|
|||||||
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
|
func (repo *Repository) ComposeWikiMetas(ctx context.Context) map[string]string {
|
||||||
// does wiki need the "teams" and "org" from common metas?
|
// does wiki need the "teams" and "org" from common metas?
|
||||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
metas["markdownLineBreakStyle"] = "document"
|
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.NewLineHardBreak)
|
||||||
metas["markupAllowShortIssuePattern"] = "true"
|
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsWiki.ShortIssuePattern)
|
||||||
return metas
|
return metas
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComposeDocumentMetas composes a map of metas for properly rendering documents (repo files)
|
// ComposeRepoFileMetas composes a map of metas for properly rendering documents (repo files)
|
||||||
func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string {
|
func (repo *Repository) ComposeRepoFileMetas(ctx context.Context) map[string]string {
|
||||||
// does document(file) need the "teams" and "org" from common metas?
|
// does document(file) need the "teams" and "org" from common metas?
|
||||||
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
metas := maps.Clone(repo.composeCommonMetas(ctx))
|
||||||
metas["markdownLineBreakStyle"] = "document"
|
metas["markdownNewLineHardBreak"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)
|
||||||
|
metas["markupAllowShortIssuePattern"] = strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.ShortIssuePattern)
|
||||||
return metas
|
return metas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ func TestMetas(t *testing.T) {
|
|||||||
|
|
||||||
repo.Units = nil
|
repo.Units = nil
|
||||||
|
|
||||||
metas := repo.ComposeMetas(db.DefaultContext)
|
metas := repo.ComposeCommentMetas(db.DefaultContext)
|
||||||
assert.Equal(t, "testRepo", metas["repo"])
|
assert.Equal(t, "testRepo", metas["repo"])
|
||||||
assert.Equal(t, "testOwner", metas["user"])
|
assert.Equal(t, "testOwner", metas["user"])
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ func TestMetas(t *testing.T) {
|
|||||||
testSuccess := func(expectedStyle string) {
|
testSuccess := func(expectedStyle string) {
|
||||||
repo.Units = []*RepoUnit{&externalTracker}
|
repo.Units = []*RepoUnit{&externalTracker}
|
||||||
repo.commonRenderingMetas = nil
|
repo.commonRenderingMetas = nil
|
||||||
metas := repo.ComposeMetas(db.DefaultContext)
|
metas := repo.ComposeCommentMetas(db.DefaultContext)
|
||||||
assert.Equal(t, expectedStyle, metas["style"])
|
assert.Equal(t, expectedStyle, metas["style"])
|
||||||
assert.Equal(t, "testRepo", metas["repo"])
|
assert.Equal(t, "testRepo", metas["repo"])
|
||||||
assert.Equal(t, "testOwner", metas["user"])
|
assert.Equal(t, "testOwner", metas["user"])
|
||||||
@ -121,7 +121,7 @@ func TestMetas(t *testing.T) {
|
|||||||
repo, err := GetRepositoryByID(db.DefaultContext, 3)
|
repo, err := GetRepositoryByID(db.DefaultContext, 3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
metas = repo.ComposeMetas(db.DefaultContext)
|
metas = repo.ComposeCommentMetas(db.DefaultContext)
|
||||||
assert.Contains(t, metas, "org")
|
assert.Contains(t, metas, "org")
|
||||||
assert.Contains(t, metas, "teams")
|
assert.Contains(t, metas, "teams")
|
||||||
assert.Equal(t, "org3", metas["org"])
|
assert.Equal(t, "org3", metas["org"])
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/internal"
|
"code.gitea.io/gitea/modules/markup/internal"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
east "github.com/yuin/goldmark/extension/ast"
|
east "github.com/yuin/goldmark/extension/ast"
|
||||||
@ -69,16 +68,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
|||||||
g.transformList(ctx, v, rc)
|
g.transformList(ctx, v, rc)
|
||||||
case *ast.Text:
|
case *ast.Text:
|
||||||
if v.SoftLineBreak() && !v.HardLineBreak() {
|
if v.SoftLineBreak() && !v.HardLineBreak() {
|
||||||
// TODO: this was a quite unclear part, old code: `if metas["mode"] != "document" { use comment link break setting }`
|
newLineHardBreak := ctx.RenderOptions.Metas["markdownNewLineHardBreak"] == "true"
|
||||||
// many places render non-comment contents with no mode=document, then these contents also use comment's hard line break setting
|
v.SetHardLineBreak(newLineHardBreak)
|
||||||
// especially in many tests.
|
|
||||||
markdownLineBreakStyle := ctx.RenderOptions.Metas["markdownLineBreakStyle"]
|
|
||||||
switch markdownLineBreakStyle {
|
|
||||||
case "comment":
|
|
||||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
|
|
||||||
case "document":
|
|
||||||
v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case *ast.CodeSpan:
|
case *ast.CodeSpan:
|
||||||
g.transformCodeSpan(ctx, v, reader)
|
g.transformCodeSpan(ctx, v, reader)
|
||||||
|
@ -126,11 +126,11 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
|
|||||||
highlighting.WithWrapperRenderer(r.highlightingRenderer),
|
highlighting.WithWrapperRenderer(r.highlightingRenderer),
|
||||||
),
|
),
|
||||||
math.NewExtension(&ctx.RenderInternal, math.Options{
|
math.NewExtension(&ctx.RenderInternal, math.Options{
|
||||||
Enabled: setting.Markdown.EnableMath,
|
Enabled: setting.Markdown.EnableMath,
|
||||||
ParseDollarInline: true,
|
ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar,
|
||||||
ParseDollarBlock: true,
|
ParseInlineParentheses: setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping
|
||||||
ParseSquareBlock: true, // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options)
|
ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar,
|
||||||
// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future
|
ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping
|
||||||
}),
|
}),
|
||||||
meta.Meta,
|
meta.Meta,
|
||||||
),
|
),
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -15,6 +17,7 @@ import (
|
|||||||
const nl = "\n"
|
const nl = "\n"
|
||||||
|
|
||||||
func TestMathRender(t *testing.T) {
|
func TestMathRender(t *testing.T) {
|
||||||
|
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true}
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
testcase string
|
testcase string
|
||||||
expected string
|
expected string
|
||||||
@ -69,7 +72,7 @@ func TestMathRender(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$$a$$",
|
"$$a$$",
|
||||||
`<code class="language-math display">a</code>` + nl,
|
`<p><code class="language-math">a</code></p>` + nl,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$$a$$ test",
|
"$$a$$ test",
|
||||||
@ -111,6 +114,7 @@ func TestMathRender(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMathRenderBlockIndent(t *testing.T) {
|
func TestMathRenderBlockIndent(t *testing.T) {
|
||||||
|
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{ParseBlockDollar: true, ParseBlockSquareBrackets: true}
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
name string
|
name string
|
||||||
testcase string
|
testcase string
|
||||||
@ -243,3 +247,64 @@ x
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMathRenderOptions(t *testing.T) {
|
||||||
|
setting.Markdown.MathCodeBlockOptions = setting.MarkdownMathCodeBlockOptions{}
|
||||||
|
defer test.MockVariableValue(&setting.Markdown.MathCodeBlockOptions)
|
||||||
|
test := func(t *testing.T, expected, input string) {
|
||||||
|
res, err := RenderString(markup.NewTestRenderContext(), input)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(res)), "input: %s", input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default (non-conflict) inline syntax
|
||||||
|
test(t, `<p><code class="language-math">a</code></p>`, "$`a`$")
|
||||||
|
|
||||||
|
// ParseInlineDollar
|
||||||
|
test(t, `<p>$a$</p>`, `$a$`)
|
||||||
|
setting.Markdown.MathCodeBlockOptions.ParseInlineDollar = true
|
||||||
|
test(t, `<p><code class="language-math">a</code></p>`, `$a$`)
|
||||||
|
|
||||||
|
// ParseInlineParentheses
|
||||||
|
test(t, `<p>(a)</p>`, `\(a\)`)
|
||||||
|
setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
|
||||||
|
test(t, `<p><code class="language-math">a</code></p>`, `\(a\)`)
|
||||||
|
|
||||||
|
// ParseBlockDollar
|
||||||
|
test(t, `<p>$$
|
||||||
|
a
|
||||||
|
$$</p>
|
||||||
|
`, `
|
||||||
|
$$
|
||||||
|
a
|
||||||
|
$$
|
||||||
|
`)
|
||||||
|
setting.Markdown.MathCodeBlockOptions.ParseBlockDollar = true
|
||||||
|
test(t, `<pre class="code-block is-loading"><code class="language-math display">
|
||||||
|
a
|
||||||
|
</code></pre>
|
||||||
|
`, `
|
||||||
|
$$
|
||||||
|
a
|
||||||
|
$$
|
||||||
|
`)
|
||||||
|
|
||||||
|
// ParseBlockSquareBrackets
|
||||||
|
test(t, `<p>[
|
||||||
|
a
|
||||||
|
]</p>
|
||||||
|
`, `
|
||||||
|
\[
|
||||||
|
a
|
||||||
|
\]
|
||||||
|
`)
|
||||||
|
setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
|
||||||
|
test(t, `<pre class="code-block is-loading"><code class="language-math display">
|
||||||
|
a
|
||||||
|
</code></pre>
|
||||||
|
`, `
|
||||||
|
\[
|
||||||
|
a
|
||||||
|
\]
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
@ -15,26 +15,26 @@ type inlineParser struct {
|
|||||||
trigger []byte
|
trigger []byte
|
||||||
endBytesSingleDollar []byte
|
endBytesSingleDollar []byte
|
||||||
endBytesDoubleDollar []byte
|
endBytesDoubleDollar []byte
|
||||||
endBytesBracket []byte
|
endBytesParentheses []byte
|
||||||
|
enableInlineDollar bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultInlineDollarParser = &inlineParser{
|
func NewInlineDollarParser(enableInlineDollar bool) parser.InlineParser {
|
||||||
trigger: []byte{'$'},
|
return &inlineParser{
|
||||||
endBytesSingleDollar: []byte{'$'},
|
trigger: []byte{'$'},
|
||||||
endBytesDoubleDollar: []byte{'$', '$'},
|
endBytesSingleDollar: []byte{'$'},
|
||||||
|
endBytesDoubleDollar: []byte{'$', '$'},
|
||||||
|
enableInlineDollar: enableInlineDollar,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInlineDollarParser() parser.InlineParser {
|
var defaultInlineParenthesesParser = &inlineParser{
|
||||||
return defaultInlineDollarParser
|
trigger: []byte{'\\', '('},
|
||||||
|
endBytesParentheses: []byte{'\\', ')'},
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultInlineBracketParser = &inlineParser{
|
func NewInlineParenthesesParser() parser.InlineParser {
|
||||||
trigger: []byte{'\\', '('},
|
return defaultInlineParenthesesParser
|
||||||
endBytesBracket: []byte{'\\', ')'},
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInlineBracketParser() parser.InlineParser {
|
|
||||||
return defaultInlineBracketParser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger triggers this parser on $ or \
|
// Trigger triggers this parser on $ or \
|
||||||
@ -46,7 +46,7 @@ func isPunctuation(b byte) bool {
|
|||||||
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
||||||
}
|
}
|
||||||
|
|
||||||
func isBracket(b byte) bool {
|
func isParenthesesClose(b byte) bool {
|
||||||
return b == ')'
|
return b == ')'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +86,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startMarkLen = 2
|
startMarkLen = 2
|
||||||
stopMark = parser.endBytesBracket
|
stopMark = parser.endBytesParentheses
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[0] == '$' && !parser.enableInlineDollar && (len(line) == 1 || line[1] != '`') {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if checkSurrounding {
|
if checkSurrounding {
|
||||||
@ -110,7 +114,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
|||||||
succeedingCharacter = line[i+len(stopMark)]
|
succeedingCharacter = line[i+len(stopMark)]
|
||||||
}
|
}
|
||||||
// check valid ending character
|
// check valid ending character
|
||||||
isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) ||
|
isValidEndingChar := isPunctuation(succeedingCharacter) || isParenthesesClose(succeedingCharacter) ||
|
||||||
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
|
succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0
|
||||||
if checkSurrounding && !isValidEndingChar {
|
if checkSurrounding && !isValidEndingChar {
|
||||||
break
|
break
|
||||||
|
@ -14,10 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
ParseDollarInline bool
|
ParseInlineDollar bool // inline $$ xxx $$ text
|
||||||
ParseDollarBlock bool
|
ParseInlineParentheses bool // inline \( xxx \) text
|
||||||
ParseSquareBlock bool
|
ParseBlockDollar bool // block $$ multiple-line $$ text
|
||||||
|
ParseBlockSquareBrackets bool // block \[ multiple-line \] text
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension is a math extension
|
// Extension is a math extension
|
||||||
@ -42,16 +43,16 @@ func (e *Extension) Extend(m goldmark.Markdown) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)}
|
var inlines []util.PrioritizedValue
|
||||||
if e.options.ParseDollarInline {
|
if e.options.ParseInlineParentheses {
|
||||||
inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502))
|
inlines = append(inlines, util.Prioritized(NewInlineParenthesesParser(), 501))
|
||||||
}
|
}
|
||||||
|
inlines = append(inlines, util.Prioritized(NewInlineDollarParser(e.options.ParseInlineDollar), 502))
|
||||||
|
|
||||||
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
|
m.Parser().AddOptions(parser.WithInlineParsers(inlines...))
|
||||||
|
|
||||||
m.Parser().AddOptions(parser.WithBlockParsers(
|
m.Parser().AddOptions(parser.WithBlockParsers(
|
||||||
util.Prioritized(NewBlockParser(e.options.ParseDollarBlock, e.options.ParseSquareBlock), 701),
|
util.Prioritized(NewBlockParser(e.options.ParseBlockDollar, e.options.ParseBlockSquareBrackets), 701),
|
||||||
))
|
))
|
||||||
|
|
||||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
|
util.Prioritized(NewBlockRenderer(e.renderInternal), 501),
|
||||||
util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
|
util.Prioritized(NewInlineRenderer(e.renderInternal), 502),
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ type RenderOptions struct {
|
|||||||
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
|
// user&repo, format&style®exp (for external issue pattern), teams&org (for mention)
|
||||||
// RefTypeNameSubURL (for iframe&asciicast)
|
// RefTypeNameSubURL (for iframe&asciicast)
|
||||||
// markupAllowShortIssuePattern
|
// markupAllowShortIssuePattern
|
||||||
// markdownLineBreakStyle (comment, document)
|
// markdownNewLineHardBreak
|
||||||
Metas map[string]string
|
Metas map[string]string
|
||||||
|
|
||||||
// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
// used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
|
||||||
@ -247,7 +248,8 @@ func Init(renderHelpFuncs *RenderHelperFuncs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ComposeSimpleDocumentMetas() map[string]string {
|
func ComposeSimpleDocumentMetas() map[string]string {
|
||||||
return map[string]string{"markdownLineBreakStyle": "document"}
|
// TODO: there is no separate config option for "simple document" rendering, so temporarily use the same config as "repo file"
|
||||||
|
return map[string]string{"markdownNewLineHardBreak": strconv.FormatBool(setting.Markdown.RenderOptionsRepoFile.NewLineHardBreak)}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestRenderHelper struct {
|
type TestRenderHelper struct {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExternalMarkupRenderers represents the external markup renderers
|
// ExternalMarkupRenderers represents the external markup renderers
|
||||||
@ -23,18 +24,33 @@ const (
|
|||||||
RenderContentModeIframe = "iframe"
|
RenderContentModeIframe = "iframe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type MarkdownRenderOptions struct {
|
||||||
|
NewLineHardBreak bool
|
||||||
|
ShortIssuePattern bool // Actually it is a "markup" option because it is used in "post processor"
|
||||||
|
}
|
||||||
|
|
||||||
|
type MarkdownMathCodeBlockOptions struct {
|
||||||
|
ParseInlineDollar bool
|
||||||
|
ParseInlineParentheses bool
|
||||||
|
ParseBlockDollar bool
|
||||||
|
ParseBlockSquareBrackets bool
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown settings
|
// Markdown settings
|
||||||
var Markdown = struct {
|
var Markdown = struct {
|
||||||
EnableHardLineBreakInComments bool
|
RenderOptionsComment MarkdownRenderOptions `ini:"-"`
|
||||||
EnableHardLineBreakInDocuments bool
|
RenderOptionsWiki MarkdownRenderOptions `ini:"-"`
|
||||||
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"`
|
RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"`
|
||||||
FileExtensions []string
|
|
||||||
EnableMath bool
|
CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor"
|
||||||
|
FileExtensions []string
|
||||||
|
|
||||||
|
EnableMath bool
|
||||||
|
MathCodeBlockDetection []string
|
||||||
|
MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"`
|
||||||
}{
|
}{
|
||||||
EnableHardLineBreakInComments: true,
|
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
|
||||||
EnableHardLineBreakInDocuments: false,
|
EnableMath: true,
|
||||||
FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","),
|
|
||||||
EnableMath: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkupRenderer defines the external parser configured in ini
|
// MarkupRenderer defines the external parser configured in ini
|
||||||
@ -60,6 +76,56 @@ type MarkupSanitizerRule struct {
|
|||||||
|
|
||||||
func loadMarkupFrom(rootCfg ConfigProvider) {
|
func loadMarkupFrom(rootCfg ConfigProvider) {
|
||||||
mustMapSetting(rootCfg, "markdown", &Markdown)
|
mustMapSetting(rootCfg, "markdown", &Markdown)
|
||||||
|
const none = "none"
|
||||||
|
|
||||||
|
const renderOptionShortIssuePattern = "short-issue-pattern"
|
||||||
|
const renderOptionNewLineHardBreak = "new-line-hard-break"
|
||||||
|
cfgMarkdown := rootCfg.Section("markdown")
|
||||||
|
parseMarkdownRenderOptions := func(key string, defaults []string) (ret MarkdownRenderOptions) {
|
||||||
|
options := cfgMarkdown.Key(key).Strings(",")
|
||||||
|
options = util.IfEmpty(options, defaults)
|
||||||
|
for _, opt := range options {
|
||||||
|
switch opt {
|
||||||
|
case renderOptionShortIssuePattern:
|
||||||
|
ret.ShortIssuePattern = true
|
||||||
|
case renderOptionNewLineHardBreak:
|
||||||
|
ret.NewLineHardBreak = true
|
||||||
|
case none:
|
||||||
|
ret = MarkdownRenderOptions{}
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
log.Error("Unknown markdown render option in %s: %s", key, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
Markdown.RenderOptionsComment = parseMarkdownRenderOptions("RENDER_OPTIONS_COMMENT", []string{renderOptionShortIssuePattern, renderOptionNewLineHardBreak})
|
||||||
|
Markdown.RenderOptionsWiki = parseMarkdownRenderOptions("RENDER_OPTIONS_WIKI", []string{renderOptionShortIssuePattern})
|
||||||
|
Markdown.RenderOptionsRepoFile = parseMarkdownRenderOptions("RENDER_OPTIONS_REPO_FILE", nil)
|
||||||
|
|
||||||
|
const mathCodeInlineDollar = "inline-dollar"
|
||||||
|
const mathCodeInlineParentheses = "inline-parentheses"
|
||||||
|
const mathCodeBlockDollar = "block-dollar"
|
||||||
|
const mathCodeBlockSquareBrackets = "block-square-brackets"
|
||||||
|
Markdown.MathCodeBlockDetection = util.IfEmpty(Markdown.MathCodeBlockDetection, []string{mathCodeInlineDollar, mathCodeBlockDollar})
|
||||||
|
Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
|
||||||
|
for _, s := range Markdown.MathCodeBlockDetection {
|
||||||
|
switch s {
|
||||||
|
case mathCodeInlineDollar:
|
||||||
|
Markdown.MathCodeBlockOptions.ParseInlineDollar = true
|
||||||
|
case mathCodeInlineParentheses:
|
||||||
|
Markdown.MathCodeBlockOptions.ParseInlineParentheses = true
|
||||||
|
case mathCodeBlockDollar:
|
||||||
|
Markdown.MathCodeBlockOptions.ParseBlockDollar = true
|
||||||
|
case mathCodeBlockSquareBrackets:
|
||||||
|
Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets = true
|
||||||
|
case none:
|
||||||
|
Markdown.MathCodeBlockOptions = MarkdownMathCodeBlockOptions{}
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
log.Error("Unknown math code block detection option: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
|
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
|
||||||
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
|
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
|
||||||
|
51
modules/setting/markup_test.go
Normal file
51
modules/setting/markup_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadMarkup(t *testing.T) {
|
||||||
|
cfg, _ := NewConfigProviderFromData(``)
|
||||||
|
loadMarkupFrom(cfg)
|
||||||
|
assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseBlockDollar: true}, Markdown.MathCodeBlockOptions)
|
||||||
|
assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsComment)
|
||||||
|
assert.Equal(t, MarkdownRenderOptions{ShortIssuePattern: true}, Markdown.RenderOptionsWiki)
|
||||||
|
assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsRepoFile)
|
||||||
|
|
||||||
|
t.Run("Math", func(t *testing.T) {
|
||||||
|
cfg, _ = NewConfigProviderFromData(`
|
||||||
|
[markdown]
|
||||||
|
MATH_CODE_BLOCK_DETECTION = none
|
||||||
|
`)
|
||||||
|
loadMarkupFrom(cfg)
|
||||||
|
assert.Equal(t, MarkdownMathCodeBlockOptions{}, Markdown.MathCodeBlockOptions)
|
||||||
|
|
||||||
|
cfg, _ = NewConfigProviderFromData(`
|
||||||
|
[markdown]
|
||||||
|
MATH_CODE_BLOCK_DETECTION = inline-dollar, inline-parentheses, block-dollar, block-square-brackets
|
||||||
|
`)
|
||||||
|
loadMarkupFrom(cfg)
|
||||||
|
assert.Equal(t, MarkdownMathCodeBlockOptions{ParseInlineDollar: true, ParseInlineParentheses: true, ParseBlockDollar: true, ParseBlockSquareBrackets: true}, Markdown.MathCodeBlockOptions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Render", func(t *testing.T) {
|
||||||
|
cfg, _ = NewConfigProviderFromData(`
|
||||||
|
[markdown]
|
||||||
|
RENDER_OPTIONS_COMMENT = none
|
||||||
|
`)
|
||||||
|
loadMarkupFrom(cfg)
|
||||||
|
assert.Equal(t, MarkdownRenderOptions{}, Markdown.RenderOptionsComment)
|
||||||
|
|
||||||
|
cfg, _ = NewConfigProviderFromData(`
|
||||||
|
[markdown]
|
||||||
|
RENDER_OPTIONS_REPO_FILE = short-issue-pattern, new-line-hard-break
|
||||||
|
`)
|
||||||
|
loadMarkupFrom(cfg)
|
||||||
|
assert.Equal(t, MarkdownRenderOptions{NewLineHardBreak: true, ShortIssuePattern: true}, Markdown.RenderOptionsRepoFile)
|
||||||
|
})
|
||||||
|
}
|
@ -51,7 +51,7 @@ var testMetas = map[string]string{
|
|||||||
"user": "user13",
|
"user": "user13",
|
||||||
"repo": "repo11",
|
"repo": "repo11",
|
||||||
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
|
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
|
||||||
"markdownLineBreakStyle": "comment",
|
"markdownNewLineHardBreak": "true",
|
||||||
"markupAllowShortIssuePattern": "true",
|
"markupAllowShortIssuePattern": "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +219,13 @@ func IfZero[T comparable](v, def T) T {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IfEmpty[T any](v, def []T) []T {
|
||||||
|
if len(v) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// OptionalArg helps the "optional argument" in Golang:
|
// OptionalArg helps the "optional argument" in Golang:
|
||||||
//
|
//
|
||||||
// func foo(optArg ...int) { return OptionalArg(optArg) }
|
// func foo(optArg ...int) { return OptionalArg(optArg) }
|
||||||
|
@ -200,9 +200,9 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
|
// TODO: "ComposeCommentMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
|
||||||
// need to be refactored together in the future
|
// need to be refactored together in the future
|
||||||
metas := ctx.Repo.Repository.ComposeMetas(ctx)
|
metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
|
||||||
|
|
||||||
// the title for the "run" is from the commit message
|
// the title for the "run" is from the commit message
|
||||||
resp.State.Run.Title = run.Title
|
resp.State.Run.Title = run.Title
|
||||||
|
@ -278,7 +278,7 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
|
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
|
||||||
if err == nil && extIssueUnit != nil {
|
if err == nil && extIssueUnit != nil {
|
||||||
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
|
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
|
||||||
metas := ctx.Repo.Repository.ComposeMetas(ctx)
|
metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
|
||||||
metas["index"] = ctx.PathParam("index")
|
metas["index"] = ctx.PathParam("index")
|
||||||
res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
|
res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -176,7 +176,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||||||
if markupType != "" && !shouldRenderSource {
|
if markupType != "" && !shouldRenderSource {
|
||||||
ctx.Data["IsMarkup"] = true
|
ctx.Data["IsMarkup"] = true
|
||||||
ctx.Data["MarkupType"] = markupType
|
ctx.Data["MarkupType"] = markupType
|
||||||
metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
|
metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
|
||||||
metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
|
metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
|
||||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button>
|
<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button>
|
||||||
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
|
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
|
||||||
</div>
|
</div>
|
||||||
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
|
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeCommentMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
|
||||||
</td>
|
</td>
|
||||||
{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}}
|
{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}}
|
||||||
<td class="tw-text-right tw-overflow-visible">
|
<td class="tw-text-right tw-overflow-visible">
|
||||||
@ -103,7 +103,7 @@
|
|||||||
<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button>
|
<button class="btn interact-fg tw-px-1" data-clipboard-text="{{.DBBranch.Name}}" data-tooltip-content="{{ctx.Locale.Tr "copy_branch"}}">{{svg "octicon-copy" 14}}</button>
|
||||||
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
|
{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
|
||||||
</div>
|
</div>
|
||||||
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
|
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeCommentMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td class="two wide ui">
|
<td class="two wide ui">
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="ui container fluid padded">
|
<div class="ui container fluid padded">
|
||||||
<div class="ui top attached header clearing segment tw-relative commit-header">
|
<div class="ui top attached header clearing segment tw-relative commit-header">
|
||||||
<div class="tw-flex tw-mb-4 tw-gap-1">
|
<div class="tw-flex tw-mb-4 tw-gap-1">
|
||||||
<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
|
<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
|
||||||
{{if not $.PageIsWiki}}
|
{{if not $.PageIsWiki}}
|
||||||
<div class="commit-header-buttons">
|
<div class="commit-header-buttons">
|
||||||
<a class="ui primary tiny button" href="{{.SourcePath}}">
|
<a class="ui primary tiny button" href="{{.SourcePath}}">
|
||||||
@ -122,7 +122,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{if IsMultilineCommitMessage .Commit.Message}}
|
{{if IsMultilineCommitMessage .Commit.Message}}
|
||||||
<pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeMetas ctx)}}</pre>
|
<pre class="commit-body">{{ctx.RenderUtils.RenderCommitBody .Commit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/commit_load_branches_and_tags" .}}
|
{{template "repo/commit_load_branches_and_tags" .}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span>
|
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | ctx.RenderUtils.RenderEmoji}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
|
{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
|
||||||
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeMetas ctx)}}</pre>
|
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .Message ($.Repository.ComposeCommentMetas ctx)}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if $.CommitsTagsMap}}
|
{{if $.CommitsTagsMap}}
|
||||||
{{range (index $.CommitsTagsMap .ID.String)}}
|
{{range (index $.CommitsTagsMap .ID.String)}}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
||||||
|
|
||||||
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
||||||
{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{if IsMultilineCommitMessage .Message}}
|
{{if IsMultilineCommitMessage .Message}}
|
||||||
<pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}">
|
<pre class="commit-body tw-ml-[33px] tw-hidden" data-singular-commit-body-for="{{$tag}}">
|
||||||
{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
{{- ctx.RenderUtils.RenderCommitBody .Message ($.comment.Issue.PullRequest.BaseRepo.ComposeCommentMetas ctx) -}}
|
||||||
</pre>
|
</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -189,7 +189,7 @@
|
|||||||
<div class="ui segment flex-text-block tw-gap-4">
|
<div class="ui segment flex-text-block tw-gap-4">
|
||||||
{{template "shared/issueicon" .}}
|
{{template "shared/issueicon" .}}
|
||||||
<div class="issue-title tw-break-anywhere">
|
<div class="issue-title tw-break-anywhere">
|
||||||
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}
|
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeCommentMetas ctx)}}
|
||||||
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary">
|
<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary">
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}}
|
{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}}
|
||||||
|
|
||||||
<span class="message tw-inline-block gt-ellipsis">
|
<span class="message tw-inline-block gt-ellipsis">
|
||||||
<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
|
<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeCommentMetas ctx)}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="commit-refs flex-text-inline">
|
<span class="commit-refs flex-text-inline">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
|
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
|
||||||
<div class="issue-title" id="issue-title-display">
|
<div class="issue-title" id="issue-title-display">
|
||||||
<h1 class="tw-break-anywhere">
|
<h1 class="tw-break-anywhere">
|
||||||
{{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx)}}
|
{{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeCommentMetas ctx)}}
|
||||||
<span class="index">#{{.Issue.Index}}</span>
|
<span class="index">#{{.Issue.Index}}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="issue-title-buttons">
|
<div class="issue-title-buttons">
|
||||||
|
@ -21,10 +21,10 @@
|
|||||||
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
|
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
|
||||||
|
|
||||||
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
||||||
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}</span>
|
||||||
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
||||||
<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button>
|
<button class="ui button ellipsis-button" aria-expanded="false" data-global-click="onRepoEllipsisButtonClick">...</button>
|
||||||
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeMetas ctx)}}</pre>
|
<pre class="commit-body tw-hidden">{{ctx.RenderUtils.RenderCommitBody .LatestCommit.Message ($.Repository.ComposeCommentMetas ctx)}}</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
<div class="repo-file-cell message loading-icon-2px">
|
<div class="repo-file-cell message loading-icon-2px">
|
||||||
{{if $commit}}
|
{{if $commit}}
|
||||||
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
|
{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
|
||||||
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink ($.Repository.ComposeMetas ctx)}}
|
{{ctx.RenderUtils.RenderCommitMessageLinkSubject $commit.Message $commitLink ($.Repository.ComposeCommentMetas ctx)}}
|
||||||
{{else}}
|
{{else}}
|
||||||
… {{/* will be loaded again by LastCommitLoaderURL */}}
|
… {{/* will be loaded again by LastCommitLoaderURL */}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
<img class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
|
<img class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
|
||||||
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
||||||
<span class="text truncate">
|
<span class="text truncate">
|
||||||
{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeMetas ctx)}}
|
{{ctx.RenderUtils.RenderCommitMessage .Message ($repo.ComposeCommentMetas ctx)}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
|
import {queryElems} from '../utils/dom.ts';
|
||||||
|
|
||||||
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
|
export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> {
|
||||||
const el = elMarkup.querySelector('.asciinema-player-container');
|
queryElems(elMarkup, '.asciinema-player-container', async (el) => {
|
||||||
if (!el) return;
|
const [player] = await Promise.all([
|
||||||
|
// @ts-expect-error: module exports no types
|
||||||
|
import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
|
||||||
|
import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
|
||||||
|
]);
|
||||||
|
|
||||||
const [player] = await Promise.all([
|
player.create(el.getAttribute('data-asciinema-player-src'), el, {
|
||||||
// @ts-expect-error: module exports no types
|
// poster (a preview frame) to display until the playback is started.
|
||||||
import(/* webpackChunkName: "asciinema-player" */'asciinema-player'),
|
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
|
||||||
import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'),
|
poster: 'npt:1:0:0',
|
||||||
]);
|
});
|
||||||
|
|
||||||
player.create(el.getAttribute('data-asciinema-player-src'), el, {
|
|
||||||
// poster (a preview frame) to display until the playback is started.
|
|
||||||
// Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more.
|
|
||||||
poster: 'npt:1:0:0',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {svg} from '../svg.ts';
|
import {svg} from '../svg.ts';
|
||||||
|
import {queryElems} from '../utils/dom.ts';
|
||||||
|
|
||||||
export function makeCodeCopyButton(): HTMLButtonElement {
|
export function makeCodeCopyButton(): HTMLButtonElement {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
@ -8,11 +9,12 @@ export function makeCodeCopyButton(): HTMLButtonElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
|
export function initMarkupCodeCopy(elMarkup: HTMLElement): void {
|
||||||
const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code
|
// .markup .code-block code
|
||||||
if (!el || !el.textContent) return;
|
queryElems(elMarkup, '.code-block code', (el) => {
|
||||||
|
if (!el.textContent) return;
|
||||||
const btn = makeCodeCopyButton();
|
const btn = makeCodeCopyButton();
|
||||||
// remove final trailing newline introduced during HTML rendering
|
// remove final trailing newline introduced during HTML rendering
|
||||||
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
|
btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, ''));
|
||||||
el.after(btn);
|
el.after(btn);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {displayError} from './common.ts';
|
import {displayError} from './common.ts';
|
||||||
|
import {queryElems} from '../utils/dom.ts';
|
||||||
|
|
||||||
function targetElement(el: Element): {target: Element, displayAsBlock: boolean} {
|
function targetElement(el: Element): {target: Element, displayAsBlock: boolean} {
|
||||||
// The target element is either the parent "code block with loading indicator", or itself
|
// The target element is either the parent "code block with loading indicator", or itself
|
||||||
@ -12,35 +13,35 @@ function targetElement(el: Element): {target: Element, displayAsBlock: boolean}
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> {
|
export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> {
|
||||||
const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math'
|
// .markup code.language-math'
|
||||||
if (!el) return;
|
queryElems(elMarkup, 'code.language-math', async (el) => {
|
||||||
|
const [{default: katex}] = await Promise.all([
|
||||||
|
import(/* webpackChunkName: "katex" */'katex'),
|
||||||
|
import(/* webpackChunkName: "katex" */'katex/dist/katex.css'),
|
||||||
|
]);
|
||||||
|
|
||||||
const [{default: katex}] = await Promise.all([
|
const MAX_CHARS = 1000;
|
||||||
import(/* webpackChunkName: "katex" */'katex'),
|
const MAX_SIZE = 25;
|
||||||
import(/* webpackChunkName: "katex" */'katex/dist/katex.css'),
|
const MAX_EXPAND = 1000;
|
||||||
]);
|
|
||||||
|
|
||||||
const MAX_CHARS = 1000;
|
const {target, displayAsBlock} = targetElement(el);
|
||||||
const MAX_SIZE = 25;
|
if (target.hasAttribute('data-render-done')) return;
|
||||||
const MAX_EXPAND = 1000;
|
const source = el.textContent;
|
||||||
|
|
||||||
const {target, displayAsBlock} = targetElement(el);
|
if (source.length > MAX_CHARS) {
|
||||||
if (target.hasAttribute('data-render-done')) return;
|
displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
|
||||||
const source = el.textContent;
|
return;
|
||||||
|
}
|
||||||
if (source.length > MAX_CHARS) {
|
try {
|
||||||
displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`));
|
const tempEl = document.createElement(displayAsBlock ? 'p' : 'span');
|
||||||
return;
|
katex.render(source, tempEl, {
|
||||||
}
|
maxSize: MAX_SIZE,
|
||||||
try {
|
maxExpand: MAX_EXPAND,
|
||||||
const tempEl = document.createElement(displayAsBlock ? 'p' : 'span');
|
displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode
|
||||||
katex.render(source, tempEl, {
|
});
|
||||||
maxSize: MAX_SIZE,
|
target.replaceWith(tempEl);
|
||||||
maxExpand: MAX_EXPAND,
|
} catch (error) {
|
||||||
displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode
|
displayError(target, error);
|
||||||
});
|
}
|
||||||
target.replaceWith(tempEl);
|
});
|
||||||
} catch (error) {
|
|
||||||
displayError(target, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {isDarkTheme} from '../utils.ts';
|
import {isDarkTheme} from '../utils.ts';
|
||||||
import {makeCodeCopyButton} from './codecopy.ts';
|
import {makeCodeCopyButton} from './codecopy.ts';
|
||||||
import {displayError} from './common.ts';
|
import {displayError} from './common.ts';
|
||||||
|
import {queryElems} from '../utils/dom.ts';
|
||||||
|
|
||||||
const {mermaidMaxSourceCharacters} = window.config;
|
const {mermaidMaxSourceCharacters} = window.config;
|
||||||
|
|
||||||
@ -11,77 +12,77 @@ body {margin: 0; padding: 0; overflow: hidden}
|
|||||||
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`;
|
||||||
|
|
||||||
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
|
export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> {
|
||||||
const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid
|
// .markup code.language-mermaid
|
||||||
if (!el) return;
|
queryElems(elMarkup, 'code.language-mermaid', async (el) => {
|
||||||
|
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
||||||
|
|
||||||
const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
mermaid.initialize({
|
theme: isDarkTheme() ? 'dark' : 'neutral',
|
||||||
startOnLoad: false,
|
securityLevel: 'strict',
|
||||||
theme: isDarkTheme() ? 'dark' : 'neutral',
|
suppressErrorRendering: true,
|
||||||
securityLevel: 'strict',
|
|
||||||
suppressErrorRendering: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pre = el.closest('pre');
|
|
||||||
if (pre.hasAttribute('data-render-done')) return;
|
|
||||||
|
|
||||||
const source = el.textContent;
|
|
||||||
if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
|
|
||||||
displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mermaid.parse(source);
|
|
||||||
} catch (err) {
|
|
||||||
displayError(pre, err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// can't use bindFunctions here because we can't cross the iframe boundary. This
|
|
||||||
// means js-based interactions won't work but they aren't intended to work either
|
|
||||||
const {svg} = await mermaid.render('mermaid', source);
|
|
||||||
|
|
||||||
const iframe = document.createElement('iframe');
|
|
||||||
iframe.classList.add('markup-content-iframe', 'tw-invisible');
|
|
||||||
iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
|
|
||||||
|
|
||||||
const mermaidBlock = document.createElement('div');
|
|
||||||
mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
|
|
||||||
mermaidBlock.append(iframe);
|
|
||||||
|
|
||||||
const btn = makeCodeCopyButton();
|
|
||||||
btn.setAttribute('data-clipboard-text', source);
|
|
||||||
mermaidBlock.append(btn);
|
|
||||||
|
|
||||||
const updateIframeHeight = () => {
|
|
||||||
const body = iframe.contentWindow?.document?.body;
|
|
||||||
if (body) {
|
|
||||||
iframe.style.height = `${body.clientHeight}px`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
|
||||||
pre.replaceWith(mermaidBlock);
|
|
||||||
mermaidBlock.classList.remove('tw-hidden');
|
|
||||||
updateIframeHeight();
|
|
||||||
setTimeout(() => { // avoid flash of iframe background
|
|
||||||
mermaidBlock.classList.remove('is-loading');
|
|
||||||
iframe.classList.remove('tw-invisible');
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// update height when element's visibility state changes, for example when the diagram is inside
|
|
||||||
// a <details> + <summary> block and the <details> block becomes visible upon user interaction, it
|
|
||||||
// would initially set a incorrect height and the correct height is set during this callback.
|
|
||||||
(new IntersectionObserver(() => {
|
|
||||||
updateIframeHeight();
|
|
||||||
}, {root: document.documentElement})).observe(iframe);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.append(mermaidBlock);
|
const pre = el.closest('pre');
|
||||||
} catch (err) {
|
if (pre.hasAttribute('data-render-done')) return;
|
||||||
displayError(pre, err);
|
|
||||||
}
|
const source = el.textContent;
|
||||||
|
if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
|
||||||
|
displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mermaid.parse(source);
|
||||||
|
} catch (err) {
|
||||||
|
displayError(pre, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// can't use bindFunctions here because we can't cross the iframe boundary. This
|
||||||
|
// means js-based interactions won't work but they aren't intended to work either
|
||||||
|
const {svg} = await mermaid.render('mermaid', source);
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.classList.add('markup-content-iframe', 'tw-invisible');
|
||||||
|
iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
|
||||||
|
|
||||||
|
const mermaidBlock = document.createElement('div');
|
||||||
|
mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
|
||||||
|
mermaidBlock.append(iframe);
|
||||||
|
|
||||||
|
const btn = makeCodeCopyButton();
|
||||||
|
btn.setAttribute('data-clipboard-text', source);
|
||||||
|
mermaidBlock.append(btn);
|
||||||
|
|
||||||
|
const updateIframeHeight = () => {
|
||||||
|
const body = iframe.contentWindow?.document?.body;
|
||||||
|
if (body) {
|
||||||
|
iframe.style.height = `${body.clientHeight}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
iframe.addEventListener('load', () => {
|
||||||
|
pre.replaceWith(mermaidBlock);
|
||||||
|
mermaidBlock.classList.remove('tw-hidden');
|
||||||
|
updateIframeHeight();
|
||||||
|
setTimeout(() => { // avoid flash of iframe background
|
||||||
|
mermaidBlock.classList.remove('is-loading');
|
||||||
|
iframe.classList.remove('tw-invisible');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
// update height when element's visibility state changes, for example when the diagram is inside
|
||||||
|
// a <details> + <summary> block and the <details> block becomes visible upon user interaction, it
|
||||||
|
// would initially set a incorrect height and the correct height is set during this callback.
|
||||||
|
(new IntersectionObserver(() => {
|
||||||
|
updateIframeHeight();
|
||||||
|
}, {root: document.documentElement})).observe(iframe);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.append(mermaidBlock);
|
||||||
|
} catch (err) {
|
||||||
|
displayError(pre, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user