// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; namespace SixLabors.Fonts.Unicode { /// /// Implementation of Unicode Bidirection Algorithm (UAX #9) /// https://unicode.org/reports/tr9/ /// /// /// /// The Bidi algorithm uses a number of memory arrays for resolved /// types, level information, bracket types, x9 removal maps and /// more... /// /// /// This implementation of the Bidi algorithm has been designed /// to reduce memory pressure on the GC by re-using the same /// work buffers, so instances of this class should be re-used /// as much as possible. /// /// internal sealed class BidiAlgorithm { /// /// The original BidiCharacterType types as provided by the caller /// private ReadOnlyArraySlice originalTypes; /// /// Paired bracket types as provided by caller /// private ReadOnlyArraySlice pairedBracketTypes; /// /// Paired bracket values as provided by caller /// private ReadOnlyArraySlice pairedBracketValues; /// /// Try if the incoming data is known to contain brackets /// private bool hasBrackets; /// /// True if the incoming data is known to contain embedding runs /// private bool hasEmbeddings; /// /// True if the incoming data is known to contain isolating runs /// private bool hasIsolates; /// /// Two directional mapping of isolate start/end pairs /// /// /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// private readonly BidiDictionary isolatePairs = new(); /// /// The working BidiCharacterType types /// private ArraySlice workingTypes; /// /// The buffer underlying _workingTypes /// private ArrayBuilder workingTypesBuffer; /// /// The buffer underlying resolvedLevels /// private ArrayBuilder resolvedLevelsBuffer; /// /// The resolve paragraph embedding level /// private sbyte paragraphEmbeddingLevel; /// /// The status stack used during resolution of explicit /// embedding and isolating runs /// private readonly Stack statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 /// private ArrayBuilder x9Map; /// /// Re-usable list of level runs /// private readonly List levelRuns = new(); /// /// Mapping for the current isolating sequence, built /// by joining level runs from the x9 map. /// private ArrayBuilder isolatedRunMapping; /// /// A stack of pending isolate openings used by FindIsolatePairs() /// private readonly Stack pendingIsolateOpenings = new(); /// /// The level of the isolating run currently being processed /// private int runLevel; /// /// The direction of the isolating run currently being processed /// private BidiCharacterType runDirection; /// /// The length of the isolating run currently being processed /// private int runLength; /// /// A mapped slice of the resolved types for the isolating run currently /// being processed /// private MappedArraySlice runResolvedTypes; /// /// A mapped slice of the original types for the isolating run currently /// being processed /// private ReadonlyMappedArraySlice runOriginalTypes; /// /// A mapped slice of the run levels for the isolating run currently /// being processed /// private MappedArraySlice runLevels; /// /// A mapped slice of the paired bracket types of the isolating /// run currently being processed /// private ReadonlyMappedArraySlice runBidiPairedBracketTypes; /// /// A mapped slice of the paired bracket values of the isolating /// run currently being processed /// private ReadonlyMappedArraySlice runPairedBracketValues; /// /// Maximum pairing depth for paired brackets /// private const int MaxPairedBracketDepth = 63; /// /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// private readonly List pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// private readonly List pairedBrackets = new(); /// /// Initializes a new instance of the class. /// public BidiAlgorithm() { } /// /// Gets a per-thread instance that can be re-used as often /// as necessary. /// public static ThreadLocal Instance { get; } = new ThreadLocal(() => new BidiAlgorithm()); /// /// Gets the resolved levels. /// public ArraySlice ResolvedLevels { get; private set; } /// /// Gets the resolved paragraph embedding level /// public int ResolvedParagraphEmbeddingLevel => this.paragraphEmbeddingLevel; /// /// Process data from a BidiData instance /// /// The Bidi Unicode data. public void Process(BidiData data) => this.Process( data.Types, data.PairedBracketTypes, data.PairedBracketValues, data.ParagraphEmbeddingLevel, data.HasBrackets, data.HasEmbeddings, data.HasIsolates, null); /// /// Processes Bidi Data /// public void Process( ReadOnlyArraySlice types, ReadOnlyArraySlice pairedBracketTypes, ReadOnlyArraySlice pairedBracketValues, sbyte paragraphEmbeddingLevel, bool? hasBrackets, bool? hasEmbeddings, bool? hasIsolates, ArraySlice? outLevels) { // Reset state this.isolatePairs.Clear(); this.workingTypesBuffer.Clear(); this.levelRuns.Clear(); this.resolvedLevelsBuffer.Clear(); // Setup original types and working types this.originalTypes = types; this.workingTypes = this.workingTypesBuffer.Add(types); // Capture paired bracket values and types this.pairedBracketTypes = pairedBracketTypes; this.pairedBracketValues = pairedBracketValues; // Store things we know this.hasBrackets = hasBrackets ?? this.pairedBracketTypes.Length == this.originalTypes.Length; this.hasEmbeddings = hasEmbeddings ?? true; this.hasIsolates = hasIsolates ?? true; // Find all isolate pairs this.FindIsolatePairs(); // Resolve the paragraph embedding level if (paragraphEmbeddingLevel == 2) { this.paragraphEmbeddingLevel = this.ResolveEmbeddingLevel(this.originalTypes); } else { this.paragraphEmbeddingLevel = paragraphEmbeddingLevel; } // Create resolved levels buffer if (outLevels.HasValue) { if (outLevels.Value.Length != this.originalTypes.Length) { throw new ArgumentException("Out levels must be the same length as the input data"); } this.ResolvedLevels = outLevels.Value; } else { this.ResolvedLevels = this.resolvedLevelsBuffer.Add(this.originalTypes.Length); this.ResolvedLevels.Fill(this.paragraphEmbeddingLevel); } // Resolve explicit embedding levels (Rules X1-X8) this.ResolveExplicitEmbeddingLevels(); // Build the rule X9 map this.BuildX9RemovalMap(); // Process all isolated run sequences this.ProcessIsolatedRunSequences(); // Reset whitespace levels this.ResetWhitespaceLevels(); // Clean up this.AssignLevelsToCodePointsRemovedByX9(); } /// /// Resolve the paragraph embedding level if not explicitly passed /// by the caller. Also used by rule X5c for FSI isolating sequences. /// /// The data to be evaluated /// The resolved embedding level public sbyte ResolveEmbeddingLevel(ReadOnlyArraySlice data) { // P2 for (int i = 0; i < data.Length; ++i) { switch (data[i]) { case BidiCharacterType.LeftToRight: // P3 return 0; case BidiCharacterType.ArabicLetter: case BidiCharacterType.RightToLeft: // P3 return 1; case BidiCharacterType.FirstStrongIsolate: case BidiCharacterType.LeftToRightIsolate: case BidiCharacterType.RightToLeftIsolate: // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) if (this.isolatePairs.TryGetValue(data.Start + i, out i)) { i -= data.Start; } else { i = data.Length; } break; } } // P3 return 0; } /// /// Build a list of matching isolates for a directionality slice /// Implements BD9 /// private void FindIsolatePairs() { // Redundant? if (!this.hasIsolates) { return; } // Lets double check this as we go and clear the flag // if there actually aren't any isolate pairs as this might // mean we can skip some later steps this.hasIsolates = false; // BD9... this.pendingIsolateOpenings.Clear(); for (int i = 0; i < this.originalTypes.Length; i++) { BidiCharacterType t = this.originalTypes[i]; if (t is BidiCharacterType.LeftToRightIsolate or BidiCharacterType.RightToLeftIsolate or BidiCharacterType.FirstStrongIsolate) { this.pendingIsolateOpenings.Push(i); this.hasIsolates = true; } else if (t == BidiCharacterType.PopDirectionalIsolate) { if (this.pendingIsolateOpenings.Count > 0) { this.isolatePairs.Add(this.pendingIsolateOpenings.Pop(), i); } this.hasIsolates = true; } } } /// /// Resolve the explicit embedding levels from the original /// data. Implements rules X1 to X8. /// private void ResolveExplicitEmbeddingLevels() { // Redundant? if (!this.hasIsolates && !this.hasEmbeddings) { return; } // Work variables this.statusStack.Clear(); int overflowIsolateCount = 0; int overflowEmbeddingCount = 0; int validIsolateCount = 0; // Constants const int maxStackDepth = 125; // Rule X1 - setup initial state this.statusStack.Clear(); // Neutral this.statusStack.Push(new Status(this.paragraphEmbeddingLevel, BidiCharacterType.OtherNeutral, false)); // Process all characters for (int i = 0; i < this.originalTypes.Length; i++) { switch (this.originalTypes[i]) { case BidiCharacterType.RightToLeftEmbedding: { // Rule X2 sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 1) | 1); if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) { this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, false)); this.ResolvedLevels[i] = newLevel; } else if (overflowIsolateCount == 0) { overflowEmbeddingCount++; } break; } case BidiCharacterType.LeftToRightEmbedding: { // Rule X3 sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 2) & ~1); if (newLevel < maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) { this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, false)); this.ResolvedLevels[i] = newLevel; } else if (overflowIsolateCount == 0) { overflowEmbeddingCount++; } break; } case BidiCharacterType.RightToLeftOverride: { // Rule X4 sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 1) | 1); if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) { this.statusStack.Push(new Status(newLevel, BidiCharacterType.RightToLeft, false)); this.ResolvedLevels[i] = newLevel; } else if (overflowIsolateCount == 0) { overflowEmbeddingCount++; } break; } case BidiCharacterType.LeftToRightOverride: { // Rule X5 sbyte newLevel = (sbyte)((this.statusStack.Peek().EmbeddingLevel + 2) & ~1); if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) { this.statusStack.Push(new Status(newLevel, BidiCharacterType.LeftToRight, false)); this.ResolvedLevels[i] = newLevel; } else if (overflowIsolateCount == 0) { overflowEmbeddingCount++; } break; } case BidiCharacterType.RightToLeftIsolate: case BidiCharacterType.LeftToRightIsolate: case BidiCharacterType.FirstStrongIsolate: { // Rule X5a, X5b and X5c BidiCharacterType resolvedIsolate = this.originalTypes[i]; if (resolvedIsolate == BidiCharacterType.FirstStrongIsolate) { if (!this.isolatePairs.TryGetValue(i, out int endOfIsolate)) { endOfIsolate = this.originalTypes.Length; } // Rule X5c if (this.ResolveEmbeddingLevel(this.originalTypes.Slice(i + 1, endOfIsolate - (i + 1))) == 1) { resolvedIsolate = BidiCharacterType.RightToLeftIsolate; } else { resolvedIsolate = BidiCharacterType.LeftToRightIsolate; } } // Replace RLI's level with current embedding level Status tos = this.statusStack.Peek(); this.ResolvedLevels[i] = tos.EmbeddingLevel; // Apply override if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) { this.workingTypes[i] = tos.OverrideStatus; } // Work out new level sbyte newLevel; if (resolvedIsolate == BidiCharacterType.RightToLeftIsolate) { newLevel = (sbyte)((tos.EmbeddingLevel + 1) | 1); } else { newLevel = (sbyte)((tos.EmbeddingLevel + 2) & ~1); } // Valid? if (newLevel <= maxStackDepth && overflowIsolateCount == 0 && overflowEmbeddingCount == 0) { validIsolateCount++; this.statusStack.Push(new Status(newLevel, BidiCharacterType.OtherNeutral, true)); } else { overflowIsolateCount++; } break; } case BidiCharacterType.BoundaryNeutral: { // Mentioned in rule X6 - "for all types besides ..., BN, ..." // no-op break; } default: { // Rule X6 Status tos = this.statusStack.Peek(); this.ResolvedLevels[i] = tos.EmbeddingLevel; if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) { this.workingTypes[i] = tos.OverrideStatus; } break; } case BidiCharacterType.PopDirectionalIsolate: { // Rule X6a if (overflowIsolateCount > 0) { overflowIsolateCount--; } else if (validIsolateCount != 0) { overflowEmbeddingCount = 0; while (!this.statusStack.Peek().IsolateStatus) { this.statusStack.Pop(); } this.statusStack.Pop(); validIsolateCount--; } Status tos = this.statusStack.Peek(); this.ResolvedLevels[i] = tos.EmbeddingLevel; if (tos.OverrideStatus != BidiCharacterType.OtherNeutral) { this.workingTypes[i] = tos.OverrideStatus; } break; } case BidiCharacterType.PopDirectionalFormat: { // Rule X7 if (overflowIsolateCount == 0) { if (overflowEmbeddingCount > 0) { overflowEmbeddingCount--; } else if (!this.statusStack.Peek().IsolateStatus && this.statusStack.Count >= 2) { this.statusStack.Pop(); } } break; } case BidiCharacterType.ParagraphSeparator: { // Rule X8 this.ResolvedLevels[i] = this.paragraphEmbeddingLevel; break; } } } } /// /// Build a map to the original data positions that excludes all /// the types defined by rule X9 /// private void BuildX9RemovalMap() { // Reserve room for the x9 map this.x9Map.Length = this.originalTypes.Length; if (this.hasEmbeddings || this.hasIsolates) { // Build a map the removes all x9 characters int j = 0; for (int i = 0; i < this.originalTypes.Length; i++) { if (!IsRemovedByX9(this.originalTypes[i])) { this.x9Map[j++] = i; } } // Set the final length this.x9Map.Length = j; } else { for (int i = 0, count = this.originalTypes.Length; i < count; i++) { this.x9Map[i] = i; } } } /// /// Find the original character index for an entry in the X9 map /// /// Index in the x9 removal map /// Index to the original data [MethodImpl(MethodImplOptions.AggressiveInlining)] private int MapX9(int index) => this.x9Map[index]; /// /// Add a new level run /// /// /// This method resolves the sos and eos values for the run /// and adds the run to the list /// /// /// The index of the start of the run (in x9 removed units) /// The length of the run (in x9 removed units) /// The level of the run private void AddLevelRun(int start, int length, int level) { // Get original indices to first and last character in this run int firstCharIndex = this.MapX9(start); int lastCharIndex = this.MapX9(start + length - 1); // Work out sos int i = firstCharIndex - 1; while (i >= 0 && IsRemovedByX9(this.originalTypes[i])) { i--; } sbyte prevLevel = i < 0 ? this.paragraphEmbeddingLevel : this.ResolvedLevels[i]; BidiCharacterType sos = DirectionFromLevel(Math.Max(prevLevel, level)); // Work out eos BidiCharacterType lastType = this.workingTypes[lastCharIndex]; int nextLevel; if (lastType is BidiCharacterType.LeftToRightIsolate or BidiCharacterType.RightToLeftIsolate or BidiCharacterType.FirstStrongIsolate) { nextLevel = this.paragraphEmbeddingLevel; } else { i = lastCharIndex + 1; while (i < this.originalTypes.Length && IsRemovedByX9(this.originalTypes[i])) { i++; } nextLevel = i >= this.originalTypes.Length ? this.paragraphEmbeddingLevel : this.ResolvedLevels[i]; } BidiCharacterType eos = DirectionFromLevel(Math.Max(nextLevel, level)); // Add the run this.levelRuns.Add(new LevelRun(start, length, level, sos, eos)); } /// /// Find all runs of the same level, populating the _levelRuns /// collection /// private void FindLevelRuns() { int currentLevel = -1; int runStart = 0; for (int i = 0; i < this.x9Map.Length; ++i) { int level = this.ResolvedLevels[this.MapX9(i)]; if (level != currentLevel) { if (currentLevel != -1) { this.AddLevelRun(runStart, i - runStart, currentLevel); } currentLevel = level; runStart = i; } } // Don't forget the final level run if (currentLevel != -1) { this.AddLevelRun(runStart, this.x9Map.Length - runStart, currentLevel); } } /// /// Given a character index, find the level run that starts at that position /// /// The index into the original (unmapped) data /// The index of the run that starts at that index private int FindRunForIndex(int index) { for (int i = 0; i < this.levelRuns.Count; i++) { // Passed index is for the original non-x9 filtered data, however // the level run ranges are for the x9 filtered data. Convert before // comparing if (this.MapX9(this.levelRuns[i].Start) == index) { return i; } } throw new InvalidOperationException("Internal error"); } /// /// Determine and the process all isolated run sequences /// private void ProcessIsolatedRunSequences() { // Find all runs with the same level this.FindLevelRuns(); // Process them one at a time by first building // a mapping using slices from the x9 map for each // run section that needs to be joined together to // form an complete run. That full run mapping // will be placed in _isolatedRunMapping and then // processed by ProcessIsolatedRunSequence(). while (this.levelRuns.Count > 0) { // Clear the mapping this.isolatedRunMapping.Clear(); // Combine mappings from this run and all runs that continue on from it int runIndex = 0; BidiCharacterType eos; BidiCharacterType sos = this.levelRuns[0].Sos; int level = this.levelRuns[0].Level; while (true) { // Get the run LevelRun r = this.levelRuns[runIndex]; // The eos of the isolating run is the eos of the // last level run that comprises it. eos = r.Eos; // Remove this run as we've now processed it this.levelRuns.RemoveAt(runIndex); // Add the x9 map indices for the run range to the mapping // for this isolated run this.isolatedRunMapping.Add(this.x9Map.AsSlice(r.Start, r.Length)); // Get the last character and see if it's an isolating run with a matching // PDI and concatenate that run to this one int lastCharacterIndex = this.isolatedRunMapping[this.isolatedRunMapping.Length - 1]; BidiCharacterType lastType = this.originalTypes[lastCharacterIndex]; if ((lastType == BidiCharacterType.LeftToRightIsolate || lastType == BidiCharacterType.RightToLeftIsolate || lastType == BidiCharacterType.FirstStrongIsolate) && this.isolatePairs.TryGetValue(lastCharacterIndex, out int nextRunIndex)) { // Find the continuing run index runIndex = this.FindRunForIndex(nextRunIndex); } else { break; } } // Process this isolated run this.ProcessIsolatedRunSequence(sos, eos, level); } } /// /// Process a single isolated run sequence, where the character sequence /// mapping is currently held in _isolatedRunMapping. /// private void ProcessIsolatedRunSequence(BidiCharacterType sos, BidiCharacterType eos, int runLevel) { // Create mappings onto the underlying data this.runResolvedTypes = new MappedArraySlice(this.workingTypes, this.isolatedRunMapping.AsSlice()); this.runOriginalTypes = new ReadonlyMappedArraySlice(this.originalTypes, this.isolatedRunMapping.AsSlice()); this.runLevels = new MappedArraySlice(this.ResolvedLevels, this.isolatedRunMapping.AsSlice()); if (this.hasBrackets) { this.runBidiPairedBracketTypes = new ReadonlyMappedArraySlice(this.pairedBracketTypes, this.isolatedRunMapping.AsSlice()); this.runPairedBracketValues = new ReadonlyMappedArraySlice(this.pairedBracketValues, this.isolatedRunMapping.AsSlice()); } this.runLevel = runLevel; this.runDirection = DirectionFromLevel(runLevel); this.runLength = this.runResolvedTypes.Length; // By tracking the types of characters known to be in the current run, we can // skip some of the rules that we know won't apply. The flags will be // initialized while we're processing rule W1 below. bool hasEN = false; bool hasAL = false; bool hasES = false; bool hasCS = false; bool hasAN = false; bool hasET = false; // Rule W1 // Also, set hasXX flags int i; BidiCharacterType prevType = sos; for (i = 0; i < this.runLength; i++) { BidiCharacterType t = this.runResolvedTypes[i]; switch (t) { case BidiCharacterType.NonspacingMark: this.runResolvedTypes[i] = prevType; break; case BidiCharacterType.LeftToRightIsolate: case BidiCharacterType.RightToLeftIsolate: case BidiCharacterType.FirstStrongIsolate: case BidiCharacterType.PopDirectionalIsolate: prevType = BidiCharacterType.OtherNeutral; break; case BidiCharacterType.EuropeanNumber: hasEN = true; prevType = t; break; case BidiCharacterType.ArabicLetter: hasAL = true; prevType = t; break; case BidiCharacterType.EuropeanSeparator: hasES = true; prevType = t; break; case BidiCharacterType.CommonSeparator: hasCS = true; prevType = t; break; case BidiCharacterType.ArabicNumber: hasAN = true; prevType = t; break; case BidiCharacterType.EuropeanTerminator: hasET = true; prevType = t; break; default: prevType = t; break; } } // Rule W2 if (hasEN) { for (i = 0; i < this.runLength; i++) { if (this.runResolvedTypes[i] == BidiCharacterType.EuropeanNumber) { for (int j = i - 1; j >= 0; j--) { BidiCharacterType t = this.runResolvedTypes[j]; if (t is BidiCharacterType.LeftToRight or BidiCharacterType.RightToLeft or BidiCharacterType.ArabicLetter) { if (t == BidiCharacterType.ArabicLetter) { this.runResolvedTypes[i] = BidiCharacterType.ArabicNumber; hasAN = true; } break; } } } } } // Rule W3 if (hasAL) { for (i = 0; i < this.runLength; i++) { if (this.runResolvedTypes[i] == BidiCharacterType.ArabicLetter) { this.runResolvedTypes[i] = BidiCharacterType.RightToLeft; } } } // Rule W4 if ((hasES || hasCS) && (hasEN || hasAN)) { for (i = 1; i < this.runLength - 1; ++i) { ref BidiCharacterType rt = ref this.runResolvedTypes[i]; if (rt == BidiCharacterType.EuropeanSeparator) { BidiCharacterType prevSepType = this.runResolvedTypes[i - 1]; BidiCharacterType succSepType = this.runResolvedTypes[i + 1]; if (prevSepType == BidiCharacterType.EuropeanNumber && succSepType == BidiCharacterType.EuropeanNumber) { // ES between EN and EN rt = BidiCharacterType.EuropeanNumber; } } else if (rt == BidiCharacterType.CommonSeparator) { BidiCharacterType prevSepType = this.runResolvedTypes[i - 1]; BidiCharacterType succSepType = this.runResolvedTypes[i + 1]; if ((prevSepType == BidiCharacterType.ArabicNumber && succSepType == BidiCharacterType.ArabicNumber) || (prevSepType == BidiCharacterType.EuropeanNumber && succSepType == BidiCharacterType.EuropeanNumber)) { // CS between (AN and AN) or (EN and EN) rt = prevSepType; } } } } // Rule W5 if (hasET && hasEN) { for (i = 0; i < this.runLength; ++i) { if (this.runResolvedTypes[i] == BidiCharacterType.EuropeanTerminator) { // Locate end of sequence int seqStart = i; int seqEnd = i; while (seqEnd < this.runLength && this.runResolvedTypes[seqEnd] == BidiCharacterType.EuropeanTerminator) { seqEnd++; } // Preceded by, or followed by EN? if ((seqStart == 0 ? sos : this.runResolvedTypes[seqStart - 1]) == BidiCharacterType.EuropeanNumber || (seqEnd == this.runLength ? eos : this.runResolvedTypes[seqEnd]) == BidiCharacterType.EuropeanNumber) { // Change the entire range for (int j = seqStart; i < seqEnd; ++i) { this.runResolvedTypes[i] = BidiCharacterType.EuropeanNumber; } } // continue at end of sequence i = seqEnd; } } } // Rule W6 if (hasES || hasET || hasCS) { for (i = 0; i < this.runLength; ++i) { ref BidiCharacterType t = ref this.runResolvedTypes[i]; if (t is BidiCharacterType.EuropeanSeparator or BidiCharacterType.EuropeanTerminator or BidiCharacterType.CommonSeparator) { t = BidiCharacterType.OtherNeutral; } } } // Rule W7. if (hasEN) { BidiCharacterType prevStrongType = sos; for (i = 0; i < this.runLength; ++i) { ref BidiCharacterType rt = ref this.runResolvedTypes[i]; if (rt == BidiCharacterType.EuropeanNumber) { // If prev strong type was an L change this to L too if (prevStrongType == BidiCharacterType.LeftToRight) { this.runResolvedTypes[i] = BidiCharacterType.LeftToRight; } } // Remember previous strong type (NB: AL should already be changed to R) if (rt is BidiCharacterType.LeftToRight or BidiCharacterType.RightToLeft) { prevStrongType = rt; } } } // Rule N0 - process bracket pairs if (this.hasBrackets) { int count; List? pairedBrackets = this.LocatePairedBrackets(); for (i = 0, count = pairedBrackets.Count; i < count; i++) { BracketPair pb = pairedBrackets[i]; BidiCharacterType dir = this.InspectPairedBracket(pb); // Case "d" - no strong types in the brackets, ignore if (dir == BidiCharacterType.OtherNeutral) { continue; } // Case "b" - strong type found that matches the embedding direction if ((dir == BidiCharacterType.LeftToRight || dir == BidiCharacterType.RightToLeft) && dir == this.runDirection) { this.SetPairedBracketDirection(pb, dir); continue; } // Case "c" - found opposite strong type found, look before to establish context dir = this.InspectBeforePairedBracket(pb, sos); if (dir == this.runDirection || dir == BidiCharacterType.OtherNeutral) { dir = this.runDirection; } this.SetPairedBracketDirection(pb, dir); } } // Rules N1 and N2 - resolve neutral types for (i = 0; i < this.runLength; ++i) { BidiCharacterType t = this.runResolvedTypes[i]; if (IsNeutralType(t)) { // Locate end of sequence int seqStart = i; int seqEnd = i; while (seqEnd < this.runLength && IsNeutralType(this.runResolvedTypes[seqEnd])) { seqEnd++; } // Work out the preceding type BidiCharacterType typeBefore; if (seqStart == 0) { typeBefore = sos; } else { typeBefore = this.runResolvedTypes[seqStart - 1]; if (typeBefore is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) { typeBefore = BidiCharacterType.RightToLeft; } } // Work out the following type BidiCharacterType typeAfter; if (seqEnd == this.runLength) { typeAfter = eos; } else { typeAfter = this.runResolvedTypes[seqEnd]; if (typeAfter is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) { typeAfter = BidiCharacterType.RightToLeft; } } // Work out the final resolved type BidiCharacterType resolvedType; if (typeBefore == typeAfter) { // Rule N1 resolvedType = typeBefore; } else { // Rule N2 resolvedType = this.runDirection; } // Apply changes for (int j = seqStart; j < seqEnd; j++) { this.runResolvedTypes[j] = resolvedType; } // continue after this run i = seqEnd; } } // Rules I1 and I2 - resolve implicit types if ((this.runLevel & 0x01) == 0) { // Rule I1 - even for (i = 0; i < this.runLength; i++) { BidiCharacterType t = this.runResolvedTypes[i]; ref sbyte l = ref this.runLevels[i]; if (t == BidiCharacterType.RightToLeft) { l++; } else if (t is BidiCharacterType.ArabicNumber or BidiCharacterType.EuropeanNumber) { l += 2; } } } else { // Rule I2 - odd for (i = 0; i < this.runLength; i++) { BidiCharacterType t = this.runResolvedTypes[i]; ref sbyte l = ref this.runLevels[i]; if (t != BidiCharacterType.RightToLeft) { l++; } } } } /// /// Locate all pair brackets in the current isolating run /// /// A sorted list of BracketPairs private List LocatePairedBrackets() { // Clear work collections this.pendingOpeningBrackets.Clear(); this.pairedBrackets.Clear(); // Since List.Sort is expensive on memory if called often (it internally // allocates an ArraySorted object) and since we will rarely have many // items in this list (most paragraphs will only have a handful of bracket // pairs - if that), we use a simple linear lookup and insert most of the // time. If there are more that `sortLimit` paired brackets we abort th // linear searching/inserting and using List.Sort at the end. const int sortLimit = 8; // Process all characters in the run, looking for paired brackets for (int ich = 0, length = this.runLength; ich < length; ich++) { // Ignore non-neutral characters if (this.runResolvedTypes[ich] != BidiCharacterType.OtherNeutral) { continue; } switch (this.runBidiPairedBracketTypes[ich]) { case BidiPairedBracketType.Open: if (this.pendingOpeningBrackets.Count == MaxPairedBracketDepth) { goto exit; } this.pendingOpeningBrackets.Insert(0, ich); break; case BidiPairedBracketType.Close: // see if there is a match for (int i = 0; i < this.pendingOpeningBrackets.Count; i++) { if (this.runPairedBracketValues[ich] == this.runPairedBracketValues[this.pendingOpeningBrackets[i]]) { // Add this paired bracket set int opener = this.pendingOpeningBrackets[i]; if (this.pairedBrackets.Count < sortLimit) { int ppi = 0; while (ppi < this.pairedBrackets.Count && this.pairedBrackets[ppi].OpeningIndex < opener) { ppi++; } this.pairedBrackets.Insert(ppi, new BracketPair(opener, ich)); } else { this.pairedBrackets.Add(new BracketPair(opener, ich)); } // remove up to and including matched opener this.pendingOpeningBrackets.RemoveRange(0, i + 1); break; } } break; } } exit: // Is a sort pending? if (this.pairedBrackets.Count > sortLimit) { this.pairedBrackets.Sort(); } return this.pairedBrackets; } /// /// Inspect a paired bracket set and determine its strong direction /// /// The paired bracket to be inspected /// The direction of the bracket set content private BidiCharacterType InspectPairedBracket(in BracketPair pb) { BidiCharacterType dirEmbed = DirectionFromLevel(this.runLevel); BidiCharacterType dirOpposite = BidiCharacterType.OtherNeutral; for (int ich = pb.OpeningIndex + 1; ich < pb.ClosingIndex; ich++) { BidiCharacterType dir = GetStrongTypeN0(this.runResolvedTypes[ich]); if (dir == BidiCharacterType.OtherNeutral) { continue; } if (dir == dirEmbed) { return dir; } dirOpposite = dir; } return dirOpposite; } /// /// Look for a strong type before a paired bracket /// /// The paired bracket set to be inspected /// The sos in case nothing found before the bracket /// The strong direction before the brackets private BidiCharacterType InspectBeforePairedBracket(in BracketPair pb, BidiCharacterType sos) { for (int ich = pb.OpeningIndex - 1; ich >= 0; --ich) { BidiCharacterType dir = GetStrongTypeN0(this.runResolvedTypes[ich]); if (dir != BidiCharacterType.OtherNeutral) { return dir; } } return sos; } /// /// Sets the direction of a bracket pair, including setting the direction of /// NSM's inside the brackets and following. /// /// The paired brackets /// The resolved direction for the bracket pair private void SetPairedBracketDirection(in BracketPair pb, BidiCharacterType dir) { // Set the direction of the brackets this.runResolvedTypes[pb.OpeningIndex] = dir; this.runResolvedTypes[pb.ClosingIndex] = dir; // Set the directionality of NSM's inside the brackets // BN characters (such as ZWJ or ZWSP) that appear between the base bracket character // and the nonspacing mark should be ignored. for (int i = pb.OpeningIndex + 1; i < pb.ClosingIndex; i++) { if (this.runOriginalTypes[i] == BidiCharacterType.NonspacingMark) { this.runResolvedTypes[i] = dir; } else if (this.runOriginalTypes[i] != BidiCharacterType.BoundaryNeutral) { break; } } // Set the directionality of NSM's following the brackets for (int i = pb.ClosingIndex + 1; i < this.runLength; i++) { if (this.runOriginalTypes[i] == BidiCharacterType.NonspacingMark) { this.runResolvedTypes[i] = dir; } else if (this.runOriginalTypes[i] != BidiCharacterType.BoundaryNeutral) { break; } } } /// /// Resets whitespace levels. Implements rule L1 /// private void ResetWhitespaceLevels() { for (int i = 0; i < this.ResolvedLevels.Length; i++) { BidiCharacterType t = this.originalTypes[i]; if (t is BidiCharacterType.ParagraphSeparator or BidiCharacterType.SegmentSeparator) { // Rule L1, clauses one and two. this.ResolvedLevels[i] = this.paragraphEmbeddingLevel; // Rule L1, clause three. for (int j = i - 1; j >= 0; --j) { if (IsWhitespace(this.originalTypes[j])) { // including format codes this.ResolvedLevels[j] = this.paragraphEmbeddingLevel; } else { break; } } } } // Rule L1, clause four. for (int j = this.ResolvedLevels.Length - 1; j >= 0; j--) { if (IsWhitespace(this.originalTypes[j])) { // including format codes this.ResolvedLevels[j] = this.paragraphEmbeddingLevel; } else { break; } } } /// /// Assign levels to any characters that would be have been /// removed by rule X9. The idea is to keep level runs together /// that would otherwise be broken by an interfering isolate/embedding /// control character. /// private void AssignLevelsToCodePointsRemovedByX9() { // Redundant? if (!this.hasIsolates && !this.hasEmbeddings) { return; } // No-op? if (this.workingTypes.Length == 0) { return; } // Fix up first character if (this.ResolvedLevels[0] < 0) { this.ResolvedLevels[0] = this.paragraphEmbeddingLevel; } if (IsRemovedByX9(this.originalTypes[0])) { this.workingTypes[0] = this.originalTypes[0]; } for (int i = 1, length = this.workingTypes.Length; i < length; i++) { BidiCharacterType t = this.originalTypes[i]; if (IsRemovedByX9(t)) { this.workingTypes[i] = t; this.ResolvedLevels[i] = this.ResolvedLevels[i - 1]; } } } /// /// Check if a directionality type represents whitespace /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsWhitespace(BidiCharacterType biditype) => biditype switch { BidiCharacterType.LeftToRightEmbedding or BidiCharacterType.RightToLeftEmbedding or BidiCharacterType.LeftToRightOverride or BidiCharacterType.RightToLeftOverride or BidiCharacterType.PopDirectionalFormat or BidiCharacterType.LeftToRightIsolate or BidiCharacterType.RightToLeftIsolate or BidiCharacterType.FirstStrongIsolate or BidiCharacterType.PopDirectionalIsolate or BidiCharacterType.BoundaryNeutral or BidiCharacterType.Whitespace => true, _ => false, }; /// /// Convert a level to a direction where odd is RTL and /// even is LTR /// /// The level to convert /// A directionality [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BidiCharacterType DirectionFromLevel(int level) => ((level & 0x1) == 0) ? BidiCharacterType.LeftToRight : BidiCharacterType.RightToLeft; /// /// Helper to check if a directionality is removed by rule X9 /// /// The bidi type to check /// True if rule X9 would remove this character; otherwise false [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsRemovedByX9(BidiCharacterType biditype) => biditype switch { BidiCharacterType.LeftToRightEmbedding or BidiCharacterType.RightToLeftEmbedding or BidiCharacterType.LeftToRightOverride or BidiCharacterType.RightToLeftOverride or BidiCharacterType.PopDirectionalFormat or BidiCharacterType.BoundaryNeutral => true, _ => false, }; /// /// Check if a a directionality is neutral for rules N1 and N2 /// /// The direction. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsNeutralType(BidiCharacterType dir) => dir switch { BidiCharacterType.ParagraphSeparator or BidiCharacterType.SegmentSeparator or BidiCharacterType.Whitespace or BidiCharacterType.OtherNeutral or BidiCharacterType.RightToLeftIsolate or BidiCharacterType.LeftToRightIsolate or BidiCharacterType.FirstStrongIsolate or BidiCharacterType.PopDirectionalIsolate => true, _ => false, }; /// /// Maps a direction to a strong type for rule N0 /// /// The direction to map /// A strong direction - R, L or ON [MethodImpl(MethodImplOptions.AggressiveInlining)] private static BidiCharacterType GetStrongTypeN0(BidiCharacterType dir) => dir switch { BidiCharacterType.EuropeanNumber or BidiCharacterType.ArabicNumber or BidiCharacterType.ArabicLetter or BidiCharacterType.RightToLeft => BidiCharacterType.RightToLeft, BidiCharacterType.LeftToRight => BidiCharacterType.LeftToRight, _ => BidiCharacterType.OtherNeutral, }; /// /// Hold the start and end index of a pair of brackets /// private readonly struct BracketPair : IComparable { /// /// Initializes a new instance of the struct. /// /// Index of the opening bracket /// Index of the closing bracket public BracketPair(int openingIndex, int closingIndex) { this.OpeningIndex = openingIndex; this.ClosingIndex = closingIndex; } /// /// Gets the index of the opening bracket /// public int OpeningIndex { get; } /// /// Gets the index of the closing bracket /// public int ClosingIndex { get; } public int CompareTo(BracketPair other) => this.OpeningIndex.CompareTo(other.OpeningIndex); } /// /// Status stack entry used while resolving explicit /// embedding levels /// private readonly struct Status { public Status(sbyte embeddingLevel, BidiCharacterType overrideStatus, bool isolateStatus) { this.EmbeddingLevel = embeddingLevel; this.OverrideStatus = overrideStatus; this.IsolateStatus = isolateStatus; } public sbyte EmbeddingLevel { get; } public BidiCharacterType OverrideStatus { get; } public bool IsolateStatus { get; } } /// /// Provides information about a level run - a continuous /// sequence of equal levels. /// private readonly struct LevelRun { public LevelRun(int start, int length, int level, BidiCharacterType sos, BidiCharacterType eos) { this.Start = start; this.Length = length; this.Level = level; this.Sos = sos; this.Eos = eos; } public int Start { get; } public int Length { get; } public int Level { get; } public BidiCharacterType Sos { get; } public BidiCharacterType Eos { get; } } } }