This commit is contained in:
Arthur Tazhitdinov 2025-12-18 12:03:27 +00:00 committed by GitHub
commit f6f00a5e26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 947 additions and 120 deletions

View File

@ -3,11 +3,12 @@
#include <GfxRenderer.h>
#include <algorithm>
#include <cmath>
#include <functional>
#include <limits>
#include <iterator>
#include <utility>
#include <vector>
#include "hyphenation/Hyphenator.h"
constexpr int MAX_COST = std::numeric_limits<int>::max();
void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
@ -24,148 +25,168 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
return;
}
const size_t totalWordCount = words.size();
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
if (pageWidth <= 0) {
words.clear();
wordStyles.clear();
return;
}
const int spaceWidth = renderer.getSpaceWidth(fontId);
// width of 1em to indent first line of paragraph if Extra Spacing is enabled
const int indentWidth = (!extraParagraphSpacing) ? 1 * renderer.getTextWidth(fontId, "m", REGULAR) : 0;
const bool allowIndent = !extraParagraphSpacing && (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN);
const int indentWidth = allowIndent ? renderer.getTextWidth(fontId, "m", REGULAR) : 0;
const int firstLinePageWidth = allowIndent ? std::max(pageWidth - indentWidth, 0) : pageWidth;
auto pageWidthForLine = [&](const bool isFirstLine) -> int { return isFirstLine ? firstLinePageWidth : pageWidth; };
std::vector<uint16_t> wordWidths;
wordWidths.reserve(totalWordCount);
auto wordIt = words.begin();
auto styleIt = wordStyles.begin();
auto lineStartWordIt = wordIt;
auto lineStartStyleIt = styleIt;
auto wordsIt = words.begin();
auto wordStylesIt = wordStyles.begin();
int lineWidthWithSpaces = 0;
int lineWordWidthSum = 0;
size_t lineWordCount = 0;
std::vector<uint16_t> lineWordWidths;
lineWordWidths.reserve(16);
while (wordsIt != words.end()) {
wordWidths.push_back(renderer.getTextWidth(fontId, wordsIt->c_str(), *wordStylesIt));
std::advance(wordsIt, 1);
std::advance(wordStylesIt, 1);
}
// DP table to store the minimum badness (cost) of lines starting at index i
std::vector<int> dp(totalWordCount);
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
std::vector<size_t> ans(totalWordCount);
// Base Case
dp[totalWordCount - 1] = 0;
ans[totalWordCount - 1] = totalWordCount - 1;
for (int i = totalWordCount - 2; i >= 0; --i) {
int currlen = -spaceWidth + indentWidth;
dp[i] = MAX_COST;
for (size_t j = i; j < totalWordCount; ++j) {
// Current line length: previous width + space + current word width
currlen += wordWidths[j] + spaceWidth;
if (currlen > pageWidth) {
break;
}
int cost;
if (j == totalWordCount - 1) {
cost = 0; // Last line
} else {
const int remainingSpace = pageWidth - currlen;
// Use long long for the square to prevent overflow
const long long cost_ll = static_cast<long long>(remainingSpace) * remainingSpace + dp[j + 1];
if (cost_ll > MAX_COST) {
cost = MAX_COST;
} else {
cost = static_cast<int>(cost_ll);
}
}
if (cost < dp[i]) {
dp[i] = cost;
ans[i] = j; // j is the index of the last word in this optimal line
}
}
}
// Stores the index of the word that starts the next line (last_word_index + 1)
std::vector<size_t> lineBreakIndices;
size_t currentWordIndex = 0;
size_t producedLines = 0;
constexpr size_t MAX_LINES = 1000;
while (currentWordIndex < totalWordCount) {
if (lineBreakIndices.size() >= MAX_LINES) {
break;
auto commitLine = [&](const bool isLastLine) {
if (lineWordCount == 0) {
return;
}
size_t nextBreakIndex = ans[currentWordIndex] + 1;
lineBreakIndices.push_back(nextBreakIndex);
const bool isFirstLine = producedLines == 0;
const int linePageWidth = pageWidthForLine(isFirstLine);
currentWordIndex = nextBreakIndex;
}
std::list<std::string> lineWords;
std::list<EpdFontStyle> lineStyles;
auto wordEndIt = wordIt;
auto styleEndIt = styleIt;
// Initialize iterators for consumption
auto wordStartIt = words.begin();
auto wordStyleStartIt = wordStyles.begin();
size_t wordWidthIndex = 0;
lineWords.splice(lineWords.begin(), words, lineStartWordIt, wordEndIt);
lineStyles.splice(lineStyles.begin(), wordStyles, lineStartStyleIt, styleEndIt);
size_t lastBreakAt = 0;
for (const size_t lineBreak : lineBreakIndices) {
const size_t lineWordCount = lineBreak - lastBreakAt;
// Calculate end iterators for the range to splice
auto wordEndIt = wordStartIt;
auto wordStyleEndIt = wordStyleStartIt;
std::advance(wordEndIt, lineWordCount);
std::advance(wordStyleEndIt, lineWordCount);
// Calculate total word width for this line
int lineWordWidthSum = 0;
for (size_t i = 0; i < lineWordCount; ++i) {
lineWordWidthSum += wordWidths[wordWidthIndex + i];
}
// Calculate spacing
int spareSpace = pageWidth - lineWordWidthSum;
if (wordWidthIndex == 0) {
spareSpace -= indentWidth;
}
const int gaps = lineWordCount > 0 ? static_cast<int>(lineWordCount - 1) : 0;
const int baseSpaceTotal = spaceWidth * gaps;
const int spaceBudget = linePageWidth - lineWordWidthSum;
int spacing = spaceWidth;
const bool isLastLine = lineBreak == totalWordCount;
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
spacing = spareSpace / (lineWordCount - 1);
int spacingRemainder = 0;
if (style == TextBlock::JUSTIFIED && !isLastLine && gaps > 0) {
const int additional = std::max(0, spaceBudget - baseSpaceTotal);
spacing = spaceWidth + (gaps > 0 ? additional / gaps : 0);
spacingRemainder = (gaps > 0) ? additional % gaps : 0;
}
// Calculate initial x position
uint16_t xpos = (wordWidthIndex == 0) ? indentWidth : 0;
int renderedWidth = lineWordWidthSum;
if (gaps > 0) {
renderedWidth += spacing * gaps;
}
uint16_t xpos = 0;
if (style == TextBlock::RIGHT_ALIGN) {
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
xpos = renderedWidth < linePageWidth ? linePageWidth - renderedWidth : 0;
} else if (style == TextBlock::CENTER_ALIGN) {
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
xpos = renderedWidth < linePageWidth ? (linePageWidth - renderedWidth) / 2 : 0;
} else if (allowIndent && isFirstLine) {
xpos = indentWidth;
}
// Pre-calculate X positions for words
std::list<uint16_t> lineXPos;
for (size_t i = 0; i < lineWordCount; ++i) {
const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i];
for (size_t idx = 0; idx < lineWordWidths.size(); ++idx) {
lineXPos.push_back(xpos);
xpos += currentWordWidth + spacing;
xpos += lineWordWidths[idx];
if (idx + 1 < lineWordWidths.size()) {
int gap = spacing;
if (spacingRemainder > 0) {
gap += 1;
spacingRemainder--;
}
xpos += gap;
}
}
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
std::list<std::string> lineWords;
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
std::list<EpdFontStyle> lineWordStyles;
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineStyles), style));
processLine(
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
producedLines++;
lineWordWidths.clear();
lineWordWidthSum = 0;
lineWidthWithSpaces = 0;
lineWordCount = 0;
lineStartWordIt = wordIt;
lineStartStyleIt = styleIt;
};
// Update pointers/indices for the next line
wordStartIt = wordEndIt;
wordStyleStartIt = wordStyleEndIt;
wordWidthIndex += lineWordCount;
lastBreakAt = lineBreak;
while (wordIt != words.end() && producedLines < MAX_LINES) {
const int currentLinePageWidth = pageWidthForLine(producedLines == 0);
if (lineWordCount == 0) {
lineStartWordIt = wordIt;
lineStartStyleIt = styleIt;
}
const int wordWidth = renderer.getTextWidth(fontId, wordIt->c_str(), *styleIt);
const int gapWidth = (lineWordCount > 0) ? spaceWidth : 0;
const int candidateWidth = lineWidthWithSpaces + gapWidth + wordWidth;
if (candidateWidth <= currentLinePageWidth) {
lineWordWidths.push_back(static_cast<uint16_t>(wordWidth));
lineWordWidthSum += wordWidth;
lineWidthWithSpaces = candidateWidth;
lineWordCount++;
++wordIt;
++styleIt;
continue;
}
const int availableWidth = currentLinePageWidth - lineWidthWithSpaces - gapWidth;
if (lineWordCount > 0 && availableWidth <= 0) {
commitLine(false);
continue;
}
if (lineWordCount > 0 && availableWidth > 0) {
HyphenationResult split;
if (Hyphenator::splitWord(renderer, fontId, *wordIt, *styleIt, availableWidth, &split, false)) {
*wordIt = std::move(split.head);
auto nextWordIt = std::next(wordIt);
auto nextStyleIt = std::next(styleIt);
words.insert(nextWordIt, std::move(split.tail));
wordStyles.insert(nextStyleIt, *styleIt);
continue;
}
}
if (lineWordCount == 0) {
HyphenationResult split;
if (Hyphenator::splitWord(renderer, fontId, *wordIt, *styleIt, currentLinePageWidth, &split, true)) {
*wordIt = std::move(split.head);
auto nextWordIt = std::next(wordIt);
auto nextStyleIt = std::next(styleIt);
words.insert(nextWordIt, std::move(split.tail));
wordStyles.insert(nextStyleIt, *styleIt);
continue;
}
lineWordWidths.push_back(static_cast<uint16_t>(wordWidth));
lineWordWidthSum += wordWidth;
lineWidthWithSpaces = candidateWidth;
lineWordCount = 1;
++wordIt;
++styleIt;
commitLine(wordIt == words.end());
continue;
}
commitLine(false);
}
}
if (lineWordCount > 0 && producedLines < MAX_LINES) {
commitLine(true);
}
words.clear();
wordStyles.clear();
}

View File

@ -0,0 +1,278 @@
#include "EnglishHyphenator.h"
#include <algorithm>
#include <initializer_list>
#include <vector>
namespace {
char lowerLatinChar(const uint32_t cp) {
if (!isLatinLetter(cp)) {
return 0;
}
return static_cast<char>(toLowerLatin(cp));
}
bool isEnglishApproximantChar(const char c) { return c == 'l' || c == 'r' || c == 'w' || c == 'y'; }
bool isEnglishStopChar(const char c) {
switch (c) {
case 'p':
case 'b':
case 't':
case 'd':
case 'k':
case 'g':
case 'c':
case 'q':
return true;
default:
return false;
}
}
bool isEnglishFricativeChar(const char c) {
switch (c) {
case 'f':
case 'v':
case 's':
case 'z':
case 'h':
case 'x':
return true;
default:
return false;
}
}
struct CharPair {
char first;
char second;
};
bool matchesDigraph(const char first, const char second, const std::initializer_list<CharPair>& pairs) {
for (const auto& pair : pairs) {
if (pair.first == first && pair.second == second) {
return true;
}
}
return false;
}
bool isEnglishDiphthong(const uint32_t first, const uint32_t second) {
if (!isLatinLetter(first) || !isLatinLetter(second)) {
return false;
}
const auto f = static_cast<char>(toLowerLatin(first));
const auto s = static_cast<char>(toLowerLatin(second));
switch (f) {
case 'a':
return s == 'i' || s == 'y' || s == 'u';
case 'e':
return s == 'a' || s == 'e' || s == 'i' || s == 'o' || s == 'u' || s == 'y';
case 'i':
return s == 'e' || s == 'u' || s == 'a';
case 'o':
return s == 'a' || s == 'e' || s == 'i' || s == 'o' || s == 'u' || s == 'y';
case 'u':
return s == 'i' || s == 'a' || s == 'e';
}
return false;
}
bool isValidEnglishOnsetBigram(const uint32_t firstCp, const uint32_t secondCp) {
const char first = lowerLatinChar(firstCp);
const char second = lowerLatinChar(secondCp);
if (!first || !second) {
return false;
}
if (matchesDigraph(first, second,
{{'c', 'h'},
{'s', 'h'},
{'t', 'h'},
{'p', 'h'},
{'w', 'h'},
{'w', 'r'},
{'k', 'n'},
{'g', 'n'},
{'p', 's'},
{'p', 't'},
{'p', 'n'},
{'r', 'h'}})) {
return true;
}
if (isEnglishStopChar(first) && isEnglishApproximantChar(second)) {
return true;
}
if (isEnglishFricativeChar(first) && isEnglishApproximantChar(second)) {
return true;
}
if (first == 's' && (second == 'p' || second == 't' || second == 'k' || second == 'm' || second == 'n' ||
second == 'f' || second == 'l' || second == 'w' || second == 'c')) {
return true;
}
if (second == 'y' && (first == 'p' || first == 'b' || first == 't' || first == 'd' || first == 'f' || first == 'k' ||
first == 'g' || first == 'h' || first == 'm' || first == 'n' || first == 'l' || first == 's')) {
return true;
}
return false;
}
bool isValidEnglishOnsetTrigram(const uint32_t firstCp, const uint32_t secondCp, const uint32_t thirdCp) {
const char first = lowerLatinChar(firstCp);
const char second = lowerLatinChar(secondCp);
const char third = lowerLatinChar(thirdCp);
if (!first || !second || !third) {
return false;
}
if (first == 's') {
if (second == 'p' && (third == 'l' || third == 'r' || third == 'w')) {
return true;
}
if (second == 't' && (third == 'r' || third == 'w' || third == 'y')) {
return true;
}
if (second == 'k' && (third == 'l' || third == 'r' || third == 'w')) {
return true;
}
if (second == 'c' && (third == 'l' || third == 'r')) {
return true;
}
if (second == 'f' && third == 'r') {
return true;
}
if (second == 'h' && third == 'r') {
return true;
}
}
if (first == 't' && second == 'h' && third == 'r') {
return true;
}
return false;
}
bool englishClusterIsValidOnset(const std::vector<CodepointInfo>& cps, const size_t start, const size_t end) {
if (start >= end) {
return false;
}
for (size_t i = start; i < end; ++i) {
const char ch = lowerLatinChar(cps[i].value);
if (!ch) {
return false;
}
if (!isLatinConsonant(cps[i].value) && ch != 'y') {
return false;
}
}
const size_t len = end - start;
if (len == 1) {
return true;
}
if (len == 2) {
return isValidEnglishOnsetBigram(cps[start].value, cps[start + 1].value);
}
if (len == 3) {
return isValidEnglishOnsetTrigram(cps[start].value, cps[start + 1].value, cps[start + 2].value);
}
return false;
}
size_t englishOnsetLength(const std::vector<CodepointInfo>& cps, const size_t clusterStart, const size_t clusterEnd) {
const size_t clusterLen = clusterEnd - clusterStart;
if (clusterLen == 0) {
return 0;
}
const size_t maxLen = std::min<size_t>(3, clusterLen);
for (size_t len = maxLen; len >= 1; --len) {
const size_t suffixStart = clusterEnd - len;
if (englishClusterIsValidOnset(cps, suffixStart, clusterEnd)) {
return len;
}
}
return 1;
}
bool nextToApostrophe(const std::vector<CodepointInfo>& cps, const size_t index) {
if (index == 0 || index >= cps.size()) {
return false;
}
const auto left = cps[index - 1].value;
const auto right = cps[index].value;
return left == '\'' || right == '\'';
}
std::vector<size_t> englishBreakIndexes(const std::vector<CodepointInfo>& cps) {
std::vector<size_t> indexes;
if (cps.size() < MIN_PREFIX_CP + MIN_SUFFIX_CP) {
return indexes;
}
std::vector<size_t> vowelPositions;
vowelPositions.reserve(cps.size());
for (size_t i = 0; i < cps.size(); ++i) {
if (isLatinVowel(cps[i].value)) {
vowelPositions.push_back(i);
}
}
if (vowelPositions.size() < 2) {
return indexes;
}
for (size_t v = 0; v + 1 < vowelPositions.size(); ++v) {
const size_t leftVowel = vowelPositions[v];
const size_t rightVowel = vowelPositions[v + 1];
if (rightVowel - leftVowel == 1) {
if (!isEnglishDiphthong(cps[leftVowel].value, cps[rightVowel].value) && rightVowel >= MIN_PREFIX_CP &&
cps.size() - rightVowel >= MIN_SUFFIX_CP && !nextToApostrophe(cps, rightVowel)) {
indexes.push_back(rightVowel);
}
continue;
}
const size_t clusterStart = leftVowel + 1;
const size_t clusterEnd = rightVowel;
const size_t onsetLen = englishOnsetLength(cps, clusterStart, clusterEnd);
size_t breakIndex = clusterEnd - onsetLen;
if (breakIndex < MIN_PREFIX_CP || cps.size() - breakIndex < MIN_SUFFIX_CP) {
continue;
}
if (nextToApostrophe(cps, breakIndex)) {
continue;
}
indexes.push_back(breakIndex);
}
std::sort(indexes.begin(), indexes.end());
indexes.erase(std::unique(indexes.begin(), indexes.end()), indexes.end());
return indexes;
}
} // namespace
const EnglishHyphenator& EnglishHyphenator::instance() {
static EnglishHyphenator instance;
return instance;
}
Script EnglishHyphenator::script() const { return Script::Latin; }
std::vector<size_t> EnglishHyphenator::breakIndexes(const std::vector<CodepointInfo>& cps) const {
return englishBreakIndexes(cps);
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "LanguageHyphenator.h"
class EnglishHyphenator final : public LanguageHyphenator {
public:
static const EnglishHyphenator& instance();
Script script() const override;
std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const override;
private:
EnglishHyphenator() = default;
};

View File

@ -0,0 +1,82 @@
#include "HyphenationCommon.h"
namespace {
uint32_t toLowerLatinImpl(const uint32_t cp) {
if (cp >= 'A' && cp <= 'Z') {
return cp - 'A' + 'a';
}
return cp;
}
uint32_t toLowerCyrillicImpl(const uint32_t cp) {
if (cp >= 0x0410 && cp <= 0x042F) {
return cp + 0x20;
}
if (cp == 0x0401) {
return 0x0451;
}
return cp;
}
} // namespace
uint32_t toLowerLatin(const uint32_t cp) { return toLowerLatinImpl(cp); }
uint32_t toLowerCyrillic(const uint32_t cp) { return toLowerCyrillicImpl(cp); }
bool isLatinLetter(const uint32_t cp) { return (cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z'); }
bool isLatinVowel(uint32_t cp) {
cp = toLowerLatinImpl(cp);
return cp == 'a' || cp == 'e' || cp == 'i' || cp == 'o' || cp == 'u' || cp == 'y';
}
bool isLatinConsonant(const uint32_t cp) { return isLatinLetter(cp) && !isLatinVowel(cp); }
bool isCyrillicLetter(const uint32_t cp) { return (cp >= 0x0400 && cp <= 0x052F); }
bool isCyrillicVowel(uint32_t cp) {
cp = toLowerCyrillicImpl(cp);
switch (cp) {
case 0x0430: // а
case 0x0435: // е
case 0x0451: // ё
case 0x0438: // и
case 0x043E: // о
case 0x0443: // у
case 0x044B: // ы
case 0x044D: // э
case 0x044E: // ю
case 0x044F: // я
return true;
default:
return false;
}
}
bool isCyrillicConsonant(const uint32_t cp) { return isCyrillicLetter(cp) && !isCyrillicVowel(cp); }
bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLetter(cp); }
bool isVowel(const uint32_t cp) { return isLatinVowel(cp) || isCyrillicVowel(cp); }
Script detectScript(const std::vector<CodepointInfo>& cps) {
bool hasLatin = false;
bool hasCyrillic = false;
for (const auto& info : cps) {
if (isLatinLetter(info.value)) {
hasLatin = true;
} else if (isCyrillicLetter(info.value)) {
hasCyrillic = true;
}
}
if (hasLatin && !hasCyrillic) {
return Script::Latin;
}
if (!hasLatin && hasCyrillic) {
return Script::Cyrillic;
}
return Script::Mixed;
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
struct CodepointInfo {
uint32_t value;
size_t byteOffset;
};
enum class Script { Latin, Cyrillic, Mixed };
constexpr size_t MIN_PREFIX_CP = 3;
constexpr size_t MIN_SUFFIX_CP = 2;
uint32_t toLowerLatin(uint32_t cp);
uint32_t toLowerCyrillic(uint32_t cp);
bool isLatinLetter(uint32_t cp);
bool isLatinVowel(uint32_t cp);
bool isLatinConsonant(uint32_t cp);
bool isCyrillicLetter(uint32_t cp);
bool isCyrillicVowel(uint32_t cp);
bool isCyrillicConsonant(uint32_t cp);
bool isAlphabetic(uint32_t cp);
bool isVowel(uint32_t cp);
Script detectScript(const std::vector<CodepointInfo>& cps);

View File

@ -0,0 +1,158 @@
#include "Hyphenator.h"
#include <GfxRenderer.h>
#include <Utf8.h>
#include <algorithm>
#include <array>
#include <limits>
#include <vector>
#include "EnglishHyphenator.h"
#include "HyphenationCommon.h"
#include "LanguageHyphenator.h"
#include "RussianHyphenator.h"
namespace {
const std::array<const LanguageHyphenator*, 2>& registeredHyphenators() {
static const std::array<const LanguageHyphenator*, 2> hyphenators = {
&EnglishHyphenator::instance(),
&RussianHyphenator::instance(),
};
return hyphenators;
}
const LanguageHyphenator* hyphenatorForScript(const Script script) {
for (const auto* hyphenator : registeredHyphenators()) {
if (hyphenator->script() == script) {
return hyphenator;
}
}
return nullptr;
}
std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
std::vector<CodepointInfo> cps;
cps.reserve(word.size());
const unsigned char* base = reinterpret_cast<const unsigned char*>(word.c_str());
const unsigned char* ptr = base;
while (*ptr != 0) {
const unsigned char* current = ptr;
const uint32_t cp = utf8NextCodepoint(&ptr);
cps.push_back({cp, static_cast<size_t>(current - base)});
}
return cps;
}
bool hasOnlyAlphabetic(const std::vector<CodepointInfo>& cps) {
if (cps.empty()) {
return false;
}
for (const auto& info : cps) {
if (!isAlphabetic(info.value)) {
return false;
}
}
return true;
}
std::vector<size_t> collectBreakIndexes(const std::vector<CodepointInfo>& cps) {
if (cps.size() < MIN_PREFIX_CP + MIN_SUFFIX_CP) {
return {};
}
const Script script = detectScript(cps);
if (const auto* hyphenator = hyphenatorForScript(script)) {
auto indexes = hyphenator->breakIndexes(cps);
return indexes;
}
return {};
}
size_t byteOffsetForIndex(const std::vector<CodepointInfo>& cps, const size_t index) {
if (index >= cps.size()) {
return cps.empty() ? 0 : cps.back().byteOffset;
}
return cps[index].byteOffset;
}
std::string slice(const std::string& word, const size_t startByte, const size_t endByte) {
if (startByte >= endByte || startByte >= word.size()) {
return std::string();
}
const size_t boundedEnd = std::min(endByte, word.size());
return word.substr(startByte, boundedEnd - startByte);
}
} // namespace
bool Hyphenator::splitWord(const GfxRenderer& renderer, const int fontId, const std::string& word,
const EpdFontStyle style, const int availableWidth, HyphenationResult* result,
const bool force) {
if (!result || word.empty()) {
return false;
}
auto cps = collectCodepoints(word);
if (cps.size() < MIN_PREFIX_CP + MIN_SUFFIX_CP) {
return false;
}
if (!force && !hasOnlyAlphabetic(cps)) {
return false;
}
const auto breakIndexes = collectBreakIndexes(cps);
const int hyphenWidth = renderer.getTextWidth(fontId, "-", style);
const int adjustedWidth = availableWidth - hyphenWidth;
size_t chosenIndex = std::numeric_limits<size_t>::max();
if (adjustedWidth > 0) {
for (const size_t idx : breakIndexes) {
const size_t byteOffset = byteOffsetForIndex(cps, idx);
const std::string prefix = word.substr(0, byteOffset);
const int prefixWidth = renderer.getTextWidth(fontId, prefix.c_str(), style);
if (prefixWidth <= adjustedWidth) {
chosenIndex = idx;
} else {
break;
}
}
}
if (chosenIndex == std::numeric_limits<size_t>::max() && force) {
for (size_t idx = MIN_PREFIX_CP; idx + MIN_SUFFIX_CP <= cps.size(); ++idx) {
const size_t byteOffset = byteOffsetForIndex(cps, idx);
const std::string prefix = word.substr(0, byteOffset);
const int prefixWidth = renderer.getTextWidth(fontId, prefix.c_str(), style);
if (adjustedWidth <= 0 || prefixWidth <= adjustedWidth) {
chosenIndex = idx;
if (adjustedWidth > 0 && prefixWidth > adjustedWidth) {
break;
}
}
}
}
if (chosenIndex == std::numeric_limits<size_t>::max()) {
return false;
}
const size_t splitByte = byteOffsetForIndex(cps, chosenIndex);
const std::string head = word.substr(0, splitByte);
const std::string tail = slice(word, splitByte, word.size());
if (head.empty() || tail.empty()) {
return false;
}
result->head = head + "-";
result->tail = tail;
return true;
}

View File

@ -0,0 +1,18 @@
#pragma once
#include <EpdFontFamily.h>
#include <string>
class GfxRenderer;
struct HyphenationResult {
std::string head;
std::string tail;
};
class Hyphenator {
public:
static bool splitWord(const GfxRenderer& renderer, int fontId, const std::string& word, EpdFontStyle style,
int availableWidth, HyphenationResult* result, bool force);
};

View File

@ -0,0 +1,12 @@
#pragma once
#include <vector>
#include "HyphenationCommon.h"
class LanguageHyphenator {
public:
virtual ~LanguageHyphenator() = default;
virtual Script script() const = 0;
virtual std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const = 0;
};

View File

@ -0,0 +1,199 @@
#include "RussianHyphenator.h"
#include <algorithm>
#include <vector>
namespace {
bool isSoftOrHardSign(const uint32_t cp) { return cp == 0x044C || cp == 0x042C || cp == 0x044A || cp == 0x042A; }
bool isRussianPrefixConsonant(uint32_t cp) {
cp = toLowerCyrillic(cp);
return cp == 0x0432 || cp == 0x0437 || cp == 0x0441; // в, з, с
}
bool isRussianSibilant(uint32_t cp) {
cp = toLowerCyrillic(cp);
switch (cp) {
case 0x0437: // з
case 0x0441: // с
case 0x0436: // ж
case 0x0448: // ш
case 0x0449: // щ
case 0x0447: // ч
case 0x0446: // ц
return true;
default:
return false;
}
}
bool isRussianStop(uint32_t cp) {
cp = toLowerCyrillic(cp);
switch (cp) {
case 0x0431: // б
case 0x0433: // г
case 0x0434: // д
case 0x043F: // п
case 0x0442: // т
case 0x043A: // к
return true;
default:
return false;
}
}
int russianSonority(uint32_t cp) {
cp = toLowerCyrillic(cp);
switch (cp) {
case 0x043B: // л
case 0x0440: // р
case 0x0439: // й
return 4;
case 0x043C: // м
case 0x043D: // н
return 3;
case 0x0432: // в
case 0x0437: // з
case 0x0436: // ж
return 2;
case 0x0444: // ф
case 0x0441: // с
case 0x0448: // ш
case 0x0449: // щ
case 0x0447: // ч
case 0x0446: // ц
case 0x0445: // х
return 1;
case 0x0431: // б
case 0x0433: // г
case 0x0434: // д
case 0x043F: // п
case 0x0442: // т
case 0x043A: // к
return 0;
default:
return 1;
}
}
bool russianClusterIsValidOnset(const std::vector<CodepointInfo>& cps, const size_t start, const size_t end) {
if (start >= end) {
return false;
}
for (size_t i = start; i < end; ++i) {
const auto cp = cps[i].value;
if (!isCyrillicConsonant(cp) || isSoftOrHardSign(cp)) {
return false;
}
}
if (end - start == 1) {
return true;
}
for (size_t i = start; i + 1 < end; ++i) {
const uint32_t current = cps[i].value;
const uint32_t next = cps[i + 1].value;
const int currentRank = russianSonority(current);
const int nextRank = russianSonority(next);
if (currentRank > nextRank) {
const bool atClusterStart = (i == start);
const bool prefixAllowance = atClusterStart && isRussianPrefixConsonant(current);
const bool sibilantAllowance = isRussianSibilant(current) && isRussianStop(next);
if (!prefixAllowance && !sibilantAllowance) {
return false;
}
}
}
return true;
}
size_t russianOnsetLength(const std::vector<CodepointInfo>& cps, const size_t clusterStart, const size_t clusterEnd) {
const size_t clusterLen = clusterEnd - clusterStart;
if (clusterLen == 0) {
return 0;
}
const size_t maxLen = std::min<size_t>(4, clusterLen);
for (size_t len = maxLen; len >= 1; --len) {
const size_t suffixStart = clusterEnd - len;
if (russianClusterIsValidOnset(cps, suffixStart, clusterEnd)) {
return len;
}
}
return 1;
}
bool nextToSoftSign(const std::vector<CodepointInfo>& cps, const size_t index) {
if (index == 0 || index >= cps.size()) {
return false;
}
const auto left = cps[index - 1].value;
const auto right = cps[index].value;
return isSoftOrHardSign(left) || isSoftOrHardSign(right);
}
std::vector<size_t> russianBreakIndexes(const std::vector<CodepointInfo>& cps) {
std::vector<size_t> indexes;
if (cps.size() < MIN_PREFIX_CP + MIN_SUFFIX_CP) {
return indexes;
}
std::vector<size_t> vowelPositions;
vowelPositions.reserve(cps.size());
for (size_t i = 0; i < cps.size(); ++i) {
if (isCyrillicVowel(cps[i].value)) {
vowelPositions.push_back(i);
}
}
if (vowelPositions.size() < 2) {
return indexes;
}
for (size_t v = 0; v + 1 < vowelPositions.size(); ++v) {
const size_t leftVowel = vowelPositions[v];
const size_t rightVowel = vowelPositions[v + 1];
if (rightVowel - leftVowel == 1) {
if (rightVowel >= MIN_PREFIX_CP && cps.size() - rightVowel >= MIN_SUFFIX_CP && !nextToSoftSign(cps, rightVowel)) {
indexes.push_back(rightVowel);
}
continue;
}
const size_t clusterStart = leftVowel + 1;
const size_t clusterEnd = rightVowel;
const size_t onsetLen = russianOnsetLength(cps, clusterStart, clusterEnd);
size_t breakIndex = clusterEnd - onsetLen;
if (breakIndex < MIN_PREFIX_CP || cps.size() - breakIndex < MIN_SUFFIX_CP) {
continue;
}
if (nextToSoftSign(cps, breakIndex)) {
continue;
}
indexes.push_back(breakIndex);
}
std::sort(indexes.begin(), indexes.end());
indexes.erase(std::unique(indexes.begin(), indexes.end()), indexes.end());
return indexes;
}
} // namespace
const RussianHyphenator& RussianHyphenator::instance() {
static RussianHyphenator instance;
return instance;
}
Script RussianHyphenator::script() const { return Script::Cyrillic; }
std::vector<size_t> RussianHyphenator::breakIndexes(const std::vector<CodepointInfo>& cps) const {
return russianBreakIndexes(cps);
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "LanguageHyphenator.h"
class RussianHyphenator final : public LanguageHyphenator {
public:
static const RussianHyphenator& instance();
Script script() const override;
std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const override;
private:
RussianHyphenator() = default;
};