fix(outboundtext): keep resource handles intact when normalizing formulas
This commit is contained in:
@@ -30,4 +30,4 @@ func TestEffectiveDownloadCodesEmpty(t *testing.T) {
|
||||
if codes := effectiveDownloadCodes(BotCallback{}); len(codes) != 0 {
|
||||
t.Fatalf("codes=%v, want empty", codes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,4 +77,4 @@ func TestPublishQuestionImageFailsClosedWithoutURL(t *testing.T) {
|
||||
if got := h2.publishQuestionImage(context.Background(), image); got != "" {
|
||||
t.Fatalf("expected empty URL on publish error, got %q", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package dingtalk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/outboundimage"
|
||||
)
|
||||
|
||||
type diagFetcher struct{}
|
||||
|
||||
func (diagFetcher) Fetch(ctx context.Context, ref string) ([]byte, string, error) {
|
||||
return []byte("PNGDATA"), "image/png", nil
|
||||
}
|
||||
|
||||
type diagPublisher struct{}
|
||||
|
||||
func (diagPublisher) Publish(ctx context.Context, data []byte, contentType string) (string, error) {
|
||||
return "https://weknora-api-test.dobot.cn/dingtalk/i/a87f73b8-15cb-546e-ba53-bf93dade916f", nil
|
||||
}
|
||||
|
||||
// The outbound rewrite must publish a resource image and keep it in the card
|
||||
// body. A reference whose handle contains "_x" (…ZM_pltQ) used to be mangled by
|
||||
// the math normalizer, which made the image unpublishable and the sanitizer
|
||||
// drop the whole line.
|
||||
func TestRewriteOutboundContentKeepsPublishedImage(t *testing.T) {
|
||||
content := "\n\n公式:$r = \\sqrt{X^2 + Y^2}$\n"
|
||||
h := &Handler{
|
||||
outboundRewriter: outboundimage.NewRewriter(diagFetcher{}, diagPublisher{}, 50),
|
||||
groupTablesFinal: true,
|
||||
}
|
||||
out := h.rewriteOutboundContent(context.Background(), content)
|
||||
if !strings.Contains(out, " {
|
||||
t.Fatalf("published image missing from card content: %q", out)
|
||||
}
|
||||
if !strings.Contains(out, "r = √(X² + Y²)") {
|
||||
t.Fatalf("formula not converted: %q", out)
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
// well-known math commands, so ordinary prose is left byte-identical.
|
||||
|
||||
var (
|
||||
mathBlockDelimRE = regexp.MustCompile(`\$\$([\s\S]+?)\$\$`)
|
||||
mathInlineDelimRE = regexp.MustCompile(`\$([^$\n]{1,400})\$`)
|
||||
parenDelimRE = regexp.MustCompile(`\\\(([\s\S]+?)\\\)`)
|
||||
bracketDelimRE = regexp.MustCompile(`\\\[([\s\S]+?)\\\]`)
|
||||
sqrtRE = regexp.MustCompile(`\\sqrt\s*(?:\[([^\]]{1,20})\])?\s*\{([^{}]{1,200})\}`)
|
||||
fracRE = regexp.MustCompile(`\\frac\s*\{([^{}]{1,200})\}\s*\{([^{}]{1,200})\}`)
|
||||
supBracedRE = regexp.MustCompile(`\^\s*\{([^{}]{1,40})\}`)
|
||||
subBracedRE = regexp.MustCompile(`_\s*\{([^{}]{1,40})\}`)
|
||||
supCharRE = regexp.MustCompile(`\^([0-9A-Za-z+\-=()])`)
|
||||
subCharRE = regexp.MustCompile(`_([0-9A-Za-z+\-=()])`)
|
||||
commandRE = regexp.MustCompile(`\\([A-Za-z]+)`)
|
||||
braceDelimRE = regexp.MustCompile(`\{([^{}]{0,200})\}`)
|
||||
blankRunRE = regexp.MustCompile(`[ \t]{2,}`)
|
||||
mathBlockDelimRE = regexp.MustCompile(`\$\$([\s\S]+?)\$\$`)
|
||||
mathSpanRE = regexp.MustCompile(`(?s)\$\$.+?\$\$|\\\[.+?\\\]|\\\(.+?\\\)|\$[^$\n]{1,400}\$`)
|
||||
parenDelimRE = regexp.MustCompile(`\\\(([\s\S]+?)\\\)`)
|
||||
bracketDelimRE = regexp.MustCompile(`\\\[([\s\S]+?)\\\]`)
|
||||
sqrtRE = regexp.MustCompile(`\\sqrt\s*(?:\[([^\]]{1,20})\])?\s*\{([^{}]{1,200})\}`)
|
||||
fracRE = regexp.MustCompile(`\\frac\s*\{([^{}]{1,200})\}\s*\{([^{}]{1,200})\}`)
|
||||
supBracedRE = regexp.MustCompile(`\^\s*\{([^{}]{1,40})\}`)
|
||||
subBracedRE = regexp.MustCompile(`_\s*\{([^{}]{1,40})\}`)
|
||||
supCharRE = regexp.MustCompile(`\^([0-9A-Za-z+\-=()])`)
|
||||
subCharRE = regexp.MustCompile(`_([0-9A-Za-z+\-=()])`)
|
||||
commandRE = regexp.MustCompile(`\\([A-Za-z]+)`)
|
||||
braceDelimRE = regexp.MustCompile(`\{([^{}]{0,200})\}`)
|
||||
blankRunRE = regexp.MustCompile(`[ \t]{2,}`)
|
||||
)
|
||||
|
||||
var mathCommands = map[string]string{
|
||||
|
||||
@@ -6,14 +6,36 @@ func NormalizeMath(markdown string) string {
|
||||
if !carriesMath(markdown) {
|
||||
return markdown
|
||||
}
|
||||
content := markdown
|
||||
content := mathSpanRE.ReplaceAllStringFunc(markdown, convertMathSpan)
|
||||
// Outside math spans only LaTeX commands are converted. Identifiers and URLs
|
||||
// (resource://…_pltQ) must never be rewritten: a mangled reference can no
|
||||
// longer be published, and the outbound sanitizer then drops the whole image.
|
||||
content = commandRE.ReplaceAllStringFunc(content, replaceCommand)
|
||||
content = blankRunRE.ReplaceAllString(content, " ")
|
||||
return content
|
||||
}
|
||||
|
||||
func convertMathSpan(span string) string {
|
||||
inner := span
|
||||
switch {
|
||||
case strings.HasPrefix(inner, "$$"):
|
||||
inner = strings.TrimSuffix(strings.TrimPrefix(inner, "$$"), "$$")
|
||||
return "\n" + convertMath(inner) + "\n"
|
||||
case strings.HasPrefix(inner, `\[`):
|
||||
inner = strings.TrimSuffix(strings.TrimPrefix(inner, `\[`), `\]`)
|
||||
case strings.HasPrefix(inner, `\(`):
|
||||
inner = strings.TrimSuffix(strings.TrimPrefix(inner, `\(`), `\)`)
|
||||
default:
|
||||
inner = strings.TrimSuffix(strings.TrimPrefix(inner, "$"), "$")
|
||||
}
|
||||
return convertMath(inner)
|
||||
}
|
||||
|
||||
func convertMath(inner string) string {
|
||||
content := inner
|
||||
for from, to := range escapedPunctuation {
|
||||
content = strings.ReplaceAll(content, from, to)
|
||||
}
|
||||
content = mathBlockDelimRE.ReplaceAllString(content, "\n$1\n")
|
||||
content = parenDelimRE.ReplaceAllString(content, "$1")
|
||||
content = bracketDelimRE.ReplaceAllString(content, "$1")
|
||||
content = mathInlineDelimRE.ReplaceAllString(content, "$1")
|
||||
for i := 0; i < 3; i++ {
|
||||
next := sqrtRE.ReplaceAllStringFunc(content, expandRoot)
|
||||
next = fracRE.ReplaceAllString(next, "($1)/($2)")
|
||||
@@ -28,7 +50,6 @@ func NormalizeMath(markdown string) string {
|
||||
}
|
||||
content = commandRE.ReplaceAllStringFunc(content, replaceCommand)
|
||||
content = braceDelimRE.ReplaceAllString(content, "$1")
|
||||
content = blankRunRE.ReplaceAllString(content, " ")
|
||||
return content
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ func TestNormalizeMath(t *testing.T) {
|
||||
{"nth root", `$\sqrt[3]{V}$`, `√[3](V)`},
|
||||
{"no math untouched", `C:\new\folder 与 100% 覆盖`, `C:\new\folder 与 100% 覆盖`},
|
||||
{"dollar free prose", `价格是 5 元, 无公式`, `价格是 5 元, 无公式`},
|
||||
{"resource url untouched", ` 公式 $r = \sqrt{X^2 + Y^2}$`, ` 公式 r = √(X² + Y²)`},
|
||||
{"code span untouched", "`x_1` 与 $x_1$", "`x_1` 与 x₁"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user