Add special handling for apostrophe handling
This commit is contained in:
@@ -107,6 +107,17 @@ bool isPunctuation(const uint32_t cp) {
|
|||||||
|
|
||||||
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
||||||
|
|
||||||
|
bool isApostrophe(const uint32_t cp) {
|
||||||
|
switch (cp) {
|
||||||
|
case '\'':
|
||||||
|
case 0x2018: // left single quotation mark
|
||||||
|
case 0x2019: // right single quotation mark
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool isExplicitHyphen(const uint32_t cp) {
|
bool isExplicitHyphen(const uint32_t cp) {
|
||||||
switch (cp) {
|
switch (cp) {
|
||||||
case '-':
|
case '-':
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ bool isCyrillicLetter(uint32_t cp);
|
|||||||
bool isAlphabetic(uint32_t cp);
|
bool isAlphabetic(uint32_t cp);
|
||||||
bool isPunctuation(uint32_t cp);
|
bool isPunctuation(uint32_t cp);
|
||||||
bool isAsciiDigit(uint32_t cp);
|
bool isAsciiDigit(uint32_t cp);
|
||||||
|
bool isApostrophe(uint32_t cp);
|
||||||
bool isExplicitHyphen(uint32_t cp);
|
bool isExplicitHyphen(uint32_t cp);
|
||||||
bool isSoftHyphen(uint32_t cp);
|
bool isSoftHyphen(uint32_t cp);
|
||||||
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
|
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
|
||||||
|
|||||||
@@ -59,6 +59,83 @@ std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<Cod
|
|||||||
return breaks;
|
return breaks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSegmentSeparator(const uint32_t cp) { return isExplicitHyphen(cp) || isApostrophe(cp); }
|
||||||
|
|
||||||
|
void appendSegmentPatternBreaks(const std::vector<CodepointInfo>& cps, const LanguageHyphenator& hyphenator,
|
||||||
|
const bool includeFallback, std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||||
|
size_t segStart = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= cps.size(); ++i) {
|
||||||
|
const bool atEnd = i == cps.size();
|
||||||
|
const bool atSeparator = !atEnd && isSegmentSeparator(cps[i].value);
|
||||||
|
if (!atEnd && !atSeparator) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > segStart) {
|
||||||
|
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
|
||||||
|
auto segIndexes = hyphenator.breakIndexes(segment);
|
||||||
|
|
||||||
|
if (includeFallback && segIndexes.empty()) {
|
||||||
|
const size_t minPrefix = hyphenator.minPrefix();
|
||||||
|
const size_t minSuffix = hyphenator.minSuffix();
|
||||||
|
for (size_t idx = minPrefix; idx + minSuffix <= segment.size(); ++idx) {
|
||||||
|
segIndexes.push_back(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const size_t idx : segIndexes) {
|
||||||
|
const size_t cpIdx = segStart + idx;
|
||||||
|
if (cpIdx < cps.size()) {
|
||||||
|
outBreaks.push_back({cps[cpIdx].byteOffset, true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void appendApostropheContractionBreaks(const std::vector<CodepointInfo>& cps,
|
||||||
|
std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||||
|
size_t segmentStart = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < cps.size(); ++i) {
|
||||||
|
if (isSegmentSeparator(cps[i].value)) {
|
||||||
|
if (isApostrophe(cps[i].value) && i > 0 && i + 1 < cps.size() && isAlphabetic(cps[i - 1].value) &&
|
||||||
|
isAlphabetic(cps[i + 1].value)) {
|
||||||
|
size_t leftPrefixLen = 0;
|
||||||
|
for (size_t j = segmentStart; j < i; ++j) {
|
||||||
|
if (isAlphabetic(cps[j].value)) {
|
||||||
|
++leftPrefixLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid stranding very short clitics like "l'" or "d'" at line end.
|
||||||
|
if (leftPrefixLen >= 3) {
|
||||||
|
outBreaks.push_back({cps[i + 1].byteOffset, false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segmentStart = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sortAndDedupeBreakInfos(std::vector<Hyphenator::BreakInfo>& infos) {
|
||||||
|
std::sort(infos.begin(), infos.end(), [](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||||
|
if (a.byteOffset != b.byteOffset) {
|
||||||
|
return a.byteOffset < b.byteOffset;
|
||||||
|
}
|
||||||
|
return a.requiresInsertedHyphen < b.requiresInsertedHyphen;
|
||||||
|
});
|
||||||
|
|
||||||
|
infos.erase(std::unique(infos.begin(), infos.end(),
|
||||||
|
[](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||||
|
return a.byteOffset == b.byteOffset && a.requiresInsertedHyphen == b.requiresInsertedHyphen;
|
||||||
|
}),
|
||||||
|
infos.end());
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
|
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
|
||||||
@@ -89,31 +166,34 @@ std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& w
|
|||||||
// @16 Satellitensys|tems (+hyphen)
|
// @16 Satellitensys|tems (+hyphen)
|
||||||
// Result: 6 sorted break points; the line-breaker picks the widest prefix that fits.
|
// Result: 6 sorted break points; the line-breaker picks the widest prefix that fits.
|
||||||
if (hyphenator) {
|
if (hyphenator) {
|
||||||
size_t segStart = 0;
|
appendSegmentPatternBreaks(cps, *hyphenator, /*includeFallback=*/false, explicitBreakInfos);
|
||||||
for (size_t i = 0; i <= cps.size(); ++i) {
|
|
||||||
const bool atEnd = (i == cps.size());
|
|
||||||
const bool atHyphen = !atEnd && isExplicitHyphen(cps[i].value);
|
|
||||||
if (atEnd || atHyphen) {
|
|
||||||
if (i > segStart) {
|
|
||||||
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
|
|
||||||
auto segIndexes = hyphenator->breakIndexes(segment);
|
|
||||||
for (const size_t idx : segIndexes) {
|
|
||||||
const size_t cpIdx = segStart + idx;
|
|
||||||
if (cpIdx < cps.size()) {
|
|
||||||
explicitBreakInfos.push_back({cps[cpIdx].byteOffset, true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segStart = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Merge explicit and pattern breaks into ascending byte-offset order.
|
// Merge explicit and pattern breaks into ascending byte-offset order.
|
||||||
std::sort(explicitBreakInfos.begin(), explicitBreakInfos.end(),
|
sortAndDedupeBreakInfos(explicitBreakInfos);
|
||||||
[](const BreakInfo& a, const BreakInfo& b) { return a.byteOffset < b.byteOffset; });
|
|
||||||
}
|
}
|
||||||
return explicitBreakInfos;
|
return explicitBreakInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apostrophe-like separators split compounds into alphabetic segments; run Liang on each segment.
|
||||||
|
// This allows words like "all'improvviso" to hyphenate within "improvviso" instead of becoming
|
||||||
|
// completely unsplittable due to the apostrophe punctuation.
|
||||||
|
bool hasApostropheLikeSeparator = false;
|
||||||
|
for (const auto& cp : cps) {
|
||||||
|
if (isApostrophe(cp.value)) {
|
||||||
|
hasApostropheLikeSeparator = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hyphenator && hasApostropheLikeSeparator) {
|
||||||
|
std::vector<BreakInfo> segmentedBreaks;
|
||||||
|
appendSegmentPatternBreaks(cps, *hyphenator, includeFallback, segmentedBreaks);
|
||||||
|
appendApostropheContractionBreaks(cps, segmentedBreaks);
|
||||||
|
if (!segmentedBreaks.empty()) {
|
||||||
|
sortAndDedupeBreakInfos(segmentedBreaks);
|
||||||
|
return segmentedBreaks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ask language hyphenator for legal break points.
|
// Ask language hyphenator for legal break points.
|
||||||
std::vector<size_t> indexes;
|
std::vector<size_t> indexes;
|
||||||
if (hyphenator) {
|
if (hyphenator) {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class Hyphenator {
|
|||||||
struct BreakInfo {
|
struct BreakInfo {
|
||||||
size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur.
|
size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur.
|
||||||
bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks).
|
bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks).
|
||||||
// false = the word already contains a hyphen at this position (explicit '-').
|
// false = break occurs at an existing visible separator boundary
|
||||||
|
// (explicit '-' or eligible apostrophe contraction boundary).
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns byte offsets where the word may be hyphenated.
|
// Returns byte offsets where the word may be hyphenated.
|
||||||
@@ -19,12 +20,16 @@ class Hyphenator {
|
|||||||
// Break sources (in priority order):
|
// Break sources (in priority order):
|
||||||
// 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD).
|
// 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD).
|
||||||
// When found, language patterns are additionally run on each alphabetic segment
|
// When found, language patterns are additionally run on each alphabetic segment
|
||||||
// between hyphens so compound words can break within their parts.
|
// between separators so compound words can break within their parts.
|
||||||
// Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen)
|
// Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen)
|
||||||
// plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems).
|
// plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems).
|
||||||
// 2. Language-specific Liang patterns (e.g. German de_patterns).
|
// 2. Apostrophe contractions between letters (e.g. all'improvviso).
|
||||||
|
// Liang patterns are run per alphabetic segment around apostrophes.
|
||||||
|
// A direct break at the apostrophe boundary is allowed only when the left
|
||||||
|
// segment has at least 3 letters to avoid stranding short clitics (e.g. l', d').
|
||||||
|
// 3. Language-specific Liang patterns (e.g. German de_patterns).
|
||||||
// Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter.
|
// Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter.
|
||||||
// 3. Fallback every-N-chars splitting (only when includeFallback is true AND no
|
// 4. Fallback every-N-chars splitting (only when includeFallback is true AND no
|
||||||
// pattern breaks were found). Used as a last resort to prevent a single oversized
|
// pattern breaks were found). Used as a last resort to prevent a single oversized
|
||||||
// word from overflowing the page width.
|
// word from overflowing the page width.
|
||||||
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
|
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
|
||||||
|
|||||||
Reference in New Issue
Block a user