Skip to content

Commit 0cb8472

Browse files
committed
fix(conversion): keep word boundaries across buffered text flushes
flushWordAlignedPrefix trimmed the buffer before splitting it, but a trailing space is the only record that a word boundary was already consumed. When the byte crossing the 220-byte threshold was that space, the trim discarded it and the next word was appended straight onto the buffered remainder.
1 parent 234e7d8 commit 0cb8472

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

src/conversion/epub/EpubContentParser.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,6 @@ namespace EpubContent {
284284
}
285285

286286
bool Parser::flushWordAlignedPrefix() {
287-
line_ = std::string{AsciiText::trim(line_)};
288287
int split = static_cast<int>(line_.length()) - 1;
289288
while (split >= 0 && !AsciiText::isWhitespace(line_[split])) {
290289
--split;

test/test_epub_conversion/test_main.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <unity.h>
44

55
#include <span>
6+
#include <string>
67
#include <vector>
78

89
#include "conversion/epub/EpubContentParser.h"
@@ -156,6 +157,38 @@ namespace {
156157
TEST_ASSERT_EQUAL(std::string::npos, output.contents().find("@writing-mode vertical-rl", first + 1));
157158
}
158159

160+
// Buffered text is flushed once it grows past 220 bytes. When the byte that crosses the
161+
// threshold is the word boundary itself, that boundary has to survive the flush, otherwise the
162+
// next word is glued onto the buffered remainder.
163+
//
164+
// "Dear Reader " is 12 bytes, so 18 of them fill the buffer to 216; the following "Dear" ends
165+
// it at exactly 220 and the separator after it is the byte that triggers the flush.
166+
void test_parser_keeps_word_boundary_across_buffered_text_flush() {
167+
const std::string padding = [] {
168+
std::string filled;
169+
for (int i = 0; i < 18; ++i)
170+
filled += "Dear Reader ";
171+
return filled;
172+
}();
173+
174+
// A literal space and a "&nbsp;" entity are the same boundary: the entity decodes to a
175+
// plain space before it ever reaches the buffer.
176+
for (const std::string_view separator : {" ", "&nbsp;"}) {
177+
File output;
178+
const std::string markup =
179+
"<body><p>" + padding + "Dear" + std::string{separator} + "Reader follows</p></body>";
180+
181+
RsvpWriter writer(output, {.source = "fixture.epub", .title = "Fixture"});
182+
EpubContent::Parser parser(writer, {}, false, "Fixture", "Fixture");
183+
TEST_ASSERT_TRUE(parser.write(reinterpret_cast<const uint8_t*>(markup.data()), markup.length()));
184+
TEST_ASSERT_TRUE(parser.finish());
185+
TEST_ASSERT_TRUE(writer.finish());
186+
187+
TEST_ASSERT_EQUAL(std::string::npos, output.contents().find("DearReader"));
188+
TEST_ASSERT_TRUE(output.contents().contains("Dear\nReader follows\n"));
189+
}
190+
}
191+
159192
} // namespace
160193

161194
int main(int, char**) {
@@ -168,5 +201,6 @@ int main(int, char**) {
168201
RUN_TEST(test_parser_preserves_punctuation_only_inline_fragments_without_counting_them_as_words);
169202
RUN_TEST(test_parser_preserves_nested_language_and_direction_changes);
170203
RUN_TEST(test_parser_emits_explicit_vertical_writing_mode_once);
204+
RUN_TEST(test_parser_keeps_word_boundary_across_buffered_text_flush);
171205
return UNITY_END();
172206
}

0 commit comments

Comments
 (0)