fix(markup): prevent a multi-byte character after a tag from rendering as broken

When the read buffer ends inside a multi-byte character that directly
follows a tag, keep the cut bytes for the next read instead of emitting
them one by one as broken characters.

Assisted-by: Claude Code:claude-opus-5-5
This commit is contained in:
Hyeonmin Park
2026-11-03 11:02:57 +09:00
parent 2ed6a6f61b
commit 2a1d09755f
2 changed files with 11 additions and 0 deletions

View File

@@ -246,6 +246,9 @@ func (e *htmlChunkReader) readRunes() (parts [][]byte, partInTag []bool, _ error
}
}
if curPartLen == 0 {
if e.readErr == nil && curPartEnd == len(e.readBuf) && !utf8.FullRune(curPart) {
break // the part is a rune cut by the read boundary, keep it for the next read
}
// actually it's impossible that the part doesn't contain any valid rune,
// the only case is that the cap(readBuf) is too small, or the origin contain indeed doesn't contain any valid rune
// * try to leave the last 4 bytes (possible longest utf-8 encoding) to next round

View File

@@ -234,4 +234,12 @@ func TestHTMLChunkReader(t *testing.T) {
testReadChunks(t, 5, input, []textPart{{"<Aé", true}, {"啊", true}, {"🌞>", true}, {"Aé", false}, {"啊", false}, {"🌞", false}})
testReadChunks(t, 6, input, []textPart{{"<Aé", true}, {"啊", true}, {"🌞>", true}, {"A", false}, {"é啊", false}, {"🌞", false}})
testReadChunks(t, 7, input, []textPart{{"<Aé啊", true}, {"🌞>", true}, {"A", false}, {"é啊", false}, {"🌞", false}})
// a rune right after a tag must stay whole when the chunk boundary cuts it
for _, r := range []string{rune2, rune3, rune4} {
for chunkSize := 6; chunkSize < 5+len(r); chunkSize++ {
testReadChunks(t, chunkSize, "<abc>"+r, []textPart{{"<abc>", true}, {r, false}})
}
}
testReadChunks(t, 6, "<abc>"+rune3[:2], []textPart{{"<abc>", true}, {rune3[:1], false}, {rune3[1:2], false}})
}