| 24 | } // namespace |
| 25 | |
| 26 | std::string utf8ComposeNfc(const std::string& in) { |
| 27 | // Fast path: NFC composition can only change text that contains a combining |
| 28 | // diacritical mark U+0300-036F (UTF-8 lead byte 0xCC or 0xCD). Plain ASCII and |
| 29 | // already-precomposed (NFC) text -- the vast majority of words -- have none, so |
| 30 | // return them untouched without walking codepoints or allocating. A 0xCD that is |
| 31 | // actually a non-combining codepoint just falls through to the full pass below. |
| 32 | bool maybeHasMarks = false; |
| 33 | for (const unsigned char c : in) { |
| 34 | if (c == 0xCC || c == 0xCD) { |
| 35 | maybeHasMarks = true; |
| 36 | break; |
| 37 | } |
| 38 | } |
| 39 | if (!maybeHasMarks) return in; |
| 40 | |
| 41 | std::string out; |
| 42 | out.reserve(in.size()); |
| 43 | const unsigned char* p = reinterpret_cast<const unsigned char*>(in.c_str()); |
| 44 | uint32_t base = 0; |
| 45 | bool haveBase = false; |
| 46 | while (*p) { |
| 47 | const uint32_t cp = utf8NextCodepoint(&p); |
| 48 | if (cp == 0) break; |
| 49 | if (utf8IsCombiningMark(cp)) { |
| 50 | const uint32_t composed = haveBase ? utf8ComposePair(base, cp) : 0; |
| 51 | if (composed) { |
| 52 | base = composed; // keep accumulating further marks onto the composed char |
| 53 | continue; |
| 54 | } |
| 55 | // No composition: flush the pending base, then emit the mark unchanged. |
| 56 | if (haveBase) { |
| 57 | utf8AppendCodepoint(base, out); |
| 58 | haveBase = false; |
| 59 | } |
| 60 | utf8AppendCodepoint(cp, out); |
| 61 | } else { |
| 62 | if (haveBase) utf8AppendCodepoint(base, out); |
| 63 | base = cp; |
| 64 | haveBase = true; |
| 65 | } |
| 66 | } |
| 67 | if (haveBase) utf8AppendCodepoint(base, out); |
| 68 | return out; |
| 69 | } |
| 70 | |
| 71 | int utf8CodepointLen(const unsigned char c) { |
| 72 | if (c < 0x80) return 1; // 0xxxxxxx |