Bug 1719546 - Create a unified bidi component; r=platform-i18n-reviewers,dminor
☠☠ backed out by c00a19449788 ☠ ☠
authorGreg Tatum <tatum.creative@gmail.com>
Tue, 19 Oct 2021 16:55:36 +0000
changeset 596370 e69fc596f2c3f306c3bab26d6c41f652f4e124d9
parent 596369 5aaf8123b97aafacc1a001396b2c1d417fbd4b0d
child 596371 a1f7ed6c42510b8a97ea6fe0393ec0c20f43a1f3
push id38896
push userabutkovits@mozilla.com
push dateTue, 19 Oct 2021 21:51:00 +0000
treeherdermozilla-central@e9071741b84c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersplatform-i18n-reviewers, dminor
bugs1719546
milestone95.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1719546 - Create a unified bidi component; r=platform-i18n-reviewers,dminor Differential Revision: https://phabricator.services.mozilla.com/D128792
intl/components/gtest/TestBidi.cpp
intl/components/gtest/moz.build
intl/components/moz.build
intl/components/src/Bidi.cpp
intl/components/src/Bidi.h
new file mode 100644
--- /dev/null
+++ b/intl/components/gtest/TestBidi.cpp
@@ -0,0 +1,278 @@
+/* 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 "gtest/gtest.h"
+
+#include "mozilla/intl/Bidi.h"
+#include "mozilla/Span.h"
+namespace mozilla::intl {
+
+struct VisualRun {
+  Span<const char16_t> string;
+  Bidi::Direction direction;
+};
+
+/**
+ * An iterator for visual runs in a paragraph. See Bug 1736597 for integrating
+ * this into the public API.
+ */
+class MOZ_STACK_CLASS VisualRunIter {
+ public:
+  VisualRunIter(Bidi& aBidi, Span<const char16_t> aParagraph,
+                Bidi::EmbeddingLevel aLevel)
+      : mBidi(aBidi), mParagraph(aParagraph) {
+    // Crash in case of errors by calling unwrap. If this were a real API, this
+    // would be a TryCreate call.
+    mBidi.SetParagraph(aParagraph, aLevel).unwrap();
+    mRunCount = mBidi.CountRuns().unwrap();
+  }
+
+  Maybe<VisualRun> Next() {
+    if (mRunIndex >= mRunCount) {
+      return Nothing();
+    }
+
+    int32_t stringIndex = -1;
+    int32_t stringLength = -1;
+
+    Bidi::Direction direction =
+        mBidi.GetVisualRun(mRunIndex, &stringIndex, &stringLength);
+
+    Span<const char16_t> string(mParagraph.Elements() + stringIndex,
+                                stringLength);
+    mRunIndex++;
+    return Some(VisualRun{string, direction});
+  }
+
+ private:
+  Bidi& mBidi;
+  Span<const char16_t> mParagraph = Span<const char16_t>();
+  int32_t mRunIndex = 0;
+  int32_t mRunCount = 0;
+};
+
+struct LogicalRun {
+  Span<const char16_t> string;
+  Bidi::EmbeddingLevel embeddingLevel;
+};
+
+/**
+ * An iterator for logical runs in a paragraph. See Bug 1736597 for integrating
+ * this into the public API.
+ */
+class MOZ_STACK_CLASS LogicalRunIter {
+ public:
+  LogicalRunIter(Bidi& aBidi, Span<const char16_t> aParagraph,
+                 Bidi::EmbeddingLevel aLevel)
+      : mBidi(aBidi), mParagraph(aParagraph) {
+    // Crash in case of errors by calling unwrap. If this were a real API, this
+    // would be a TryCreate call.
+    mBidi.SetParagraph(aParagraph, aLevel).unwrap();
+    mBidi.CountRuns().unwrap();
+  }
+
+  Maybe<LogicalRun> Next() {
+    if (mRunIndex >= static_cast<int32_t>(mParagraph.Length())) {
+      return Nothing();
+    }
+
+    int32_t logicalLimit;
+
+    Bidi::EmbeddingLevel embeddingLevel;
+    mBidi.GetLogicalRun(mRunIndex, &logicalLimit, &embeddingLevel);
+
+    Span<const char16_t> string(mParagraph.Elements() + mRunIndex,
+                                logicalLimit - mRunIndex);
+
+    mRunIndex = logicalLimit;
+    return Some(LogicalRun{string, embeddingLevel});
+  }
+
+ private:
+  Bidi& mBidi;
+  Span<const char16_t> mParagraph = Span<const char16_t>();
+  int32_t mRunIndex = 0;
+};
+
+TEST(IntlBidi, SimpleLTR)
+{
+  Bidi bidi{};
+  LogicalRunIter logicalRunIter(bidi, MakeStringSpan(u"this is a paragraph"),
+                                Bidi::EmbeddingLevel::DefaultLTR());
+  ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+  ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::LTR);
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"this is a paragraph"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 0);
+    ASSERT_EQ(logicalRun->embeddingLevel.Direction(), Bidi::Direction::LTR);
+  }
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isNothing());
+  }
+}
+
+TEST(IntlBidi, SimpleRTL)
+{
+  Bidi bidi{};
+  LogicalRunIter logicalRunIter(bidi, MakeStringSpan(u"فايرفوكس رائع"),
+                                Bidi::EmbeddingLevel::DefaultLTR());
+  ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 1);
+  ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::RTL);
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"فايرفوكس رائع"));
+    ASSERT_EQ(logicalRun->embeddingLevel.Direction(), Bidi::Direction::RTL);
+    ASSERT_EQ(logicalRun->embeddingLevel, 1);
+  }
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isNothing());
+  }
+}
+
+TEST(IntlBidi, MultiLevel)
+{
+  Bidi bidi{};
+  LogicalRunIter logicalRunIter(
+      bidi, MakeStringSpan(u"Firefox is awesome: رائع Firefox"),
+      Bidi::EmbeddingLevel::DefaultLTR());
+  ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+  ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::Mixed);
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"Firefox is awesome: "));
+    ASSERT_EQ(logicalRun->embeddingLevel, 0);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"رائع"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 1);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u" Firefox"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 0);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isNothing());
+  }
+}
+
+TEST(IntlBidi, RtlOverride)
+{
+  Bidi bidi{};
+  // Set the paragraph using the RTL embedding mark U+202B, and the LTR
+  // embedding mark U+202A to increase the embedding level. This mark switches
+  // the weakly directional character "_". This demonstrates that embedding
+  // levels can be computed.
+  LogicalRunIter logicalRunIter(
+      bidi, MakeStringSpan(u"ltr\u202b___رائع___\u202a___ltr__"),
+      Bidi::EmbeddingLevel::DefaultLTR());
+  ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+  ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::Mixed);
+
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"ltr"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 0);
+    ASSERT_EQ(logicalRun->embeddingLevel.Direction(), Bidi::Direction::LTR);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"\u202b___رائع___"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 1);
+    ASSERT_EQ(logicalRun->embeddingLevel.Direction(), Bidi::Direction::RTL);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isSome());
+    ASSERT_EQ(logicalRun->string, MakeStringSpan(u"\u202a___ltr__"));
+    ASSERT_EQ(logicalRun->embeddingLevel, 2);
+    ASSERT_EQ(logicalRun->embeddingLevel.Direction(), Bidi::Direction::LTR);
+  }
+  {
+    auto logicalRun = logicalRunIter.Next();
+    ASSERT_TRUE(logicalRun.isNothing());
+  }
+}
+
+TEST(IntlBidi, VisualRuns)
+{
+  Bidi bidi{};
+
+  VisualRunIter visualRunIter(
+      bidi,
+      MakeStringSpan(
+          u"first visual run التشغيل البصري الثاني third visual run"),
+      Bidi::EmbeddingLevel::DefaultLTR());
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u"first visual run "));
+    ASSERT_EQ(run->direction, Bidi::Direction::LTR);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u"التشغيل البصري الثاني"));
+    ASSERT_EQ(run->direction, Bidi::Direction::RTL);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u" third visual run"));
+    ASSERT_EQ(run->direction, Bidi::Direction::LTR);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isNothing());
+  }
+}
+
+TEST(IntlBidi, VisualRunsWithEmbeds)
+{
+  // Compare this test to the logical order test.
+  Bidi bidi{};
+  VisualRunIter visualRunIter(
+      bidi, MakeStringSpan(u"ltr\u202b___رائع___\u202a___ltr___"),
+      Bidi::EmbeddingLevel::DefaultLTR());
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u"ltr"));
+    ASSERT_EQ(run->direction, Bidi::Direction::LTR);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u"\u202a___ltr___"));
+    ASSERT_EQ(run->direction, Bidi::Direction::LTR);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isSome());
+    ASSERT_EQ(run->string, MakeStringSpan(u"\u202b___رائع___"));
+    ASSERT_EQ(run->direction, Bidi::Direction::RTL);
+  }
+  {
+    Maybe<VisualRun> run = visualRunIter.Next();
+    ASSERT_TRUE(run.isNothing());
+  }
+}
+
+}  // namespace mozilla::intl
--- a/intl/components/gtest/moz.build
+++ b/intl/components/gtest/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 UNIFIED_SOURCES += [
+    "TestBidi.cpp",
     "TestCalendar.cpp",
     "TestCollator.cpp",
     "TestCurrency.cpp",
     "TestDateTimeFormat.cpp",
     "TestListFormat.cpp",
     "TestLocale.cpp",
     "TestLocaleCanonicalizer.cpp",
     "TestMeasureUnit.cpp",
--- a/intl/components/moz.build
+++ b/intl/components/moz.build
@@ -1,14 +1,15 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 EXPORTS.mozilla.intl = [
+    "src/Bidi.h",
     "src/Calendar.h",
     "src/Collator.h",
     "src/Currency.h",
     "src/DateTimeFormat.h",
     "src/DateTimePatternGenerator.h",
     "src/ICU4CGlue.h",
     "src/ICU4CLibrary.h",
     "src/ICUError.h",
@@ -22,16 +23,17 @@ EXPORTS.mozilla.intl = [
     "src/NumberRangeFormat.h",
     "src/PluralRules.h",
     "src/RelativeTimeFormat.h",
     "src/String.h",
     "src/TimeZone.h",
 ]
 
 UNIFIED_SOURCES += [
+    "src/Bidi.cpp",
     "src/Calendar.cpp",
     "src/Collator.cpp",
     "src/Currency.cpp",
     "src/DateTimeFormat.cpp",
     "src/DateTimePatternGenerator.cpp",
     "src/ICU4CGlue.cpp",
     "src/ICU4CLibrary.cpp",
     "src/ListFormat.cpp",
new file mode 100644
--- /dev/null
+++ b/intl/components/src/Bidi.cpp
@@ -0,0 +1,163 @@
+/* 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 "mozilla/intl/Bidi.h"
+#include "mozilla/Casting.h"
+#include "mozilla/intl/ICU4CGlue.h"
+
+#include "unicode/ubidi.h"
+
+namespace mozilla::intl {
+
+Bidi::Bidi() { mBidi = ubidi_open(); }
+Bidi::~Bidi() { ubidi_close(mBidi.GetMut()); }
+
+ICUResult Bidi::SetParagraph(Span<const char16_t> aParagraph,
+                             Bidi::EmbeddingLevel aLevel) {
+  // Do not allow any reordering of the runs, as this can change the
+  // performance characteristics of working with runs. In the default mode,
+  // the levels can be iterated over directly, rather than relying on computing
+  // logical runs on the fly. This can have negative performance characteristics
+  // compared to iterating over the levels.
+  //
+  // In the UBIDI_REORDER_RUNS_ONLY the levels are encoded with additional
+  // information which can be safely ignored in this Bidi implementation.
+  // Note that this check is here since setting the mode must be done before
+  // calls to setting the paragraph.
+  MOZ_ASSERT(ubidi_getReorderingMode(mBidi.GetMut()) == UBIDI_REORDER_DEFAULT);
+
+  UErrorCode status = U_ZERO_ERROR;
+  ubidi_setPara(mBidi.GetMut(), aParagraph.Elements(),
+                AssertedCast<int32_t>(aParagraph.Length()), aLevel, nullptr,
+                &status);
+
+  mLevels = nullptr;
+
+  return ToICUResult(status);
+}
+
+Bidi::ParagraphDirection Bidi::GetParagraphDirection() const {
+  switch (ubidi_getDirection(mBidi.GetConst())) {
+    case UBIDI_LTR:
+      return Bidi::ParagraphDirection::LTR;
+    case UBIDI_RTL:
+      return Bidi::ParagraphDirection::RTL;
+    case UBIDI_MIXED:
+      return Bidi::ParagraphDirection::Mixed;
+    case UBIDI_NEUTRAL:
+      // This is only used in `ubidi_getBaseDirection` which is unused in this
+      // API.
+      MOZ_ASSERT_UNREACHABLE("Unexpected UBiDiDirection value.");
+  };
+  return Bidi::ParagraphDirection::Mixed;
+}
+
+/* static */
+void Bidi::ReorderVisual(const EmbeddingLevel* aLevels, int32_t aLength,
+                         int32_t* aIndexMap) {
+  ubidi_reorderVisual(reinterpret_cast<const uint8_t*>(aLevels), aLength,
+                      aIndexMap);
+}
+
+static Bidi::Direction ToBidiDirection(UBiDiDirection aDirection) {
+  switch (aDirection) {
+    case UBIDI_LTR:
+      return Bidi::Direction::LTR;
+    case UBIDI_RTL:
+      return Bidi::Direction::RTL;
+    case UBIDI_MIXED:
+    case UBIDI_NEUTRAL:
+      MOZ_ASSERT_UNREACHABLE("Unexpected UBiDiDirection value.");
+  }
+  return Bidi::Direction::LTR;
+}
+
+Result<int32_t, ICUError> Bidi::CountRuns() {
+  UErrorCode status = U_ZERO_ERROR;
+  int32_t runCount = ubidi_countRuns(mBidi.GetMut(), &status);
+  if (U_FAILURE(status)) {
+    return Err(ToICUError(status));
+  }
+
+  mLength = ubidi_getProcessedLength(mBidi.GetConst());
+  mLevels = mLength > 0 ? reinterpret_cast<const Bidi::EmbeddingLevel*>(
+                              ubidi_getLevels(mBidi.GetMut(), &status))
+                        : nullptr;
+  if (U_FAILURE(status)) {
+    return Err(ToICUError(status));
+  }
+
+  return runCount;
+}
+
+void Bidi::GetLogicalRun(int32_t aLogicalStart, int32_t* aLogicalLimitOut,
+                         Bidi::EmbeddingLevel* aLevelOut) {
+  MOZ_ASSERT(mLevels, "CountRuns hasn't been run?");
+  MOZ_RELEASE_ASSERT(aLogicalStart < mLength, "Out of bound");
+  EmbeddingLevel level = mLevels[aLogicalStart];
+  int32_t limit;
+  for (limit = aLogicalStart + 1; limit < mLength; limit++) {
+    if (mLevels[limit] != level) {
+      break;
+    }
+  }
+  *aLogicalLimitOut = limit;
+  *aLevelOut = level;
+}
+
+bool Bidi::EmbeddingLevel::IsDefaultLTR() const {
+  return mValue == UBIDI_DEFAULT_LTR;
+};
+
+bool Bidi::EmbeddingLevel::IsDefaultRTL() const {
+  return mValue == UBIDI_DEFAULT_RTL;
+};
+
+bool Bidi::EmbeddingLevel::IsRTL() const {
+  // If the least significant bit is 1, then the embedding level
+  // is right-to-left.
+  // If the least significant bit is 0, then the embedding level
+  // is left-to-right.
+  return (mValue & 0x1) == 1;
+};
+
+bool Bidi::EmbeddingLevel::IsLTR() const { return !IsRTL(); };
+
+bool Bidi::EmbeddingLevel::IsSameDirection(EmbeddingLevel aOther) const {
+  return (((mValue ^ aOther) & 1) == 0);
+}
+
+Bidi::EmbeddingLevel Bidi::EmbeddingLevel::LTR() {
+  return Bidi::EmbeddingLevel(0);
+};
+
+Bidi::EmbeddingLevel Bidi::EmbeddingLevel::RTL() {
+  return Bidi::EmbeddingLevel(1);
+};
+
+Bidi::EmbeddingLevel Bidi::EmbeddingLevel::DefaultLTR() {
+  return Bidi::EmbeddingLevel(UBIDI_DEFAULT_LTR);
+};
+
+Bidi::EmbeddingLevel Bidi::EmbeddingLevel::DefaultRTL() {
+  return Bidi::EmbeddingLevel(UBIDI_DEFAULT_RTL);
+};
+
+Bidi::Direction Bidi::EmbeddingLevel::Direction() {
+  return IsRTL() ? Direction::RTL : Direction::LTR;
+};
+
+uint8_t Bidi::EmbeddingLevel::Value() const { return mValue; }
+
+Bidi::EmbeddingLevel Bidi::GetParagraphEmbeddingLevel() const {
+  return Bidi::EmbeddingLevel(ubidi_getParaLevel(mBidi.GetConst()));
+}
+
+Bidi::Direction Bidi::GetVisualRun(int32_t aRunIndex, int32_t* aLogicalStart,
+                                   int32_t* aLength) {
+  return ToBidiDirection(
+      ubidi_getVisualRun(mBidi.GetMut(), aRunIndex, aLogicalStart, aLength));
+}
+
+}  // namespace mozilla::intl
new file mode 100644
--- /dev/null
+++ b/intl/components/src/Bidi.h
@@ -0,0 +1,241 @@
+/* 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/. */
+#ifndef intl_components_Bidi_h_
+#define intl_components_Bidi_h_
+
+#include "mozilla/intl/ICU4CGlue.h"
+
+struct UBiDi;
+
+namespace mozilla::intl {
+
+/**
+ * This component is a Mozilla-focused API for working with bidirectional (bidi)
+ * text. Text is commonly displayed left to right (LTR), especially for
+ * Latin-based alphabets. However, languages like Arabic and Hebrew displays
+ * text right to left (RTL). When displaying text, LTR and RTL text can be
+ * combined together in the same paragraph. This class gives tools for working
+ * with unidirectional, and mixed direction paragraphs.
+ *
+ * See the Unicode Bidirectional Algorithm document for implementation details:
+ * https://unicode.org/reports/tr9/
+ */
+class Bidi final {
+ public:
+  Bidi();
+  ~Bidi();
+
+  // Not copyable or movable
+  Bidi(const Bidi&) = delete;
+  Bidi& operator=(const Bidi&) = delete;
+
+  /**
+   * This enum unambiguously classifies text runs as either being left to right,
+   * or right to left.
+   */
+  enum class Direction : uint8_t {
+    // Left to right text.
+    LTR = 0,
+    // Right to left text.
+    RTL = 1,
+  };
+
+  /**
+   * This enum indicates the text direction for the set paragraph. Some
+   * paragraphs are unidirectional, where they only have one direction, or a
+   * paragraph could use both LTR and RTL. In this case the paragraph's
+   * direction would be mixed.
+   */
+  enum ParagraphDirection { LTR, RTL, Mixed };
+
+  /**
+   * Embedding levels are numbers that indicate how deeply the bidi text is
+   * embedded, and the direction of text on that embedding level. When switching
+   * between strongly LTR code points and strongly RTL code points the embedding
+   * level normally switches between an embedding level of 0 (LTR) and 1 (RTL).
+   * The only time the embedding level increases is if the embedding code points
+   * are used. This is the Left-to-Right Embedding (LRE) code point (U+202A), or
+   * the Right-to-Left Embedding (RLE) code point (U+202B). The minimum
+   * embedding level of text is zero, and the maximum explicit depth is 125.
+   *
+   * The most significant bit is reserved for additional meaning. It can be used
+   * to signify in certain APIs that the text should by default be LTR or RTL if
+   * no strongly directional code points are found.
+   *
+   * Bug 1736595: At the time of this writing, some places in Gecko code use a 1
+   * in the most significant bit to indicate that an embedding level has not
+   * been set. This leads to an ambiguous understanding of what the most
+   * significant bit actually means.
+   */
+  class EmbeddingLevel {
+   public:
+    explicit EmbeddingLevel(uint8_t aValue) : mValue(aValue) {}
+    explicit EmbeddingLevel(int aValue)
+        : mValue(static_cast<uint8_t>(aValue)) {}
+
+    EmbeddingLevel() = default;
+
+    // Enable the copy operators, but disable move as this is only a uint8_t.
+    EmbeddingLevel(const EmbeddingLevel& other) = default;
+    EmbeddingLevel& operator=(const EmbeddingLevel& other) = default;
+
+    /**
+     * Determine the direction of the embedding level by looking at the least
+     * significant bit. If it is 0, then it is LTR. If it is 1, then it is RTL.
+     */
+    Direction Direction();
+
+    /**
+     * Create a left-to-right embedding level.
+     */
+    static EmbeddingLevel LTR();
+
+    /**
+     * Create an right-to-left embedding level.
+     */
+    static EmbeddingLevel RTL();
+
+    /**
+     * When passed into `SetParagraph`, the direction is determined by first
+     * strongly directional character, with the default set to left-to-right if
+     * none is found.
+     *
+     * This is encoded with the highest bit set to 1.
+     */
+    static EmbeddingLevel DefaultLTR();
+
+    /**
+     * When passed into `SetParagraph`, the direction is determined by first
+     * strongly directional character, with the default set to right-to-left if
+     * none is found.
+     *
+     * * This is encoded with the highest and lowest bits set to 1.
+     */
+    static EmbeddingLevel DefaultRTL();
+
+    bool IsDefaultLTR() const;
+    bool IsDefaultRTL() const;
+    bool IsLTR() const;
+    bool IsRTL() const;
+    bool IsSameDirection(EmbeddingLevel aOther) const;
+
+    /**
+     * Get the underlying value as a uint8_t.
+     */
+    uint8_t Value() const;
+
+    /**
+     * Implicitly convert to the underlying value.
+     */
+    operator uint8_t() const { return mValue; }
+
+   private:
+    uint8_t mValue = 0;
+  };
+
+  /**
+   * Set the current paragraph of text to analyze for its bidi properties. This
+   * performs the Unicode bidi algorithm as specified by:
+   * https://unicode.org/reports/tr9/
+   *
+   * After setting the text, the other getter methods can be used to find out
+   * the directionality of the paragraph text.
+   */
+  ICUResult SetParagraph(Span<const char16_t> aParagraph,
+                         EmbeddingLevel aLevel);
+
+  /**
+   * Get the embedding level for the paragraph that was set by SetParagraph.
+   */
+  EmbeddingLevel GetParagraphEmbeddingLevel() const;
+
+  /**
+   * Get the directionality of the paragraph text that was set by SetParagraph.
+   */
+  ParagraphDirection GetParagraphDirection() const;
+
+  /**
+   * Get the number of runs. This function may invoke the actual reordering on
+   * the Bidi object, after SetParagraph may have resolved only the levels of
+   * the text. Therefore, `CountRuns` may have to allocate memory, and may fail
+   * doing so.
+   */
+  Result<int32_t, ICUError> CountRuns();
+
+  /**
+   * Get the next logical run. The logical runs are a run of text that has the
+   * same directionality and embedding level. These runs are in memory order,
+   * and not in display order.
+   *
+   * Important! `Bidi::CountRuns` must be called before calling this method.
+   *
+   * @param aLogicalStart is the offset into the paragraph text that marks the
+   *      logical start of the text.
+   * @param aLogicalLimitOut is an out param that is the length of the string
+   *      that makes up the logical run.
+   * @param aLevelOut is an out parameter that returns the embedding level for
+   *      the run
+   */
+  void GetLogicalRun(int32_t aLogicalStart, int32_t* aLogicalLimitOut,
+                     EmbeddingLevel* aLevelOut);
+
+  /**
+   * This is a convenience function that does not use the ICU Bidi object.
+   * It is intended to be used for when an application has determined the
+   * embedding levels of objects (character sequences) and just needs to have
+   * them reordered (L2).
+   *
+   * @param aLevels is an array with `aLength` levels that have been
+   *      determined by the application.
+   *
+   * @param aLength is the number of levels in the array, or, semantically,
+   *      the number of objects to be reordered. It must be greater than 0.
+   *
+   * @param aIndexMap is a pointer to an array of `aLength`
+   *      indexes which will reflect the reordering of the characters.
+   *      The array does not need to be initialized.
+   *      The index map will result in
+   *        `aIndexMap[aVisualIndex]==aLogicalIndex`.
+   */
+  static void ReorderVisual(const EmbeddingLevel* aLevels, int32_t aLength,
+                            int32_t* aIndexMap);
+
+  /**
+   * Get one run's logical start, length, and directionality. In an RTL run, the
+   * character at the logical start is visually on the right of the displayed
+   * run. The length is the number of characters in the run.
+   * `Bidi::CountRuns` should be called before the runs are retrieved.
+   *
+   * @param aRunIndex is the number of the run in visual order, in the
+   *      range `[0..CountRuns-1]`.
+   *
+   * @param aLogicalStart is the first logical character index in the text.
+   *      The pointer may be `nullptr` if this index is not needed.
+   *
+   * @param aLength is the number of characters (at least one) in the run.
+   *      The pointer may be `nullptr` if this is not needed.
+   *
+   * Note that in right-to-left runs, the code places modifier letters before
+   * base characters and second surrogates before first ones.
+   */
+  Direction GetVisualRun(int32_t aRunIndex, int32_t* aLogicalStart,
+                         int32_t* aLength);
+
+ private:
+  ICUPointer<UBiDi> mBidi = ICUPointer<UBiDi>(nullptr);
+
+  /**
+   * An array of levels that is the same length as the paragraph from
+   * `Bidi::SetParagraph`.
+   */
+  const EmbeddingLevel* mLevels = nullptr;
+
+  /**
+   * The length of the paragraph from `Bidi::SetParagraph`.
+   */
+  int32_t mLength = 0;
+};
+
+}  // namespace mozilla::intl
+#endif