@@ -273,7 +273,8 @@ private struct MatrixHTMLParser { // swiftlint:disable:this type_body_length
273273 // Collapse whitespace in non-preformatted context.
274274 let collapsed = collapseWhitespace ( text)
275275 // Suppress whitespace-only text between block elements.
276- if collapsed. allSatisfy ( \. isWhitespace) && result. length > 0 {
276+ if collapsed. unicodeScalars. allSatisfy ( \. properties. isWhitespace)
277+ && result. length > 0 {
277278 let lastChar = result. attributedSubstring (
278279 from: NSRange ( location: result. length - 1 , length: 1 )
279280 ) . string
@@ -749,28 +750,39 @@ private struct MatrixHTMLParser { // swiftlint:disable:this type_body_length
749750
750751 /// Tokenizes HTML into a sequence of text, open-tag, and close-tag tokens.
751752 /// Handles entity decoding, attribute parsing, and self-closing tags.
753+ ///
754+ /// Scans using the string's `unicodeScalars` view so that combining marks
755+ /// (e.g. U+0E4B) following `<` or `>` are not merged into a single
756+ /// grapheme cluster with the delimiter, which would cause `Character`-level
757+ /// comparisons like `firstIndex(of: ">")` to fail.
752758 private func tokenize( _ html: String ) -> [ Token ] {
753759 var tokens : [ Token ] = [ ]
754- var index = html. startIndex
760+ let scalars = html. unicodeScalars
761+ var index = scalars. startIndex
755762
756- while index < html . endIndex {
757- if html [ index] == " < " {
763+ while index < scalars . endIndex {
764+ if scalars [ index] == " < " {
758765 // Try to parse a tag.
759- if let tagEnd = html [ index... ] . firstIndex ( of: " > " ) {
760- let tagContent = html [ html. index ( after: index) ..< tagEnd]
761- let tagString = String ( tagContent) . trimmingCharacters ( in: . whitespaces)
766+ let afterLT = scalars. index ( after: index)
767+ if let tagEnd = scalars [ afterLT... ] . firstIndex ( of: " > " ) {
768+ let tagContent = String ( scalars [ afterLT..< tagEnd] )
769+ let tagString = tagContent. trimmingCharacters ( in: . whitespaces)
762770
763771 if tagString. hasPrefix ( " !-- " ) {
764772 // HTML comment — skip entirely.
765- if let commentEnd = html [ index... ] . range ( of: " --> " ) {
766- index = commentEnd. upperBound
773+ if let commentEnd = String ( scalars [ index... ] ) . range ( of: " --> " ) {
774+ let offset = String ( scalars [ index... ] ) . distance (
775+ from: String ( scalars [ index... ] ) . startIndex,
776+ to: commentEnd. upperBound
777+ )
778+ index = scalars. index ( index, offsetBy: offset)
767779 } else {
768- index = html . endIndex
780+ index = scalars . endIndex
769781 }
770782 continue
771783 } else if tagString. hasPrefix ( " ! " ) || tagString. hasPrefix ( " ? " ) {
772784 // Doctype or processing instruction — skip.
773- index = html . index ( after: tagEnd)
785+ index = scalars . index ( after: tagEnd)
774786 continue
775787 } else if tagString. hasPrefix ( " / " ) {
776788 // Close tag.
@@ -792,19 +804,19 @@ private struct MatrixHTMLParser { // swiftlint:disable:this type_body_length
792804 // Void tags don't get a close token — handled by not pushing style.
793805 }
794806 }
795- index = html . index ( after: tagEnd)
807+ index = scalars . index ( after: tagEnd)
796808 } else {
797809 // Malformed: < without matching >. Treat as text.
798- tokens. append ( . text( String ( html [ index] ) ) )
799- index = html . index ( after: index)
810+ tokens. append ( . text( String ( scalars [ index] ) ) )
811+ index = scalars . index ( after: index)
800812 }
801813 } else {
802814 // Accumulate text until the next tag.
803815 var textEnd = index
804- while textEnd < html . endIndex && html [ textEnd] != " < " {
805- textEnd = html . index ( after: textEnd)
816+ while textEnd < scalars . endIndex && scalars [ textEnd] != " < " {
817+ textEnd = scalars . index ( after: textEnd)
806818 }
807- let rawText = String ( html [ index..< textEnd] )
819+ let rawText = String ( scalars [ index..< textEnd] )
808820 let decoded = decodeHTMLEntities ( rawText)
809821 tokens. append ( . text( decoded) )
810822 index = textEnd
@@ -981,17 +993,23 @@ private struct MatrixHTMLParser { // swiftlint:disable:this type_body_length
981993 // MARK: - Whitespace Helpers
982994
983995 /// Collapses runs of whitespace into single spaces (HTML whitespace normalization).
996+ ///
997+ /// Operates on unicode scalars rather than grapheme clusters so that
998+ /// combining marks (e.g. U+0E4B Thai Mai Chattawa) following a space
999+ /// are preserved. Swift groups a space + combining mark into a single
1000+ /// `Character` whose `.isWhitespace` returns `true`, which would
1001+ /// silently discard the combining mark.
9841002 private func collapseWhitespace( _ text: String ) -> String {
9851003 var result = " "
9861004 var lastWasSpace = false
987- for char in text {
988- if char . isWhitespace || char . isNewline {
1005+ for scalar in text. unicodeScalars {
1006+ if scalar . properties . isWhitespace || scalar == " \n " {
9891007 if !lastWasSpace {
9901008 result. append ( " " )
9911009 lastWasSpace = true
9921010 }
9931011 } else {
994- result. append ( char )
1012+ result. unicodeScalars . append ( scalar )
9951013 lastWasSpace = false
9961014 }
9971015 }
0 commit comments