reordering.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { getBidiCharType, TRAILING_TYPES } from './charTypes.js'
  2. import { getMirroredCharacter } from './mirroring.js'
  3. /**
  4. * Given a start and end denoting a single line within a string, and a set of precalculated
  5. * bidi embedding levels, produce a list of segments whose ordering should be flipped, in sequence.
  6. * @param {string} string - the full input string
  7. * @param {GetEmbeddingLevelsResult} embeddingLevelsResult - the result object from getEmbeddingLevels
  8. * @param {number} [start] - first character in a subset of the full string
  9. * @param {number} [end] - last character in a subset of the full string
  10. * @return {number[][]} - the list of start/end segments that should be flipped, in order.
  11. */
  12. export function getReorderSegments(string, embeddingLevelsResult, start, end) {
  13. let strLen = string.length
  14. start = Math.max(0, start == null ? 0 : +start)
  15. end = Math.min(strLen - 1, end == null ? strLen - 1 : +end)
  16. const segments = []
  17. embeddingLevelsResult.paragraphs.forEach(paragraph => {
  18. const lineStart = Math.max(start, paragraph.start)
  19. const lineEnd = Math.min(end, paragraph.end)
  20. if (lineStart < lineEnd) {
  21. // Local slice for mutation
  22. const lineLevels = embeddingLevelsResult.levels.slice(lineStart, lineEnd + 1)
  23. // 3.4 L1.4: Reset any sequence of whitespace characters and/or isolate formatting characters at the
  24. // end of the line to the paragraph level.
  25. for (let i = lineEnd; i >= lineStart && (getBidiCharType(string[i]) & TRAILING_TYPES); i--) {
  26. lineLevels[i] = paragraph.level
  27. }
  28. // L2. From the highest level found in the text to the lowest odd level on each line, including intermediate levels
  29. // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
  30. let maxLevel = paragraph.level
  31. let minOddLevel = Infinity
  32. for (let i = 0; i < lineLevels.length; i++) {
  33. const level = lineLevels[i]
  34. if (level > maxLevel) maxLevel = level
  35. if (level < minOddLevel) minOddLevel = level | 1
  36. }
  37. for (let lvl = maxLevel; lvl >= minOddLevel; lvl--) {
  38. for (let i = 0; i < lineLevels.length; i++) {
  39. if (lineLevels[i] >= lvl) {
  40. const segStart = i
  41. while (i + 1 < lineLevels.length && lineLevels[i + 1] >= lvl) {
  42. i++
  43. }
  44. if (i > segStart) {
  45. segments.push([segStart + lineStart, i + lineStart])
  46. }
  47. }
  48. }
  49. }
  50. }
  51. })
  52. return segments
  53. }
  54. /**
  55. * @param {string} string
  56. * @param {GetEmbeddingLevelsResult} embedLevelsResult
  57. * @param {number} [start]
  58. * @param {number} [end]
  59. * @return {string} the new string with bidi segments reordered
  60. */
  61. export function getReorderedString(string, embedLevelsResult, start, end) {
  62. const indices = getReorderedIndices(string, embedLevelsResult, start, end)
  63. const chars = [...string]
  64. indices.forEach((charIndex, i) => {
  65. chars[i] = (
  66. (embedLevelsResult.levels[charIndex] & 1) ? getMirroredCharacter(string[charIndex]) : null
  67. ) || string[charIndex]
  68. })
  69. return chars.join('')
  70. }
  71. /**
  72. * @param {string} string
  73. * @param {GetEmbeddingLevelsResult} embedLevelsResult
  74. * @param {number} [start]
  75. * @param {number} [end]
  76. * @return {number[]} an array with character indices in their new bidi order
  77. */
  78. export function getReorderedIndices(string, embedLevelsResult, start, end) {
  79. const segments = getReorderSegments(string, embedLevelsResult, start, end)
  80. // Fill an array with indices
  81. const indices = []
  82. for (let i = 0; i < string.length; i++) {
  83. indices[i] = i
  84. }
  85. // Reverse each segment in order
  86. segments.forEach(([start, end]) => {
  87. const slice = indices.slice(start, end + 1)
  88. for (let i = slice.length; i--;) {
  89. indices[end - i] = slice[i]
  90. }
  91. })
  92. return indices
  93. }