summaryrefslogtreecommitdiffstats
path: root/layout/generic/nsTextRunTransformations.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/generic/nsTextRunTransformations.cpp896
1 files changed, 896 insertions, 0 deletions
diff --git a/layout/generic/nsTextRunTransformations.cpp b/layout/generic/nsTextRunTransformations.cpp
new file mode 100644
index 0000000000..5dd85fe61f
--- /dev/null
+++ b/layout/generic/nsTextRunTransformations.cpp
@@ -0,0 +1,896 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsTextRunTransformations.h"
+
+#include <utility>
+
+#include "GreekCasing.h"
+#include "IrishCasing.h"
+#include "mozilla/ComputedStyleInlines.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/TextEditor.h"
+#include "mozilla/gfx/2D.h"
+#include "nsGkAtoms.h"
+#include "nsSpecialCasingData.h"
+#include "nsStyleConsts.h"
+#include "nsTextFrameUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsUnicodeProperties.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+// Unicode characters needing special casing treatment in tr/az languages
+#define LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE 0x0130
+#define LATIN_SMALL_LETTER_DOTLESS_I 0x0131
+
+// Greek sigma needs custom handling for the lowercase transform; for details
+// see bug 740120.
+#define GREEK_CAPITAL_LETTER_SIGMA 0x03A3
+#define GREEK_SMALL_LETTER_FINAL_SIGMA 0x03C2
+#define GREEK_SMALL_LETTER_SIGMA 0x03C3
+
+already_AddRefed<nsTransformedTextRun> nsTransformedTextRun::Create(
+ const gfxTextRunFactory::Parameters* aParams,
+ nsTransformingTextRunFactory* aFactory, gfxFontGroup* aFontGroup,
+ const char16_t* aString, uint32_t aLength,
+ const gfx::ShapedTextFlags aFlags, const nsTextFrameUtils::Flags aFlags2,
+ nsTArray<RefPtr<nsTransformedCharStyle>>&& aStyles, bool aOwnsFactory) {
+ NS_ASSERTION(!(aFlags & gfx::ShapedTextFlags::TEXT_IS_8BIT),
+ "didn't expect text to be marked as 8-bit here");
+
+ void* storage =
+ AllocateStorageForTextRun(sizeof(nsTransformedTextRun), aLength);
+ if (!storage) {
+ return nullptr;
+ }
+
+ RefPtr<nsTransformedTextRun> result = new (storage)
+ nsTransformedTextRun(aParams, aFactory, aFontGroup, aString, aLength,
+ aFlags, aFlags2, std::move(aStyles), aOwnsFactory);
+ return result.forget();
+}
+
+void nsTransformedTextRun::SetCapitalization(uint32_t aStart, uint32_t aLength,
+ bool* aCapitalization) {
+ if (mCapitalize.IsEmpty()) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mCapitalize.AppendElements(GetLength());
+ memset(mCapitalize.Elements(), 0, GetLength() * sizeof(bool));
+ }
+ memcpy(mCapitalize.Elements() + aStart, aCapitalization,
+ aLength * sizeof(bool));
+ mNeedsRebuild = true;
+}
+
+bool nsTransformedTextRun::SetPotentialLineBreaks(Range aRange,
+ const uint8_t* aBreakBefore) {
+ bool changed = gfxTextRun::SetPotentialLineBreaks(aRange, aBreakBefore);
+ if (changed) {
+ mNeedsRebuild = true;
+ }
+ return changed;
+}
+
+size_t nsTransformedTextRun::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t total = gfxTextRun::SizeOfExcludingThis(aMallocSizeOf);
+ total += mStyles.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ total += mCapitalize.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ if (mOwnsFactory) {
+ total += aMallocSizeOf(mFactory);
+ }
+ return total;
+}
+
+size_t nsTransformedTextRun::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+already_AddRefed<nsTransformedTextRun>
+nsTransformingTextRunFactory::MakeTextRun(
+ const char16_t* aString, uint32_t aLength,
+ const gfxTextRunFactory::Parameters* aParams, gfxFontGroup* aFontGroup,
+ gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2,
+ nsTArray<RefPtr<nsTransformedCharStyle>>&& aStyles, bool aOwnsFactory) {
+ return nsTransformedTextRun::Create(aParams, this, aFontGroup, aString,
+ aLength, aFlags, aFlags2,
+ std::move(aStyles), aOwnsFactory);
+}
+
+already_AddRefed<nsTransformedTextRun>
+nsTransformingTextRunFactory::MakeTextRun(
+ const uint8_t* aString, uint32_t aLength,
+ const gfxTextRunFactory::Parameters* aParams, gfxFontGroup* aFontGroup,
+ gfx::ShapedTextFlags aFlags, nsTextFrameUtils::Flags aFlags2,
+ nsTArray<RefPtr<nsTransformedCharStyle>>&& aStyles, bool aOwnsFactory) {
+ // We'll only have a Unicode code path to minimize the amount of code needed
+ // for these rarely used features
+ NS_ConvertASCIItoUTF16 unicodeString(reinterpret_cast<const char*>(aString),
+ aLength);
+ return MakeTextRun(unicodeString.get(), aLength, aParams, aFontGroup,
+ aFlags & ~gfx::ShapedTextFlags::TEXT_IS_8BIT, aFlags2,
+ std::move(aStyles), aOwnsFactory);
+}
+
+void MergeCharactersInTextRun(gfxTextRun* aDest, gfxTextRun* aSrc,
+ const bool* aCharsToMerge,
+ const bool* aDeletedChars) {
+ MOZ_ASSERT(!aDest->TrailingGlyphRun(), "unexpected glyphRuns in aDest!");
+ uint32_t offset = 0;
+ AutoTArray<gfxTextRun::DetailedGlyph, 2> glyphs;
+ const gfxTextRun::CompressedGlyph continuationGlyph =
+ gfxTextRun::CompressedGlyph::MakeComplex(false, false);
+ const gfxTextRun::CompressedGlyph* srcGlyphs = aSrc->GetCharacterGlyphs();
+ gfxTextRun::CompressedGlyph* destGlyphs = aDest->GetCharacterGlyphs();
+ for (gfxTextRun::GlyphRunIterator iter(aSrc, gfxTextRun::Range(aSrc));
+ !iter.AtEnd(); iter.NextRun()) {
+ const gfxTextRun::GlyphRun* run = iter.GlyphRun();
+ aDest->AddGlyphRun(run->mFont, run->mMatchType, offset, false,
+ run->mOrientation, run->mIsCJK);
+
+ bool anyMissing = false;
+ uint32_t mergeRunStart = iter.StringStart();
+ // Initialize to a copy of the first source glyph in the merge run.
+ gfxTextRun::CompressedGlyph mergedGlyph = srcGlyphs[mergeRunStart];
+ uint32_t stringEnd = iter.StringEnd();
+ for (uint32_t k = iter.StringStart(); k < stringEnd; ++k) {
+ const gfxTextRun::CompressedGlyph g = srcGlyphs[k];
+ if (g.IsSimpleGlyph()) {
+ if (!anyMissing) {
+ gfxTextRun::DetailedGlyph details;
+ details.mGlyphID = g.GetSimpleGlyph();
+ details.mAdvance = g.GetSimpleAdvance();
+ glyphs.AppendElement(details);
+ }
+ } else {
+ if (g.IsMissing()) {
+ anyMissing = true;
+ glyphs.Clear();
+ }
+ if (g.GetGlyphCount() > 0) {
+ glyphs.AppendElements(aSrc->GetDetailedGlyphs(k), g.GetGlyphCount());
+ }
+ }
+
+ if (k + 1 < iter.StringEnd() && aCharsToMerge[k + 1]) {
+ // next char is supposed to merge with current, so loop without
+ // writing current merged glyph to the destination
+ continue;
+ }
+
+ // If the start of the merge run is actually a character that should
+ // have been merged with the previous character (this can happen
+ // if there's a font change in the middle of a case-mapped character,
+ // that decomposed into a sequence of base+diacritics, for example),
+ // just discard the entire merge run. See comment at start of this
+ // function.
+ NS_WARNING_ASSERTION(
+ !aCharsToMerge[mergeRunStart],
+ "unable to merge across a glyph run boundary, glyph(s) discarded");
+ if (!aCharsToMerge[mergeRunStart]) {
+ // Determine if we can just copy the existing simple glyph record.
+ if (mergedGlyph.IsSimpleGlyph() && glyphs.Length() == 1) {
+ destGlyphs[offset] = mergedGlyph;
+ } else {
+ // Otherwise set up complex glyph record and store detailed glyphs.
+ mergedGlyph.SetComplex(mergedGlyph.IsClusterStart(),
+ mergedGlyph.IsLigatureGroupStart());
+ destGlyphs[offset] = mergedGlyph;
+ aDest->SetDetailedGlyphs(offset, glyphs.Length(), glyphs.Elements());
+ if (anyMissing) {
+ destGlyphs[offset].SetMissing();
+ }
+ }
+ offset++;
+
+ while (offset < aDest->GetLength() && aDeletedChars[offset]) {
+ destGlyphs[offset++] = continuationGlyph;
+ }
+ }
+
+ glyphs.Clear();
+ anyMissing = false;
+ mergeRunStart = k + 1;
+ if (mergeRunStart < stringEnd) {
+ mergedGlyph = srcGlyphs[mergeRunStart];
+ }
+ }
+ NS_ASSERTION(glyphs.Length() == 0,
+ "Leftover glyphs, don't request merging of the last character "
+ "with its next!");
+ }
+ NS_ASSERTION(offset == aDest->GetLength(), "Bad offset calculations");
+}
+
+gfxTextRunFactory::Parameters GetParametersForInner(
+ nsTransformedTextRun* aTextRun, gfx::ShapedTextFlags* aFlags,
+ DrawTarget* aRefDrawTarget) {
+ gfxTextRunFactory::Parameters params = {
+ aRefDrawTarget, nullptr, nullptr,
+ nullptr, 0, aTextRun->GetAppUnitsPerDevUnit()};
+ *aFlags = aTextRun->GetFlags();
+ return params;
+}
+
+// Some languages have special casing conventions that differ from the
+// default Unicode mappings.
+// The enum values here are named for well-known exemplar languages that
+// exhibit the behavior in question; multiple lang tags may map to the
+// same setting here, if the behavior is shared by other languages.
+enum LanguageSpecificCasingBehavior {
+ eLSCB_None, // default non-lang-specific behavior
+ eLSCB_Dutch, // treat "ij" digraph as a unit for capitalization
+ eLSCB_Greek, // strip accent when uppercasing Greek vowels
+ eLSCB_Irish, // keep prefix letters as lowercase when uppercasing Irish
+ eLSCB_Turkish, // preserve dotted/dotless-i distinction in uppercase
+ eLSCB_Lithuanian // retain dot on lowercase i/j when an accent is present
+};
+
+static LanguageSpecificCasingBehavior GetCasingFor(const nsAtom* aLang) {
+ if (!aLang) {
+ return eLSCB_None;
+ }
+ if (aLang == nsGkAtoms::tr || aLang == nsGkAtoms::az ||
+ aLang == nsGkAtoms::ba || aLang == nsGkAtoms::crh ||
+ aLang == nsGkAtoms::tt) {
+ return eLSCB_Turkish;
+ }
+ if (aLang == nsGkAtoms::nl) {
+ return eLSCB_Dutch;
+ }
+ if (aLang == nsGkAtoms::el) {
+ return eLSCB_Greek;
+ }
+ if (aLang == nsGkAtoms::ga) {
+ return eLSCB_Irish;
+ }
+ if (aLang == nsGkAtoms::lt_) {
+ return eLSCB_Lithuanian;
+ }
+
+ // Is there a region subtag we should ignore?
+ nsAtomString langStr(const_cast<nsAtom*>(aLang));
+ int index = langStr.FindChar('-');
+ if (index > 0) {
+ langStr.Truncate(index);
+ RefPtr<nsAtom> truncatedLang = NS_Atomize(langStr);
+ return GetCasingFor(truncatedLang);
+ }
+
+ return eLSCB_None;
+}
+
+bool nsCaseTransformTextRunFactory::TransformString(
+ const nsAString& aString, nsString& aConvertedString,
+ const Maybe<StyleTextTransform>& aGlobalTransform, char16_t aMaskChar,
+ bool aCaseTransformsOnly, const nsAtom* aLanguage,
+ nsTArray<bool>& aCharsToMergeArray, nsTArray<bool>& aDeletedCharsArray,
+ const nsTransformedTextRun* aTextRun, uint32_t aOffsetInTextRun,
+ nsTArray<uint8_t>* aCanBreakBeforeArray,
+ nsTArray<RefPtr<nsTransformedCharStyle>>* aStyleArray) {
+ bool auxiliaryOutputArrays = aCanBreakBeforeArray && aStyleArray;
+ MOZ_ASSERT(!auxiliaryOutputArrays || aTextRun,
+ "text run must be provided to use aux output arrays");
+
+ uint32_t length = aString.Length();
+ const char16_t* str = aString.BeginReading();
+ // If an unconditional mask character was passed, we'll use it; if not, any
+ // masking called for by the textrun styles will use TextEditor's mask char.
+ const char16_t mask = aMaskChar ? aMaskChar : TextEditor::PasswordMask();
+
+ bool mergeNeeded = false;
+
+ bool capitalizeDutchIJ = false;
+ bool prevIsLetter = false;
+ bool ntPrefix = false; // true immediately after a word-initial 'n' or 't'
+ // when doing Irish lowercasing
+ bool seenSoftDotted = false; // true immediately after an I or J that is
+ // converted to lowercase in Lithuanian mode
+ uint32_t sigmaIndex = uint32_t(-1);
+ nsUGenCategory cat;
+
+ StyleTextTransform style =
+ aGlobalTransform.valueOr(StyleTextTransform::None());
+ bool forceNonFullWidth = false;
+ const nsAtom* lang = aLanguage;
+
+ LanguageSpecificCasingBehavior languageSpecificCasing = GetCasingFor(lang);
+ mozilla::GreekCasing::State greekState;
+ mozilla::IrishCasing::State irishState;
+ uint32_t irishMark = uint32_t(-1); // location of possible prefix letter(s)
+ // in the output string
+ uint32_t irishMarkSrc = uint32_t(-1); // corresponding location in source
+ // string (may differ from output due
+ // to expansions like eszet -> 'SS')
+ uint32_t greekMark = uint32_t(-1); // location of uppercase ETA that may need
+ // tonos added (if it is disjunctive eta)
+ const char16_t kGreekUpperEta = 0x0397;
+
+ for (uint32_t i = 0; i < length; ++i, ++aOffsetInTextRun) {
+ uint32_t ch = str[i];
+
+ RefPtr<nsTransformedCharStyle> charStyle;
+ if (aTextRun) {
+ charStyle = aTextRun->mStyles[aOffsetInTextRun];
+ style = aGlobalTransform.valueOr(charStyle->mTextTransform);
+ forceNonFullWidth = charStyle->mForceNonFullWidth;
+
+ nsAtom* newLang =
+ charStyle->mExplicitLanguage ? charStyle->mLanguage.get() : nullptr;
+ if (lang != newLang) {
+ lang = newLang;
+ languageSpecificCasing = GetCasingFor(lang);
+ greekState.Reset();
+ irishState.Reset();
+ irishMark = uint32_t(-1);
+ irishMarkSrc = uint32_t(-1);
+ greekMark = uint32_t(-1);
+ }
+ }
+
+ // These should be mutually exclusive: mMaskPassword is set if we are
+ // handling <input type=password>, where the TextEditor code controls
+ // masking and we use its PasswordMask() character, in which case
+ // aMaskChar (from -webkit-text-security) is not used.
+ MOZ_ASSERT_IF(aMaskChar, !(charStyle && charStyle->mMaskPassword));
+
+ bool maskPassword = (charStyle && charStyle->mMaskPassword) || aMaskChar;
+ int extraChars = 0;
+ const mozilla::unicode::MultiCharMapping* mcm;
+ bool inhibitBreakBefore = false; // have we just deleted preceding hyphen?
+
+ if (i < length - 1 && NS_IS_SURROGATE_PAIR(ch, str[i + 1])) {
+ ch = SURROGATE_TO_UCS4(ch, str[i + 1]);
+ }
+
+ // Skip case transform if we're masking current character.
+ if (!maskPassword) {
+ switch (style.case_) {
+ case StyleTextTransformCase::None:
+ break;
+
+ case StyleTextTransformCase::Lowercase:
+ if (languageSpecificCasing == eLSCB_Turkish) {
+ if (ch == 'I') {
+ ch = LATIN_SMALL_LETTER_DOTLESS_I;
+ prevIsLetter = true;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ if (ch == LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE) {
+ ch = 'i';
+ prevIsLetter = true;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ }
+
+ if (languageSpecificCasing == eLSCB_Lithuanian) {
+ // clang-format off
+ /* From SpecialCasing.txt:
+ * # Introduce an explicit dot above when lowercasing capital I's and J's
+ * # whenever there are more accents above.
+ * # (of the accents used in Lithuanian: grave, acute, tilde above, and ogonek)
+ *
+ * 0049; 0069 0307; 0049; 0049; lt More_Above; # LATIN CAPITAL LETTER I
+ * 004A; 006A 0307; 004A; 004A; lt More_Above; # LATIN CAPITAL LETTER J
+ * 012E; 012F 0307; 012E; 012E; lt More_Above; # LATIN CAPITAL LETTER I WITH OGONEK
+ * 00CC; 0069 0307 0300; 00CC; 00CC; lt; # LATIN CAPITAL LETTER I WITH GRAVE
+ * 00CD; 0069 0307 0301; 00CD; 00CD; lt; # LATIN CAPITAL LETTER I WITH ACUTE
+ * 0128; 0069 0307 0303; 0128; 0128; lt; # LATIN CAPITAL LETTER I WITH TILDE
+ */
+ // clang-format on
+ if (ch == 'I' || ch == 'J' || ch == 0x012E) {
+ ch = ToLowerCase(ch);
+ prevIsLetter = true;
+ seenSoftDotted = true;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ if (ch == 0x00CC) {
+ aConvertedString.Append('i');
+ aConvertedString.Append(0x0307);
+ extraChars += 2;
+ ch = 0x0300;
+ prevIsLetter = true;
+ seenSoftDotted = false;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ if (ch == 0x00CD) {
+ aConvertedString.Append('i');
+ aConvertedString.Append(0x0307);
+ extraChars += 2;
+ ch = 0x0301;
+ prevIsLetter = true;
+ seenSoftDotted = false;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ if (ch == 0x0128) {
+ aConvertedString.Append('i');
+ aConvertedString.Append(0x0307);
+ extraChars += 2;
+ ch = 0x0303;
+ prevIsLetter = true;
+ seenSoftDotted = false;
+ sigmaIndex = uint32_t(-1);
+ break;
+ }
+ }
+
+ cat = mozilla::unicode::GetGenCategory(ch);
+
+ if (languageSpecificCasing == eLSCB_Irish &&
+ cat == nsUGenCategory::kLetter) {
+ // See bug 1018805 for Irish lowercasing requirements
+ if (!prevIsLetter && (ch == 'n' || ch == 't')) {
+ ntPrefix = true;
+ } else {
+ if (ntPrefix && mozilla::IrishCasing::IsUpperVowel(ch)) {
+ aConvertedString.Append('-');
+ ++extraChars;
+ }
+ ntPrefix = false;
+ }
+ } else {
+ ntPrefix = false;
+ }
+
+ if (seenSoftDotted && cat == nsUGenCategory::kMark) {
+ // The seenSoftDotted flag will only be set in Lithuanian mode.
+ if (ch == 0x0300 || ch == 0x0301 || ch == 0x0303) {
+ aConvertedString.Append(0x0307);
+ ++extraChars;
+ }
+ }
+ seenSoftDotted = false;
+
+ // Special lowercasing behavior for Greek Sigma: note that this is
+ // listed as context-sensitive in Unicode's SpecialCasing.txt, but is
+ // *not* a language-specific mapping; it applies regardless of the
+ // language of the element.
+ //
+ // The lowercase mapping for CAPITAL SIGMA should be to SMALL SIGMA
+ // (i.e. the non-final form) whenever there is a following letter, or
+ // when the CAPITAL SIGMA occurs in isolation (neither preceded nor
+ // followed by a LETTER); and to FINAL SIGMA when it is preceded by
+ // another letter but not followed by one.
+ //
+ // To implement the context-sensitive nature of this mapping, we keep
+ // track of whether the previous character was a letter. If not,
+ // CAPITAL SIGMA will map directly to SMALL SIGMA. If the previous
+ // character was a letter, CAPITAL SIGMA maps to FINAL SIGMA and we
+ // record the position in the converted string; if we then encounter
+ // another letter, that FINAL SIGMA is replaced with a standard
+ // SMALL SIGMA.
+
+ // If sigmaIndex is not -1, it marks where we have provisionally
+ // mapped a CAPITAL SIGMA to FINAL SIGMA; if we now find another
+ // letter, we need to change it to SMALL SIGMA.
+ if (sigmaIndex != uint32_t(-1)) {
+ if (cat == nsUGenCategory::kLetter) {
+ aConvertedString.SetCharAt(GREEK_SMALL_LETTER_SIGMA, sigmaIndex);
+ }
+ }
+
+ if (ch == GREEK_CAPITAL_LETTER_SIGMA) {
+ // If preceding char was a letter, map to FINAL instead of SMALL,
+ // and note where it occurred by setting sigmaIndex; we'll change
+ // it to standard SMALL SIGMA later if another letter follows
+ if (prevIsLetter) {
+ ch = GREEK_SMALL_LETTER_FINAL_SIGMA;
+ sigmaIndex = aConvertedString.Length();
+ } else {
+ // CAPITAL SIGMA not preceded by a letter is unconditionally
+ // mapped to SMALL SIGMA
+ ch = GREEK_SMALL_LETTER_SIGMA;
+ sigmaIndex = uint32_t(-1);
+ }
+ prevIsLetter = true;
+ break;
+ }
+
+ // ignore diacritics for the purpose of contextual sigma mapping;
+ // otherwise, reset prevIsLetter appropriately and clear the
+ // sigmaIndex marker
+ if (cat != nsUGenCategory::kMark) {
+ prevIsLetter = (cat == nsUGenCategory::kLetter);
+ sigmaIndex = uint32_t(-1);
+ }
+
+ mcm = mozilla::unicode::SpecialLower(ch);
+ if (mcm) {
+ int j = 0;
+ while (j < 2 && mcm->mMappedChars[j + 1]) {
+ aConvertedString.Append(mcm->mMappedChars[j]);
+ ++extraChars;
+ ++j;
+ }
+ ch = mcm->mMappedChars[j];
+ break;
+ }
+
+ ch = ToLowerCase(ch);
+ break;
+
+ case StyleTextTransformCase::Uppercase:
+ if (languageSpecificCasing == eLSCB_Turkish && ch == 'i') {
+ ch = LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE;
+ break;
+ }
+
+ if (languageSpecificCasing == eLSCB_Greek) {
+ bool markEta;
+ bool updateEta;
+ ch = mozilla::GreekCasing::UpperCase(ch, greekState, markEta,
+ updateEta);
+ if (markEta) {
+ greekMark = aConvertedString.Length();
+ } else if (updateEta) {
+ // Remove the TONOS from an uppercase ETA-TONOS that turned out
+ // not to be disjunctive-eta.
+ MOZ_ASSERT(aConvertedString.Length() > 0 &&
+ greekMark < aConvertedString.Length(),
+ "bad greekMark!");
+ aConvertedString.SetCharAt(kGreekUpperEta, greekMark);
+ greekMark = uint32_t(-1);
+ }
+ break;
+ }
+
+ if (languageSpecificCasing == eLSCB_Lithuanian) {
+ /*
+ * # Remove DOT ABOVE after "i" with upper or titlecase
+ *
+ * 0307; 0307; ; ; lt After_Soft_Dotted; # COMBINING DOT ABOVE
+ */
+ if (ch == 'i' || ch == 'j' || ch == 0x012F) {
+ seenSoftDotted = true;
+ ch = ToTitleCase(ch);
+ break;
+ }
+ if (seenSoftDotted) {
+ seenSoftDotted = false;
+ if (ch == 0x0307) {
+ ch = uint32_t(-1);
+ break;
+ }
+ }
+ }
+
+ if (languageSpecificCasing == eLSCB_Irish) {
+ bool mark;
+ uint8_t action;
+ ch = mozilla::IrishCasing::UpperCase(ch, irishState, mark, action);
+ if (mark) {
+ irishMark = aConvertedString.Length();
+ irishMarkSrc = i;
+ break;
+ } else if (action) {
+ nsString& str = aConvertedString; // shorthand
+ switch (action) {
+ case 1:
+ // lowercase a single prefix letter
+ NS_ASSERTION(str.Length() > 0 && irishMark < str.Length(),
+ "bad irishMark!");
+ str.SetCharAt(ToLowerCase(str[irishMark]), irishMark);
+ irishMark = uint32_t(-1);
+ irishMarkSrc = uint32_t(-1);
+ break;
+ case 2:
+ // lowercase two prefix letters (immediately before current
+ // pos)
+ NS_ASSERTION(
+ str.Length() >= 2 && irishMark == str.Length() - 2,
+ "bad irishMark!");
+ str.SetCharAt(ToLowerCase(str[irishMark]), irishMark);
+ str.SetCharAt(ToLowerCase(str[irishMark + 1]), irishMark + 1);
+ irishMark = uint32_t(-1);
+ irishMarkSrc = uint32_t(-1);
+ break;
+ case 3:
+ // lowercase one prefix letter, and delete following hyphen
+ // (which must be the immediately-preceding char)
+ NS_ASSERTION(
+ str.Length() >= 2 && irishMark == str.Length() - 2,
+ "bad irishMark!");
+ MOZ_ASSERT(
+ irishMark != uint32_t(-1) && irishMarkSrc != uint32_t(-1),
+ "failed to set irishMarks");
+ str.Replace(irishMark, 2, ToLowerCase(str[irishMark]));
+ aDeletedCharsArray[irishMarkSrc + 1] = true;
+ // Remove the trailing entries (corresponding to the deleted
+ // hyphen) from the auxiliary arrays.
+ aCharsToMergeArray.SetLength(aCharsToMergeArray.Length() - 1);
+ if (auxiliaryOutputArrays) {
+ aStyleArray->SetLength(aStyleArray->Length() - 1);
+ aCanBreakBeforeArray->SetLength(
+ aCanBreakBeforeArray->Length() - 1);
+ inhibitBreakBefore = true;
+ }
+ mergeNeeded = true;
+ irishMark = uint32_t(-1);
+ irishMarkSrc = uint32_t(-1);
+ break;
+ }
+ // ch has been set to the uppercase for current char;
+ // No need to check for SpecialUpper here as none of the
+ // characters that could trigger an Irish casing action have
+ // special mappings.
+ break;
+ }
+ // If we didn't have any special action to perform, fall through
+ // to check for special uppercase (ß)
+ }
+
+ // Updated mapping for German eszett, not currently reflected in the
+ // Unicode data files. This is behind a pref, as it may not work well
+ // with many (esp. older) fonts.
+ if (ch == 0x00DF &&
+ StaticPrefs::
+ layout_css_text_transform_uppercase_eszett_enabled()) {
+ ch = 0x1E9E;
+ break;
+ }
+
+ mcm = mozilla::unicode::SpecialUpper(ch);
+ if (mcm) {
+ int j = 0;
+ while (j < 2 && mcm->mMappedChars[j + 1]) {
+ aConvertedString.Append(mcm->mMappedChars[j]);
+ ++extraChars;
+ ++j;
+ }
+ ch = mcm->mMappedChars[j];
+ break;
+ }
+
+ // Bug 1476304: we exclude Georgian letters U+10D0..10FF because of
+ // lack of widespread font support for the corresponding Mtavruli
+ // characters at this time (July 2018).
+ // This condition is to be removed once the major platforms ship with
+ // fonts that support U+1C90..1CBF.
+ if (ch < 0x10D0 || ch > 0x10FF) {
+ ch = ToUpperCase(ch);
+ }
+ break;
+
+ case StyleTextTransformCase::Capitalize:
+ if (aTextRun) {
+ if (capitalizeDutchIJ && ch == 'j') {
+ ch = 'J';
+ capitalizeDutchIJ = false;
+ break;
+ }
+ capitalizeDutchIJ = false;
+ if (aOffsetInTextRun < aTextRun->mCapitalize.Length() &&
+ aTextRun->mCapitalize[aOffsetInTextRun]) {
+ if (languageSpecificCasing == eLSCB_Turkish && ch == 'i') {
+ ch = LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE;
+ break;
+ }
+ if (languageSpecificCasing == eLSCB_Dutch && ch == 'i') {
+ ch = 'I';
+ capitalizeDutchIJ = true;
+ break;
+ }
+ if (languageSpecificCasing == eLSCB_Lithuanian) {
+ /*
+ * # Remove DOT ABOVE after "i" with upper or titlecase
+ *
+ * 0307; 0307; ; ; lt After_Soft_Dotted; # COMBINING DOT ABOVE
+ */
+ if (ch == 'i' || ch == 'j' || ch == 0x012F) {
+ seenSoftDotted = true;
+ ch = ToTitleCase(ch);
+ break;
+ }
+ if (seenSoftDotted) {
+ seenSoftDotted = false;
+ if (ch == 0x0307) {
+ ch = uint32_t(-1);
+ break;
+ }
+ }
+ }
+
+ mcm = mozilla::unicode::SpecialTitle(ch);
+ if (mcm) {
+ int j = 0;
+ while (j < 2 && mcm->mMappedChars[j + 1]) {
+ aConvertedString.Append(mcm->mMappedChars[j]);
+ ++extraChars;
+ ++j;
+ }
+ ch = mcm->mMappedChars[j];
+ break;
+ }
+
+ ch = ToTitleCase(ch);
+ }
+ }
+ break;
+
+ default:
+ MOZ_ASSERT_UNREACHABLE("all cases should be handled");
+ break;
+ }
+
+ if (!aCaseTransformsOnly) {
+ if (!forceNonFullWidth &&
+ (style.other_ & StyleTextTransformOther::FULL_WIDTH)) {
+ ch = mozilla::unicode::GetFullWidth(ch);
+ }
+
+ if (style.other_ & StyleTextTransformOther::FULL_SIZE_KANA) {
+ // clang-format off
+ static const uint16_t kSmallKanas[] = {
+ // ぁ ぃ ぅ ぇ ぉ っ ゃ ゅ ょ
+ 0x3041, 0x3043, 0x3045, 0x3047, 0x3049, 0x3063, 0x3083, 0x3085, 0x3087,
+ // ゎ ゕ ゖ
+ 0x308E, 0x3095, 0x3096,
+ // ァ ィ ゥ ェ ォ ッ ャ ュ ョ
+ 0x30A1, 0x30A3, 0x30A5, 0x30A7, 0x30A9, 0x30C3, 0x30E3, 0x30E5, 0x30E7,
+ // ヮ ヵ ヶ ㇰ ㇱ ㇲ ㇳ ㇴ ㇵ
+ 0x30EE, 0x30F5, 0x30F6, 0x31F0, 0x31F1, 0x31F2, 0x31F3, 0x31F4, 0x31F5,
+ // ㇶ ㇷ ㇸ ㇹ ㇺ ㇻ ㇼ ㇽ ㇾ
+ 0x31F6, 0x31F7, 0x31F8, 0x31F9, 0x31FA, 0x31FB, 0x31FC, 0x31FD, 0x31FE,
+ // ㇿ
+ 0x31FF,
+ // ァ ィ ゥ ェ ォ ャ ュ ョ ッ
+ 0xFF67, 0xFF68, 0xFF69, 0xFF6A, 0xFF6B, 0xFF6C, 0xFF6D, 0xFF6E, 0xFF6F};
+ static const uint16_t kFullSizeKanas[] = {
+ // あ い う え お つ や ゆ よ
+ 0x3042, 0x3044, 0x3046, 0x3048, 0x304A, 0x3064, 0x3084, 0x3086, 0x3088,
+ // わ か け
+ 0x308F, 0x304B, 0x3051,
+ // ア イ ウ エ オ ツ ヤ ユ ヨ
+ 0x30A2, 0x30A4, 0x30A6, 0x30A8, 0x30AA, 0x30C4, 0x30E4, 0x30E6, 0x30E8,
+ // ワ カ ケ ク シ ス ト ヌ ハ
+ 0x30EF, 0x30AB, 0x30B1, 0x30AF, 0x30B7, 0x30B9, 0x30C8, 0x30CC, 0x30CF,
+ // ヒ フ ヘ ホ ム ラ リ ル レ
+ 0x30D2, 0x30D5, 0x30D8, 0x30DB, 0x30E0, 0x30E9, 0x30EA, 0x30EB, 0x30EC,
+ // ロ
+ 0x30ED,
+ // ア イ ウ エ オ ヤ ユ ヨ ツ
+ 0xFF71, 0xFF72, 0xFF73, 0xFF74, 0xFF75, 0xFF94, 0xFF95, 0xFF96, 0xFF82};
+ // clang-format on
+
+ size_t index;
+ const uint16_t len = MOZ_ARRAY_LENGTH(kSmallKanas);
+ if (mozilla::BinarySearch(kSmallKanas, 0, len, ch, &index)) {
+ ch = kFullSizeKanas[index];
+ }
+ }
+ }
+
+ if (forceNonFullWidth) {
+ ch = mozilla::unicode::GetFullWidthInverse(ch);
+ }
+ }
+
+ if (ch == uint32_t(-1)) {
+ aDeletedCharsArray.AppendElement(true);
+ mergeNeeded = true;
+ } else {
+ aDeletedCharsArray.AppendElement(false);
+ aCharsToMergeArray.AppendElement(false);
+ if (auxiliaryOutputArrays) {
+ aStyleArray->AppendElement(charStyle);
+ aCanBreakBeforeArray->AppendElement(
+ inhibitBreakBefore
+ ? gfxShapedText::CompressedGlyph::FLAG_BREAK_TYPE_NONE
+ : aTextRun->CanBreakBefore(aOffsetInTextRun));
+ }
+
+ if (IS_IN_BMP(ch)) {
+ aConvertedString.Append(maskPassword ? mask : ch);
+ } else {
+ if (maskPassword) {
+ aConvertedString.Append(mask);
+ // TODO: We should show a password mask for a surrogate pair later.
+ aConvertedString.Append(mask);
+ } else {
+ aConvertedString.Append(H_SURROGATE(ch));
+ aConvertedString.Append(L_SURROGATE(ch));
+ }
+ ++extraChars;
+ ++i;
+ ++aOffsetInTextRun;
+ // Skip the trailing surrogate.
+ aDeletedCharsArray.AppendElement(true);
+ }
+
+ while (extraChars-- > 0) {
+ mergeNeeded = true;
+ aCharsToMergeArray.AppendElement(true);
+ if (auxiliaryOutputArrays) {
+ aStyleArray->AppendElement(charStyle);
+ aCanBreakBeforeArray->AppendElement(
+ gfxShapedText::CompressedGlyph::FLAG_BREAK_TYPE_NONE);
+ }
+ }
+ }
+ }
+
+ return mergeNeeded;
+}
+
+void nsCaseTransformTextRunFactory::RebuildTextRun(
+ nsTransformedTextRun* aTextRun, DrawTarget* aRefDrawTarget,
+ gfxMissingFontRecorder* aMFR) {
+ nsAutoString convertedString;
+ AutoTArray<bool, 50> charsToMergeArray;
+ AutoTArray<bool, 50> deletedCharsArray;
+ AutoTArray<uint8_t, 50> canBreakBeforeArray;
+ AutoTArray<RefPtr<nsTransformedCharStyle>, 50> styleArray;
+
+ auto globalTransform =
+ mAllUppercase
+ ? Some(StyleTextTransform{StyleTextTransformCase::Uppercase, {}})
+ : Nothing();
+ bool mergeNeeded = TransformString(
+ aTextRun->mString, convertedString, globalTransform, mMaskChar,
+ /* aCaseTransformsOnly = */ false, nullptr, charsToMergeArray,
+ deletedCharsArray, aTextRun, 0, &canBreakBeforeArray, &styleArray);
+
+ gfx::ShapedTextFlags flags;
+ gfxTextRunFactory::Parameters innerParams =
+ GetParametersForInner(aTextRun, &flags, aRefDrawTarget);
+ gfxFontGroup* fontGroup = aTextRun->GetFontGroup();
+
+ RefPtr<nsTransformedTextRun> transformedChild;
+ RefPtr<gfxTextRun> cachedChild;
+ gfxTextRun* child;
+
+ if (mInnerTransformingTextRunFactory) {
+ transformedChild = mInnerTransformingTextRunFactory->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+ fontGroup, flags, nsTextFrameUtils::Flags(), std::move(styleArray),
+ false);
+ child = transformedChild.get();
+ } else {
+ cachedChild = fontGroup->MakeTextRun(
+ convertedString.BeginReading(), convertedString.Length(), &innerParams,
+ flags, nsTextFrameUtils::Flags(), aMFR);
+ child = cachedChild.get();
+ }
+ if (!child) {
+ return;
+ }
+ // Copy potential linebreaks into child so they're preserved
+ // (and also child will be shaped appropriately)
+ NS_ASSERTION(convertedString.Length() == canBreakBeforeArray.Length(),
+ "Dropped characters or break-before values somewhere!");
+ gfxTextRun::Range range(0, uint32_t(canBreakBeforeArray.Length()));
+ child->SetPotentialLineBreaks(range, canBreakBeforeArray.Elements());
+ if (transformedChild) {
+ transformedChild->FinishSettingProperties(aRefDrawTarget, aMFR);
+ }
+
+ aTextRun->ResetGlyphRuns();
+ if (mergeNeeded) {
+ // Now merge multiple characters into one multi-glyph character as required
+ // and deal with skipping deleted accent chars
+ NS_ASSERTION(charsToMergeArray.Length() == child->GetLength(),
+ "source length mismatch");
+ NS_ASSERTION(deletedCharsArray.Length() == aTextRun->GetLength(),
+ "destination length mismatch");
+ MergeCharactersInTextRun(aTextRun, child, charsToMergeArray.Elements(),
+ deletedCharsArray.Elements());
+ } else {
+ // No merging to do, so just copy; this produces a more optimized textrun.
+ // We can't steal the data because the child may be cached and stealing
+ // the data would break the cache.
+ aTextRun->CopyGlyphDataFrom(child, gfxTextRun::Range(child), 0);
+ }
+}