mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Fix slight bug in katex (#21171)
There is a small bug in #20571 whereby `$a a$b b$` will not be correctly detected as a math inline block of `a a$b b`. This PR fixes this. Also reenable test cases as per #21340 Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -7,6 +7,7 @@ package markup_test | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @@ -32,6 +33,7 @@ func TestMain(m *testing.M) { | |||||||
| 	if err := git.InitSimple(context.Background()); err != nil { | 	if err := git.InitSimple(context.Background()); err != nil { | ||||||
| 		log.Fatal("git init failed, err: %v", err) | 		log.Fatal("git init failed, err: %v", err) | ||||||
| 	} | 	} | ||||||
|  | 	os.Exit(m.Run()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRender_Commits(t *testing.T) { | func TestRender_Commits(t *testing.T) { | ||||||
| @@ -336,7 +338,7 @@ func TestRender_emoji(t *testing.T) { | |||||||
| 		`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span><span class="emoji" aria-label="grinning face with smiling eyes">😄</span> 2 emoji next to each other</p>`) | 		`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span><span class="emoji" aria-label="grinning face with smiling eyes">😄</span> 2 emoji next to each other</p>`) | ||||||
| 	test( | 	test( | ||||||
| 		"😎🤪🔐🤑❓", | 		"😎🤪🔐🤑❓", | ||||||
| 		`<p><span class="emoji" aria-label="smiling face with sunglasses">😎</span><span class="emoji" aria-label="zany face">🤪</span><span class="emoji" aria-label="locked with key">🔐</span><span class="emoji" aria-label="money-mouth face">🤑</span><span class="emoji" aria-label="question mark">❓</span></p>`) | 		`<p><span class="emoji" aria-label="smiling face with sunglasses">😎</span><span class="emoji" aria-label="zany face">🤪</span><span class="emoji" aria-label="locked with key">🔐</span><span class="emoji" aria-label="money-mouth face">🤑</span><span class="emoji" aria-label="red question mark">❓</span></p>`) | ||||||
|  |  | ||||||
| 	// should match nothing | 	// should match nothing | ||||||
| 	test( | 	test( | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ package markdown_test | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @@ -37,6 +38,7 @@ func TestMain(m *testing.M) { | |||||||
| 	if err := git.InitSimple(context.Background()); err != nil { | 	if err := git.InitSimple(context.Background()); err != nil { | ||||||
| 		log.Fatal("git init failed, err: %v", err) | 		log.Fatal("git init failed, err: %v", err) | ||||||
| 	} | 	} | ||||||
|  | 	os.Exit(m.Run()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestRender_StandardLinks(t *testing.T) { | func TestRender_StandardLinks(t *testing.T) { | ||||||
| @@ -426,3 +428,51 @@ func TestRenderEmojiInLinks_Issue12331(t *testing.T) { | |||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, expected, res) | 	assert.Equal(t, expected, res) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestMathBlock(t *testing.T) { | ||||||
|  | 	const nl = "\n" | ||||||
|  | 	testcases := []struct { | ||||||
|  | 		testcase string | ||||||
|  | 		expected string | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			"$a$", | ||||||
|  | 			`<p><code class="language-math is-loading">a</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"$ a $", | ||||||
|  | 			`<p><code class="language-math is-loading">a</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"$a$ $b$", | ||||||
|  | 			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`\(a\) \(b\)`, | ||||||
|  | 			`<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`$a a$b b$`, | ||||||
|  | 			`<p><code class="language-math is-loading">a a$b b</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`a a$b b`, | ||||||
|  | 			`<p>a a$b b</p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			`a$b $a a$b b$`, | ||||||
|  | 			`<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"$$a$$", | ||||||
|  | 			`<pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre>` + nl, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, test := range testcases { | ||||||
|  | 		res, err := RenderString(&markup.RenderContext{}, test.testcase) | ||||||
|  | 		assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) | ||||||
|  | 		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ func NewInlineBracketParser() parser.InlineParser { | |||||||
| 	return defaultInlineBracketParser | 	return defaultInlineBracketParser | ||||||
| } | } | ||||||
|  |  | ||||||
| // Trigger triggers this parser on $ | // Trigger triggers this parser on $ or \ | ||||||
| func (parser *inlineParser) Trigger() []byte { | func (parser *inlineParser) Trigger() []byte { | ||||||
| 	return parser.start[0:1] | 	return parser.start[0:1] | ||||||
| } | } | ||||||
| @@ -50,29 +50,50 @@ func isAlphanumeric(b byte) bool { | |||||||
| // Parse parses the current line and returns a result of parsing. | // Parse parses the current line and returns a result of parsing. | ||||||
| func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { | func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { | ||||||
| 	line, _ := block.PeekLine() | 	line, _ := block.PeekLine() | ||||||
| 	opener := bytes.Index(line, parser.start) |  | ||||||
| 	if opener < 0 { | 	if !bytes.HasPrefix(line, parser.start) { | ||||||
| 		return nil | 		// We'll catch this one on the next time round | ||||||
| 	} |  | ||||||
| 	if opener != 0 && isAlphanumeric(line[opener-1]) { |  | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opener += len(parser.start) | 	precedingCharacter := block.PrecendingCharacter() | ||||||
| 	ender := bytes.Index(line[opener:], parser.end) | 	if precedingCharacter < 256 && isAlphanumeric(byte(precedingCharacter)) { | ||||||
| 	if ender < 0 { | 		// need to exclude things like `a$` from being considered a start | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	if len(line) > opener+ender+len(parser.end) && isAlphanumeric(line[opener+ender+len(parser.end)]) { |  | ||||||
| 		return nil | 	// move the opener marker point at the start of the text | ||||||
|  | 	opener := len(parser.start) | ||||||
|  |  | ||||||
|  | 	// Now look for an ending line | ||||||
|  | 	ender := opener | ||||||
|  | 	for { | ||||||
|  | 		pos := bytes.Index(line[ender:], parser.end) | ||||||
|  | 		if pos < 0 { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		ender += pos | ||||||
|  |  | ||||||
|  | 		// Now we want to check the character at the end of our parser section | ||||||
|  | 		// that is ender + len(parser.end) | ||||||
|  | 		pos = ender + len(parser.end) | ||||||
|  | 		if len(line) <= pos { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		if !isAlphanumeric(line[pos]) { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		// move the pointer onwards | ||||||
|  | 		ender += len(parser.end) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	block.Advance(opener) | 	block.Advance(opener) | ||||||
| 	_, pos := block.Position() | 	_, pos := block.Position() | ||||||
| 	node := NewInline() | 	node := NewInline() | ||||||
| 	segment := pos.WithStop(pos.Start + ender) | 	segment := pos.WithStop(pos.Start + ender - opener) | ||||||
| 	node.AppendChild(node, ast.NewRawTextSegment(segment)) | 	node.AppendChild(node, ast.NewRawTextSegment(segment)) | ||||||
| 	block.Advance(ender + len(parser.end)) | 	block.Advance(ender - opener + len(parser.end)) | ||||||
|  |  | ||||||
| 	trimBlock(node, block) | 	trimBlock(node, block) | ||||||
| 	return node | 	return node | ||||||
|   | |||||||
| @@ -88,7 +88,9 @@ func ExtractMetadataBytes(contents []byte, out interface{}) ([]byte, error) { | |||||||
| 		line := contents[start:end] | 		line := contents[start:end] | ||||||
| 		if isYAMLSeparator(line) { | 		if isYAMLSeparator(line) { | ||||||
| 			front = contents[frontMatterStart:start] | 			front = contents[frontMatterStart:start] | ||||||
| 			body = contents[end+1:] | 			if end+1 < len(contents) { | ||||||
|  | 				body = contents[end+1:] | ||||||
|  | 			} | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ func TestExtractMetadataBytes(t *testing.T) { | |||||||
| 		var meta structs.IssueTemplate | 		var meta structs.IssueTemplate | ||||||
| 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) | 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, bodyTest, body) | 		assert.Equal(t, bodyTest, string(body)) | ||||||
| 		assert.Equal(t, metaTest, meta) | 		assert.Equal(t, metaTest, meta) | ||||||
| 		assert.True(t, validateMetadata(meta)) | 		assert.True(t, validateMetadata(meta)) | ||||||
| 	}) | 	}) | ||||||
| @@ -82,7 +82,7 @@ func TestExtractMetadataBytes(t *testing.T) { | |||||||
| 		var meta structs.IssueTemplate | 		var meta structs.IssueTemplate | ||||||
| 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) | 		body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, "", body) | 		assert.Equal(t, "", string(body)) | ||||||
| 		assert.Equal(t, metaTest, meta) | 		assert.Equal(t, metaTest, meta) | ||||||
| 		assert.True(t, validateMetadata(meta)) | 		assert.True(t, validateMetadata(meta)) | ||||||
| 	}) | 	}) | ||||||
|   | |||||||
| @@ -5,10 +5,9 @@ | |||||||
| package markdown | package markdown | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
|  |  | ||||||
| 	"github.com/yuin/goldmark/ast" | 	"github.com/yuin/goldmark/ast" | ||||||
| 	"gopkg.in/yaml.v3" | 	"gopkg.in/yaml.v3" | ||||||
| ) | ) | ||||||
| @@ -33,17 +32,13 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { | |||||||
| 	} | 	} | ||||||
| 	rc.yamlNode = value | 	rc.yamlNode = value | ||||||
|  |  | ||||||
| 	type basicRenderConfig struct { | 	type commonRenderConfig struct { | ||||||
| 		Gitea *yaml.Node `yaml:"gitea"` | 		TOC  bool   `yaml:"include_toc"` | ||||||
| 		TOC   bool       `yaml:"include_toc"` | 		Lang string `yaml:"lang"` | ||||||
| 		Lang  string     `yaml:"lang"` |  | ||||||
| 	} | 	} | ||||||
|  | 	var basic commonRenderConfig | ||||||
| 	var basic basicRenderConfig | 	if err := value.Decode(&basic); err != nil { | ||||||
|  | 		return fmt.Errorf("unable to decode into commonRenderConfig %w", err) | ||||||
| 	err := value.Decode(&basic) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if basic.Lang != "" { | 	if basic.Lang != "" { | ||||||
| @@ -51,14 +46,48 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	rc.TOC = basic.TOC | 	rc.TOC = basic.TOC | ||||||
| 	if basic.Gitea == nil { |  | ||||||
|  | 	type controlStringRenderConfig struct { | ||||||
|  | 		Gitea string `yaml:"gitea"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var stringBasic controlStringRenderConfig | ||||||
|  |  | ||||||
|  | 	if err := value.Decode(&stringBasic); err == nil { | ||||||
|  | 		if stringBasic.Gitea != "" { | ||||||
|  | 			switch strings.TrimSpace(strings.ToLower(stringBasic.Gitea)) { | ||||||
|  | 			case "none": | ||||||
|  | 				rc.Meta = "none" | ||||||
|  | 			case "table": | ||||||
|  | 				rc.Meta = "table" | ||||||
|  | 			default: // "details" | ||||||
|  | 				rc.Meta = "details" | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var control *string | 	type giteaControl struct { | ||||||
| 	if err := basic.Gitea.Decode(&control); err == nil && control != nil { | 		Meta *string `yaml:"meta"` | ||||||
| 		log.Info("control %v", control) | 		Icon *string `yaml:"details_icon"` | ||||||
| 		switch strings.TrimSpace(strings.ToLower(*control)) { | 		TOC  *bool   `yaml:"include_toc"` | ||||||
|  | 		Lang *string `yaml:"lang"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	type complexGiteaConfig struct { | ||||||
|  | 		Gitea *giteaControl `yaml:"gitea"` | ||||||
|  | 	} | ||||||
|  | 	var complex complexGiteaConfig | ||||||
|  | 	if err := value.Decode(&complex); err != nil { | ||||||
|  | 		return fmt.Errorf("unable to decode into complexRenderConfig %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if complex.Gitea == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if complex.Gitea.Meta != nil { | ||||||
|  | 		switch strings.TrimSpace(strings.ToLower(*complex.Gitea.Meta)) { | ||||||
| 		case "none": | 		case "none": | ||||||
| 			rc.Meta = "none" | 			rc.Meta = "none" | ||||||
| 		case "table": | 		case "table": | ||||||
| @@ -66,39 +95,18 @@ func (rc *RenderConfig) UnmarshalYAML(value *yaml.Node) error { | |||||||
| 		default: // "details" | 		default: // "details" | ||||||
| 			rc.Meta = "details" | 			rc.Meta = "details" | ||||||
| 		} | 		} | ||||||
| 		return nil |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	type giteaControl struct { | 	if complex.Gitea.Icon != nil { | ||||||
| 		Meta string     `yaml:"meta"` | 		rc.Icon = strings.TrimSpace(strings.ToLower(*complex.Gitea.Icon)) | ||||||
| 		Icon string     `yaml:"details_icon"` |  | ||||||
| 		TOC  *yaml.Node `yaml:"include_toc"` |  | ||||||
| 		Lang string     `yaml:"lang"` |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var controlStruct *giteaControl | 	if complex.Gitea.Lang != nil && *complex.Gitea.Lang != "" { | ||||||
| 	if err := basic.Gitea.Decode(controlStruct); err != nil || controlStruct == nil { | 		rc.Lang = *complex.Gitea.Lang | ||||||
| 		return err |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch strings.TrimSpace(strings.ToLower(controlStruct.Meta)) { | 	if complex.Gitea.TOC != nil { | ||||||
| 	case "none": | 		rc.TOC = *complex.Gitea.TOC | ||||||
| 		rc.Meta = "none" |  | ||||||
| 	case "table": |  | ||||||
| 		rc.Meta = "table" |  | ||||||
| 	default: // "details" |  | ||||||
| 		rc.Meta = "details" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	rc.Icon = strings.TrimSpace(strings.ToLower(controlStruct.Icon)) |  | ||||||
|  |  | ||||||
| 	if controlStruct.Lang != "" { |  | ||||||
| 		rc.Lang = controlStruct.Lang |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var toc bool |  | ||||||
| 	if err := controlStruct.TOC.Decode(&toc); err == nil { |  | ||||||
| 		rc.TOC = toc |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| package markdown | package markdown | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"gopkg.in/yaml.v3" | 	"gopkg.in/yaml.v3" | ||||||
| @@ -81,9 +82,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { | |||||||
| 				TOC:  true, | 				TOC:  true, | ||||||
| 				Lang: "testlang", | 				Lang: "testlang", | ||||||
| 			}, ` | 			}, ` | ||||||
| 	include_toc: true | 				include_toc: true | ||||||
| 	lang: testlang | 				lang: testlang | ||||||
| `, | 				`, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"complexlang", &RenderConfig{ | 			"complexlang", &RenderConfig{ | ||||||
| @@ -91,9 +92,9 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { | |||||||
| 				Icon: "table", | 				Icon: "table", | ||||||
| 				Lang: "testlang", | 				Lang: "testlang", | ||||||
| 			}, ` | 			}, ` | ||||||
| 	gitea: | 				gitea: | ||||||
| 		lang: testlang | 					lang: testlang | ||||||
| `, | 				`, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"complexlang2", &RenderConfig{ | 			"complexlang2", &RenderConfig{ | ||||||
| @@ -140,8 +141,8 @@ func TestRenderConfig_UnmarshalYAML(t *testing.T) { | |||||||
| 				Icon: "table", | 				Icon: "table", | ||||||
| 				Lang: "", | 				Lang: "", | ||||||
| 			} | 			} | ||||||
| 			if err := yaml.Unmarshal([]byte(tt.args), got); err != nil { | 			if err := yaml.Unmarshal([]byte(strings.ReplaceAll(tt.args, "\t", "    ")), got); err != nil { | ||||||
| 				t.Errorf("RenderConfig.UnmarshalYAML() error = %v", err) | 				t.Errorf("RenderConfig.UnmarshalYAML() error = %v\n%q", err, tt.args) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 zeripath
					zeripath