| 251 | } // namespace |
| 252 | |
| 253 | void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline, |
| 254 | const bool attachToPrevious) { |
| 255 | if (word.empty()) return; |
| 256 | |
| 257 | // The device fonts carry no combining-mark positioning, so EPUB text stored in NFD |
| 258 | // (a base letter followed by separate combining accents -- common for Vietnamese, |
| 259 | // and used for many EPUB <h1> chapter headings) renders with the marks detached or |
| 260 | // misplaced. Compose to NFC here, the single funnel every word passes through, so a |
| 261 | // precomposed glyph is used instead. This runs once per word at layout time (the |
| 262 | // result is cached in the section file) and is a cheap no-op for mark-free text. |
| 263 | word = utf8ComposeNfc(word); |
| 264 | |
| 265 | EpdFontFamily::Style baseStyle = fontStyle; |
| 266 | if (underline) { |
| 267 | baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE); |
| 268 | } |
| 269 | const bool wordStartsRtl = !hasRtlWord && mayContainRtlBytes(word.c_str()) && |
| 270 | BidiUtils::startsWithRtl(word.c_str(), RTL_PER_WORD_PROBE_DEPTH); |
| 271 | |
| 272 | const auto pushToken = [&](std::string token, const bool continues, const bool noSpaceBefore, |
| 273 | const bool isFocusSuffix) { |
| 274 | words.push_back(std::move(token)); |
| 275 | wordStyles.push_back(baseStyle); |
| 276 | wordContinues.push_back(continues); |
| 277 | wordNoSpaceBefore.push_back(noSpaceBefore); |
| 278 | wordIsFocusSuffix.push_back(isFocusSuffix); |
| 279 | }; |
| 280 | |
| 281 | bool effectiveAttachToPrevious = attachToPrevious; |
| 282 | bool effectiveNoSpaceBefore = false; |
| 283 | if (attachToPrevious && !words.empty() && |
| 284 | hasCjkBreakOpportunityBetween(lastCodepoint(words.back()), firstCodepoint(word))) { |
| 285 | effectiveAttachToPrevious = false; |
| 286 | effectiveNoSpaceBefore = true; |
| 287 | } |
| 288 | |
| 289 | if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) { |
| 290 | bool firstToken = true; |
| 291 | size_t tokenStart = 0; |
| 292 | for (const size_t breakOffset : breakOffsets) { |
| 293 | if (breakOffset <= tokenStart || breakOffset > word.size()) continue; |
| 294 | pushToken(word.substr(tokenStart, breakOffset - tokenStart), firstToken ? effectiveAttachToPrevious : false, |
| 295 | firstToken ? effectiveNoSpaceBefore : true, false); |
| 296 | firstToken = false; |
| 297 | tokenStart = breakOffset; |
| 298 | } |
| 299 | if (tokenStart < word.size()) { |
| 300 | pushToken(word.substr(tokenStart), firstToken ? effectiveAttachToPrevious : false, |
| 301 | firstToken ? effectiveNoSpaceBefore : true, false); |
| 302 | } |
| 303 | if (wordStartsRtl) { |
| 304 | hasRtlWord = true; |
| 305 | } |
| 306 | return; |
| 307 | } |
| 308 | |
| 309 | if (containsCjkBreakableCodepoint(word)) { |
| 310 | pushToken(std::move(word), effectiveAttachToPrevious, effectiveNoSpaceBefore, false); |
no test coverage detected