diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift index e8a54ec1102..25c1828deb3 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorInputView.swift @@ -10,6 +10,7 @@ protocol WKEditorInputViewDelegate: AnyObject { func didTapNumberList(isSelected: Bool) func didTapIncreaseIndent() func didTapDecreaseIndent() + func didTapHeading(type: WKEditorInputView.HeadingButtonType) func didTapStrikethrough(isSelected: Bool) } @@ -225,6 +226,18 @@ class WKEditorInputView: WKComponentView { ]) updateColors() + + NotificationCenter.default.addObserver(self, selector: #selector(updateButtonSelectionState(_:)), name: Notification.WKSourceEditorSelectionState, object: nil) + } + + // MARK: - Notifications + + @objc private func updateButtonSelectionState(_ notification: NSNotification) { + guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { + return + } + + configure(selectionState: selectionState) } // MARK: - Overrides @@ -287,20 +300,40 @@ class WKEditorInputView: WKComponentView { switch type { case .paragraph: - paragraphButton.isSelected.toggle() + paragraphButton.isSelected = true case .heading: - headerButton.isSelected.toggle() + headerButton.isSelected = true case .subheading1: - subheader1Button.isSelected.toggle() + subheader1Button.isSelected = true case .subheading2: - subheader2Button.isSelected.toggle() + subheader2Button.isSelected = true case .subheading3: - subheader3Button.isSelected.toggle() + subheader3Button.isSelected = true case .subheading4: - subheader4Button.isSelected.toggle() + subheader4Button.isSelected = true } + + delegate?.didTapHeading(type: type) }) return UIButton(configuration: configuration, primaryAction: action) } + + func configure(selectionState: WKSourceEditorSelectionState) { + headingButtons.forEach { $0.isSelected = false } + + if selectionState.isHeading { + headerButton.isSelected = true + } else if selectionState.isSubheading1 { + subheader1Button.isSelected = true + } else if selectionState.isSubheading2 { + subheader2Button.isSelected = true + } else if selectionState.isSubheading3 { + subheader3Button.isSelected = true + } else if selectionState.isSubheading4 { + subheader4Button.isSelected = true + } else { + paragraphButton.isSelected = true + } + } } diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift index 2095711d9fa..ce9f1869e4e 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatter+ButtonActions.swift @@ -79,7 +79,7 @@ extension WKSourceEditorFormatter { // MARK: - Expanding selected range methods - private func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { if let textPositions = textPositionsCloserToNearestFormattingStrings(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView) { textView.selectedTextRange = textView.textRange(from: textPositions.startPosition, to: textPositions.endPosition) } @@ -152,7 +152,7 @@ extension WKSourceEditorFormatter { // MARK: - Nearby formatting string determination - private func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool { + func selectedRangeIsSurroundedByFormattingString(formattingString: String, in textView: UITextView) -> Bool { selectedRangeIsSurroundedByFormattingStrings(startingFormattingString: formattingString, endingFormattingString: formattingString, in: textView) } @@ -174,7 +174,7 @@ extension WKSourceEditorFormatter { return startingString == formattingString } - private func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool { + func rangeIsFollowedByFormattingString(range: UITextRange?, formattingString: String, in textView: UITextView) -> Bool { guard let range = range, let newEnd = textView.position(from: range.end, offset: formattingString.count) else { return false @@ -190,7 +190,7 @@ extension WKSourceEditorFormatter { // MARK: Adding and removing text - private func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func addStringFormattingCharacters(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { let startingCursorOffset = startingFormattingString.count let endingCursorOffset = endingFormattingString.count @@ -217,7 +217,7 @@ extension WKSourceEditorFormatter { } } - private func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { + func removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: String, endingFormattingString: String, in textView: UITextView) { guard let originalSelectedTextRange = textView.selectedTextRange, let formattingTextStart = textView.position(from: originalSelectedTextRange.start, offset: -startingFormattingString.count), diff --git a/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift new file mode 100644 index 00000000000..869c6502bcd --- /dev/null +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterHeading+ButtonActions.swift @@ -0,0 +1,54 @@ +import Foundation +import ComponentsObjC + +extension WKSourceEditorFormatterHeading { + func toggleHeadingFormatting(selectedHeading: WKEditorInputView.HeadingButtonType, currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + var currentStateIsParagraph = false + if currentSelectionState.isHeading { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "==", endingFormattingString: "==", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "==", endingFormattingString: "==", in: textView) + } else if currentSelectionState.isSubheading1 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "===", endingFormattingString: "===", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "===", endingFormattingString: "===", in: textView) + } else if currentSelectionState.isSubheading2 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "====", endingFormattingString: "====", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "====", endingFormattingString: "====", in: textView) + } else if currentSelectionState.isSubheading3 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "=====", endingFormattingString: "=====", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "=====", endingFormattingString: "=====", in: textView) + } else if currentSelectionState.isSubheading4 { + expandSelectedRangeUpToNearestFormattingStrings(startingFormattingString: "======", endingFormattingString: "======", in: textView) + removeSurroundingFormattingStringsFromSelectedRange(startingFormattingString: "======", endingFormattingString: "======", in: textView) + } else { + currentStateIsParagraph = true + } + + let currentlySurroundedByLineBreaks = selectedRangeIsSurroundedByFormattingString(formattingString: "\n", in: textView) || textView.selectedRange.location == 0 && rangeIsFollowedByFormattingString(range: textView.selectedTextRange, formattingString: "\n", in: textView) + + let surroundingLineBreak = currentStateIsParagraph && !currentlySurroundedByLineBreaks ? "\n" : "" + let startingFormattingString: String + let endingFormattingString: String + switch selectedHeading { + case .paragraph: + return + case .heading: + startingFormattingString = surroundingLineBreak + "==" + endingFormattingString = "==" + surroundingLineBreak + case .subheading1: + startingFormattingString = surroundingLineBreak + "===" + endingFormattingString = "===" + surroundingLineBreak + case .subheading2: + startingFormattingString = surroundingLineBreak + "====" + endingFormattingString = "====" + surroundingLineBreak + case .subheading3: + startingFormattingString = surroundingLineBreak + "=====" + endingFormattingString = "=====" + surroundingLineBreak + case .subheading4: + startingFormattingString = surroundingLineBreak + "======" + endingFormattingString = "======" + surroundingLineBreak + } + + addStringFormattingCharacters(startingFormattingString: startingFormattingString, endingFormattingString: endingFormattingString, in: textView) + } +} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index d1af809454f..24893b850f1 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -21,9 +21,14 @@ fileprivate var needsTextKit2: Bool { let isBulletMultipleList: Bool let isNumberSingleList: Bool let isNumberMultipleList: Bool + let isHeading: Bool + let isSubheading1: Bool + let isSubheading2: Bool + let isSubheading3: Bool + let isSubheading4: Bool let isStrikethrough: Bool - - init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isBulletSingleList: Bool, isBulletMultipleList: Bool, isNumberSingleList: Bool, isNumberMultipleList: Bool, isStrikethrough: Bool) { + + init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isBulletSingleList: Bool, isBulletMultipleList: Bool, isNumberSingleList: Bool, isNumberMultipleList: Bool, isHeading: Bool, isSubheading1: Bool, isSubheading2: Bool, isSubheading3: Bool, isSubheading4: Bool, isStrikethrough: Bool) { self.isBold = isBold self.isItalics = isItalics self.isHorizontalTemplate = isHorizontalTemplate @@ -31,6 +36,11 @@ fileprivate var needsTextKit2: Bool { self.isBulletMultipleList = isBulletMultipleList self.isNumberSingleList = isNumberSingleList self.isNumberMultipleList = isNumberMultipleList + self.isHeading = isHeading + self.isSubheading1 = isSubheading1 + self.isSubheading2 = isSubheading2 + self.isSubheading3 = isSubheading3 + self.isSubheading4 = isSubheading4 self.isStrikethrough = isStrikethrough } } @@ -46,6 +56,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { private(set) var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics? private(set) var templateFormatter: WKSourceEditorFormatterTemplate? private(set) var listFormatter: WKSourceEditorFormatterList? + private(set) var headingFormatter: WKSourceEditorFormatterHeading? private(set) var strikethroughFormatter: WKSourceEditorFormatterStrikethrough? var isSyntaxHighlightingEnabled: Bool = true { @@ -116,16 +127,18 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) let listFormatter = WKSourceEditorFormatterList(colors: colors, fonts: fonts) + let headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts) let strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts) self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment), templateFormatter, boldItalicsFormatter, listFormatter, + headingFormatter, strikethroughFormatter] - self.boldItalicsFormatter = boldItalicsFormatter self.templateFormatter = templateFormatter self.listFormatter = listFormatter + self.headingFormatter = headingFormatter self.strikethroughFormatter = strikethroughFormatter if needsTextKit2 { @@ -166,7 +179,7 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { if needsTextKit2 { guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isStrikethrough: false) + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false) } let isBold = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBoldIn: textKit2Data.paragraphSelectedRange) ?? false @@ -176,14 +189,19 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let isBulletMultipleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBulletMultipleIn: textKit2Data.paragraphSelectedRange) ?? false let isNumberSingleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isNumberSingleIn: textKit2Data.paragraphSelectedRange) ?? false let isNumberMultipleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isNumberMultipleIn: textKit2Data.paragraphSelectedRange) ?? false + let isHeading = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHeadingIn: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading1 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading1In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading2 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading2In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading3 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading3In: textKit2Data.paragraphSelectedRange) ?? false + let isSubheading4 = headingFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSubheading4In: textKit2Data.paragraphSelectedRange) ?? false let isStrikethrough = strikethroughFormatter?.attributedString(textKit2Data.paragraphAttributedString, isStrikethroughIn: textKit2Data.paragraphSelectedRange) ?? false - - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isStrikethrough: isStrikethrough) + + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough) } else { guard let textKit1Storage else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isStrikethrough: false) - } - + return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isBulletSingleList: false, isBulletMultipleList: false, isNumberSingleList: false, isNumberMultipleList: false, isHeading: false, isSubheading1: false, isSubheading2: false, isSubheading3: false, isSubheading4: false, isStrikethrough: false) + } + let isBold = boldItalicsFormatter?.attributedString(textKit1Storage, isBoldIn: selectedDocumentRange) ?? false let isItalics = boldItalicsFormatter?.attributedString(textKit1Storage, isItalicsIn: selectedDocumentRange) ?? false let isHorizontalTemplate = templateFormatter?.attributedString(textKit1Storage, isHorizontalTemplateIn: selectedDocumentRange) ?? false @@ -191,9 +209,14 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let isBulletMultipleList = listFormatter?.attributedString(textKit1Storage, isBulletMultipleIn: selectedDocumentRange) ?? false let isNumberSingleList = listFormatter?.attributedString(textKit1Storage, isNumberSingleIn: selectedDocumentRange) ?? false let isNumberMultipleList = listFormatter?.attributedString(textKit1Storage, isNumberMultipleIn: selectedDocumentRange) ?? false + let isHeading = headingFormatter?.attributedString(textKit1Storage, isHeadingIn: selectedDocumentRange) ?? false + let isSubheading1 = headingFormatter?.attributedString(textKit1Storage, isSubheading1In: selectedDocumentRange) ?? false + let isSubheading2 = headingFormatter?.attributedString(textKit1Storage, isSubheading2In: selectedDocumentRange) ?? false + let isSubheading3 = headingFormatter?.attributedString(textKit1Storage, isSubheading3In: selectedDocumentRange) ?? false + let isSubheading4 = headingFormatter?.attributedString(textKit1Storage, isSubheading4In: selectedDocumentRange) ?? false let isStrikethrough = strikethroughFormatter?.attributedString(textKit1Storage, isStrikethroughIn: selectedDocumentRange) ?? false - - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isStrikethrough: isStrikethrough) + + return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isBulletSingleList: isBulletSingleList, isBulletMultipleList: isBulletMultipleList, isNumberSingleList: isNumberSingleList, isNumberMultipleList: isNumberMultipleList, isHeading: isHeading, isSubheading1: isSubheading1, isSubheading2: isSubheading2, isSubheading3: isSubheading3, isSubheading4: isSubheading4, isStrikethrough: isStrikethrough) } } @@ -243,6 +266,11 @@ extension WKSourceEditorTextFrameworkMediator: WKSourceEditorStorageDelegate { fonts.boldFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldBody, compatibleWith: traitCollection) : baseFont fonts.italicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.italicsBody, compatibleWith: traitCollection) : baseFont fonts.boldItalicsFont = isSyntaxHighlightingEnabled ? WKFont.for(.boldItalicsBody, compatibleWith: traitCollection) : baseFont + fonts.headingFont = isSyntaxHighlightingEnabled ? WKFont.for(.editorHeading, compatibleWith: traitCollection) : baseFont + fonts.subheading1Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading1, compatibleWith: traitCollection) : baseFont + fonts.subheading2Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading2, compatibleWith: traitCollection) : baseFont + fonts.subheading3Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading3, compatibleWith: traitCollection) : baseFont + fonts.subheading4Font = isSyntaxHighlightingEnabled ? WKFont.for(.editorSubheading4, compatibleWith: traitCollection) : baseFont return fonts } } diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift index fb0a28569e9..d876ce9501d 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -309,6 +309,9 @@ extension WKSourceEditorViewController: WKEditorToolbarHighlightViewDelegate { // MARK: - WKEditorInputViewDelegate extension WKSourceEditorViewController: WKEditorInputViewDelegate { + func didTapHeading(type: WKEditorInputView.HeadingButtonType) { + textFrameworkMediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: type, currentSelectionState: selectionState(), textView: textView) + } func didTapBold(isSelected: Bool) { let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add diff --git a/Components/Sources/Components/Style/WKFont.swift b/Components/Sources/Components/Style/WKFont.swift index 1314ee54e95..fd8b50b5d1b 100644 --- a/Components/Sources/Components/Style/WKFont.swift +++ b/Components/Sources/Components/Style/WKFont.swift @@ -6,69 +6,85 @@ public enum WKFont { case headline case title case boldTitle - case body + case body case boldBody case italicsBody case boldItalicsBody - case smallBody - case callout - case subheadline - case boldSubheadline + case smallBody + case callout + case subheadline + case boldSubheadline case mediumSubheadline case caption1 case footnote - case boldFootnote + case boldFootnote + case editorHeading + case editorSubheading1 + case editorSubheading2 + case editorSubheading3 + case editorSubheading4 - static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { - switch font { - case .headline: - return UIFont.preferredFont(forTextStyle: .headline, compatibleWith: traitCollection) - case .title: - return UIFont.preferredFont(forTextStyle: .title1, compatibleWith: traitCollection) - case .boldTitle: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .body: - return UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection) - case .boldBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .italicsBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitItalic) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .boldItalicsBody: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold.union(.traitItalic)) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .smallBody: - return UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .regular)) - case .callout: - return UIFont.preferredFont(forTextStyle: .callout, compatibleWith: traitCollection) - case .subheadline: - return UIFont.preferredFont(forTextStyle: .subheadline, compatibleWith: traitCollection) - case .mediumSubheadline: - return UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .medium)) - case .boldSubheadline: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - case .caption1: - return UIFont.preferredFont(forTextStyle: .caption1, compatibleWith: traitCollection) - case .footnote: - return UIFont.preferredFont(forTextStyle: .footnote, compatibleWith: traitCollection) - case .boldFootnote: - guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .footnote, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { - fatalError() - } - return UIFont(descriptor: descriptor, size: 0) - } + static func `for`(_ font: WKFont, compatibleWith traitCollection: UITraitCollection = WKAppEnvironment.current.traitCollection) -> UIFont { + switch font { + case .headline: + return UIFont.preferredFont(forTextStyle: .headline, compatibleWith: traitCollection) + case .title: + return UIFont.preferredFont(forTextStyle: .title1, compatibleWith: traitCollection) + case .boldTitle: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .body: + return UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection) + case .boldBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .italicsBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitItalic) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .boldItalicsBody: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body, compatibleWith: traitCollection).withSymbolicTraits(.traitBold.union(.traitItalic)) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .smallBody: + return UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .regular)) + case .callout: + return UIFont.preferredFont(forTextStyle: .callout, compatibleWith: traitCollection) + case .subheadline: + return UIFont.preferredFont(forTextStyle: .subheadline, compatibleWith: traitCollection) + case .mediumSubheadline: + return UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: UIFont.systemFont(ofSize: 15, weight: .medium)) + case .boldSubheadline: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .caption1: + return UIFont.preferredFont(forTextStyle: .caption1, compatibleWith: traitCollection) + case .footnote: + return UIFont.preferredFont(forTextStyle: .footnote, compatibleWith: traitCollection) + case .boldFootnote: + guard let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .footnote, compatibleWith: traitCollection).withSymbolicTraits(.traitBold) else { + fatalError() + } + return UIFont(descriptor: descriptor, size: 0) + case .editorHeading: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 28, weight: .semibold), maximumPointSize: 32, compatibleWith: traitCollection) + case .editorSubheading1: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 26, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading2: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 24, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading3: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 22, weight: .semibold), compatibleWith: traitCollection) + case .editorSubheading4: + return UIFontMetrics(forTextStyle: .headline).scaledFont(for: UIFont.systemFont(ofSize: 20, weight: .semibold), compatibleWith: traitCollection) + } + } } diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h b/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h index e7be6dd21f7..a5bdf06e53f 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFonts.h @@ -7,6 +7,11 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) UIFont *boldFont; @property (nonatomic, strong) UIFont *italicsFont; @property (nonatomic, strong) UIFont *boldItalicsFont; +@property (nonatomic, strong) UIFont *headingFont; +@property (nonatomic, strong) UIFont *subheading1Font; +@property (nonatomic, strong) UIFont *subheading2Font; +@property (nonatomic, strong) UIFont *subheading3Font; +@property (nonatomic, strong) UIFont *subheading4Font; @end NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m index 19042761268..ea4e1d64297 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m @@ -34,6 +34,7 @@ - (void)addSyntaxHighlightingToAttributedString:(NSMutableAttributedString *)att [attributedString removeAttribute:NSForegroundColorAttributeName range:range]; // reset shared custom attributes + [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyColorGreen range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m index 58b608816f0..fd12af389ff 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m @@ -59,7 +59,6 @@ - (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEdi - (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { // Reset - [attributedString removeAttribute:WKSourceEditorCustomKeyColorOrange range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontBoldItalics range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontBold range:range]; [attributedString removeAttribute:WKSourceEditorCustomKeyFontItalics range:range]; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h new file mode 100644 index 00000000000..78a6013e552 --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.h @@ -0,0 +1,15 @@ +#import "WKSourceEditorFormatter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface WKSourceEditorFormatterHeading : WKSourceEditorFormatter + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isHeadingInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading1InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading2InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading3InRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading4InRange:(NSRange)range; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m new file mode 100644 index 00000000000..0d36a57eaac --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterHeading.m @@ -0,0 +1,342 @@ +#import "WKSourceEditorFormatterHeading.h" +#import "WKSourceEditorColors.h" +#import "WKSourceEditorFonts.h" + +@interface WKSourceEditorFormatterHeading () +@property (nonatomic, strong) NSDictionary *headingFontAttributes; +@property (nonatomic, strong) NSDictionary *subheading1FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading2FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading3FontAttributes; +@property (nonatomic, strong) NSDictionary *subheading4FontAttributes; +@property (nonatomic, strong) NSDictionary *orangeAttributes; + +@property (nonatomic, strong) NSDictionary *headingContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading1ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading2ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading3ContentAttributes; +@property (nonatomic, strong) NSDictionary *subheading4ContentAttributes; + +@property (nonatomic, strong) NSRegularExpression *headingRegex; +@property (nonatomic, strong) NSRegularExpression *subheading1Regex; +@property (nonatomic, strong) NSRegularExpression *subheading2Regex; +@property (nonatomic, strong) NSRegularExpression *subheading3Regex; +@property (nonatomic, strong) NSRegularExpression *subheading4Regex; +@end + +@implementation WKSourceEditorFormatterHeading + +#pragma mark - Custom Attributed String Keys + +// Font custom keys span across entire match, i.e. "== Test ==". The entire match is a particular font. This helps us quickly seek and update fonts upon popover change. +NSString * const WKSourceEditorCustomKeyFontHeading = @"WKSourceEditorCustomKeyFontHeading"; +NSString * const WKSourceEditorCustomKeyFontSubheading1 = @"WKSourceEditorCustomKeyFontSubheading1"; +NSString * const WKSourceEditorCustomKeyFontSubheading2 = @"WKSourceEditorCustomKeyFontSubheading2"; +NSString * const WKSourceEditorCustomKeyFontSubheading3 = @"WKSourceEditorCustomKeyFontSubheading3"; +NSString * const WKSourceEditorCustomKeyFontSubheading4 = @"WKSourceEditorCustomKeyFontSubheading4"; + +// Content custom keys span across only the content, i.e. " Test ". This helps us detect for button selection states. +NSString * const WKSourceEditorCustomKeyContentHeading = @"WKSourceEditorCustomKeyContentHeading"; +NSString * const WKSourceEditorCustomKeyContentSubheading1 = @"WKSourceEditorCustomKeyContentSubheading1"; +NSString * const WKSourceEditorCustomKeyContentSubheading2 = @"WKSourceEditorCustomKeyContentSubheading2"; +NSString * const WKSourceEditorCustomKeyContentSubheading3 = @"WKSourceEditorCustomKeyContentSubheading3"; +NSString * const WKSourceEditorCustomKeyContentSubheading4 = @"WKSourceEditorCustomKeyContentSubheading4"; + +#pragma mark - Public + +- (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEditorFonts *)fonts { + self = [super initWithColors:colors fonts:fonts]; + if (self) { + _orangeAttributes = @{ + NSForegroundColorAttributeName: colors.orangeForegroundColor, + WKSourceEditorCustomKeyColorOrange: [NSNumber numberWithBool:YES] + }; + + _headingFontAttributes = @{ + NSFontAttributeName: fonts.headingFont, + WKSourceEditorCustomKeyFontHeading: [NSNumber numberWithBool:YES] + }; + + _subheading1FontAttributes = @{ + NSFontAttributeName: fonts.subheading1Font, + WKSourceEditorCustomKeyFontSubheading1: [NSNumber numberWithBool:YES] + }; + + _subheading2FontAttributes = @{ + NSFontAttributeName: fonts.subheading2Font, + WKSourceEditorCustomKeyFontSubheading2: [NSNumber numberWithBool:YES] + }; + + _subheading3FontAttributes = @{ + NSFontAttributeName: fonts.subheading3Font, + WKSourceEditorCustomKeyFontSubheading3: [NSNumber numberWithBool:YES] + }; + + _subheading4FontAttributes = @{ + NSFontAttributeName: fonts.subheading4Font, + WKSourceEditorCustomKeyFontSubheading4: [NSNumber numberWithBool:YES] + }; + + _headingContentAttributes = @{ + WKSourceEditorCustomKeyContentHeading: [NSNumber numberWithBool:YES] + }; + + _subheading1ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading1: [NSNumber numberWithBool:YES] + }; + + _subheading2ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading2: [NSNumber numberWithBool:YES] + }; + + _subheading3ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading3: [NSNumber numberWithBool:YES] + }; + + _subheading4ContentAttributes = @{ + WKSourceEditorCustomKeyContentSubheading4: [NSNumber numberWithBool:YES] + }; + + _headingRegex = [[NSRegularExpression alloc] initWithPattern:@"^(={2})([^=]*)(={2})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading1Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={3})([^=]*)(={3})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading2Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={4})([^=]*)(={4})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading3Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={5})([^=]*)(={5})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _subheading4Regex = [[NSRegularExpression alloc] initWithPattern:@"^(={6})([^=]*)(={6})(?!=)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + } + return self; +} + +#pragma mark - Overrides + +- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + // Reset + [attributedString removeAttribute:WKSourceEditorCustomKeyFontHeading range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading1 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading2 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading3 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyFontSubheading4 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentHeading range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading1 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading2 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading3 range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentSubheading4 range:range]; + + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.headingRegex fontAttributes:self.headingFontAttributes contentAttributes:self.headingContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading1Regex fontAttributes:self.subheading1FontAttributes contentAttributes:self.subheading1ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading2Regex fontAttributes:self.subheading2FontAttributes contentAttributes:self.subheading2ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading3Regex fontAttributes:self.subheading3FontAttributes contentAttributes:self.subheading3ContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range regex:self.subheading4Regex fontAttributes:self.subheading4FontAttributes contentAttributes:self.subheading4ContentAttributes]; +} + +- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [self updateColorAttributesWithColors:colors]; + [self enumerateAndUpdateColorsInAttributedString:attributedString range:range]; +} + +- (void)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [self updateFontAttributesWithFonts:fonts]; + [self enumerateAndUpdateFontsInAttributedString:attributedString range:range]; +} + +#pragma mark - Public + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isHeadingInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentHeading inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading1InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading1 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading2InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading2 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading3InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading3 inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isSubheading4InRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentSubheading4 inRange:range]; +} + +#pragma mark - Private + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isContentKey:(NSString *)contentKey inRange:(NSRange)range { + __block BOOL isContentKey = NO; + + if (range.length == 0) { + + if (attributedString.length > range.location) { + NSDictionary *attrs = [attributedString attributesAtIndex:range.location effectiveRange:nil]; + + if (attrs[contentKey] != nil) { + isContentKey = YES; + } else { + // Edge case, check previous character if we are up against closing string + if (attrs[WKSourceEditorCustomKeyColorOrange]) { + attrs = [attributedString attributesAtIndex:range.location - 1 effectiveRange:nil]; + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + } + } + } + + } else { + __block NSRange unionRange = NSMakeRange(NSNotFound, 0); + [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) { + if (attrs[contentKey] != nil) { + if (unionRange.location == NSNotFound) { + unionRange = loopRange; + } else { + unionRange = NSUnionRange(unionRange, loopRange); + } + stop = YES; + } + }]; + + if (NSEqualRanges(unionRange, range)) { + isContentKey = YES; + } + } + + return isContentKey; +} + +- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range regex:(NSRegularExpression *)regex fontAttributes:(NSDictionary *)fontAttributes contentAttributes:(NSDictionary *)contentAttributes { + + [regex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange openingRange = [result rangeAtIndex:1]; + NSRange textRange = [result rangeAtIndex:2]; + NSRange closingRange = [result rangeAtIndex:3]; + + if (fullMatch.location != NSNotFound) { + [attributedString addAttributes:fontAttributes range:fullMatch]; + } + + if (openingRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:openingRange]; + } + + if (textRange.location != NSNotFound) { + [attributedString addAttributes:contentAttributes range:textRange]; + } + + if (closingRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:closingRange]; + } + }]; +} + +- (void)updateColorAttributesWithColors: (WKSourceEditorColors *)colors { + NSMutableDictionary *mutAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.orangeAttributes]; + [mutAttributes setObject:colors.orangeForegroundColor forKey:NSForegroundColorAttributeName]; + self.orangeAttributes = [[NSDictionary alloc] initWithDictionary:mutAttributes]; +} + +- (void)enumerateAndUpdateColorsInAttributedString: (NSMutableAttributedString *)attributedString range: (NSRange)range { + [attributedString enumerateAttribute:WKSourceEditorCustomKeyColorOrange + inRange:range + options:nil + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.orangeAttributes range:localRange]; + } + } + }]; +} + +- (void)updateFontAttributesWithFonts: (WKSourceEditorFonts *)fonts { + NSMutableDictionary *mutHeadingAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.headingFontAttributes]; + [mutHeadingAttributes setObject:fonts.headingFont forKey:NSFontAttributeName]; + self.headingFontAttributes = [[NSDictionary alloc] initWithDictionary:mutHeadingAttributes]; + + NSMutableDictionary *mutSubheading1Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading1FontAttributes]; + [mutSubheading1Attributes setObject:fonts.subheading1Font forKey:NSFontAttributeName]; + self.subheading1FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading1Attributes]; + + NSMutableDictionary *mutSubheading2Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading2FontAttributes]; + [mutSubheading2Attributes setObject:fonts.subheading2Font forKey:NSFontAttributeName]; + self.subheading2FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading2Attributes]; + + NSMutableDictionary *mutSubheading3Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading3FontAttributes]; + [mutSubheading3Attributes setObject:fonts.subheading3Font forKey:NSFontAttributeName]; + self.subheading3FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading3Attributes]; + + NSMutableDictionary *mutSubheading4Attributes = [[NSMutableDictionary alloc] initWithDictionary:self.subheading4FontAttributes]; + [mutSubheading4Attributes setObject:fonts.subheading4Font forKey:NSFontAttributeName]; + self.subheading4FontAttributes = [[NSDictionary alloc] initWithDictionary:mutSubheading4Attributes]; +} + +- (void)enumerateAndUpdateFontsInAttributedString: (NSMutableAttributedString *)attributedString range: (NSRange)range { + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontHeading + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.headingFontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading1 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading1FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading2 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading2FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading3 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading3FontAttributes range:localRange]; + } + } + }]; + + [attributedString enumerateAttribute:WKSourceEditorCustomKeyFontSubheading4 + inRange:range + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(id value, NSRange localRange, BOOL *stop) { + if ([value isKindOfClass: [NSNumber class]]) { + NSNumber *numValue = (NSNumber *)value; + if ([numValue boolValue] == YES) { + [attributedString addAttributes:self.subheading4FontAttributes range:localRange]; + } + } + }]; +} + +@end diff --git a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h index ea83336ffab..4813b802b81 100644 --- a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h +++ b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h @@ -9,6 +9,7 @@ #import "WKSourceEditorFormatterBoldItalics.h" #import "WKSourceEditorFormatterTemplate.h" #import "WKSourceEditorFormatterList.h" +#import "WKSourceEditorFormatterHeading.h" #import "WKSourceEditorFormatterStrikethrough.h" #import "WKSourceEditorStorageDelegate.h" diff --git a/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h new file mode 120000 index 00000000000..5fc70400b83 --- /dev/null +++ b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterHeading.h @@ -0,0 +1 @@ +../WKSourceEditorFormatterHeading.h \ No newline at end of file diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift index 391fb576903..56c3bf99429 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift @@ -226,6 +226,116 @@ final class WKSourceEditorFormatterButtonActionTests: XCTestCase { mediator.listFormatter?.toggleListNumber(action: .remove, in: mediator.textView) XCTAssertEqual(mediator.textView.attributedText.string, "Test") } + + func testHeadingAdd() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .heading, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n==Test==\n") + } + + func testHeadingRemove() throws { + let text = "==Test==" + let selectedRange = NSRange(location: 2, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading1Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading1, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n===Test===\n") + } + + func testSubheading1Remove() throws { + let text = "===Test===" + let selectedRange = NSRange(location: 3, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading2Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading2, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n====Test====\n") + } + + func testSubheading2Remove() throws { + let text = "====Test====" + let selectedRange = NSRange(location: 4, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading3Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading3, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n=====Test=====\n") + } + + func testSubheading3Remove() throws { + let text = "=====Test=====" + let selectedRange = NSRange(location: 5, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testSubheading4Add() throws { + let text = "Test" + let selectedRange = NSRange(location: 0, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading4, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "\n======Test======\n") + } + + func testSubheading4Remove() throws { + let text = "======Test======" + let selectedRange = NSRange(location: 6, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .paragraph, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testHeadingSwitchToSubheading3() throws { + let text = "==Test==" + let selectedRange = NSRange(location: 2, length: 4) + let textView = mediator.textView + textView.attributedText = NSAttributedString(string: text) + textView.selectedRange = selectedRange + mediator.headingFormatter?.toggleHeadingFormatting(selectedHeading: .subheading3, currentSelectionState: mediator.selectionState(selectedDocumentRange: selectedRange), textView: textView) + XCTAssertEqual(mediator.textView.attributedText.string, "=====Test=====") + } func testStrikethroughInsertAndRemove() throws { let text = "One Two Three Four" diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift index 56c2a3ce9ce..638407f3005 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift @@ -11,9 +11,10 @@ final class WKSourceEditorFormatterTests: XCTestCase { var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics! var templateFormatter: WKSourceEditorFormatterTemplate! var listFormatter: WKSourceEditorFormatterList! + var headingFormatter: WKSourceEditorFormatterHeading! var strikethroughFormatter: WKSourceEditorFormatterStrikethrough! var formatters: [WKSourceEditorFormatter] { - return [baseFormatter, templateFormatter, boldItalicsFormatter, listFormatter, strikethroughFormatter] + return [baseFormatter, templateFormatter, boldItalicsFormatter, listFormatter, headingFormatter, strikethroughFormatter] } override func setUpWithError() throws { @@ -30,11 +31,17 @@ final class WKSourceEditorFormatterTests: XCTestCase { self.fonts.boldFont = WKFont.for(.boldBody, compatibleWith: traitCollection) self.fonts.italicsFont = WKFont.for(.italicsBody, compatibleWith: traitCollection) self.fonts.boldItalicsFont = WKFont.for(.boldItalicsBody, compatibleWith: traitCollection) + self.fonts.headingFont = WKFont.for(.editorHeading, compatibleWith: traitCollection) + self.fonts.subheading1Font = WKFont.for(.editorSubheading1, compatibleWith: traitCollection) + self.fonts.subheading2Font = WKFont.for(.editorSubheading2, compatibleWith: traitCollection) + self.fonts.subheading3Font = WKFont.for(.editorSubheading3, compatibleWith: traitCollection) + self.fonts.subheading4Font = WKFont.for(.editorSubheading4, compatibleWith: traitCollection) self.baseFormatter = WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: .left) self.boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) self.templateFormatter = WKSourceEditorFormatterTemplate(colors: colors, fonts: fonts) self.listFormatter = WKSourceEditorFormatterList(colors: colors, fonts: fonts) + self.headingFormatter = WKSourceEditorFormatterHeading(colors: colors, fonts: fonts) self.strikethroughFormatter = WKSourceEditorFormatterStrikethrough(colors: colors, fonts: fonts) } @@ -886,57 +893,256 @@ final class WKSourceEditorFormatterTests: XCTestCase { XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") } + func testHeading() { + let string = "== Test ==" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 2, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 8, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect heading formatting") + XCTAssertEqual(openingRange.length, 2, "Incorrect heading formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect heading formatting") + + // " Heading Test " + XCTAssertEqual(contentRange.location, 2, "Incorrect heading formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect heading formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect heading formatting") + + // "==" + XCTAssertEqual(closingRange.location, 8, "Incorrect heading formatting") + XCTAssertEqual(closingRange.length, 2, "Incorrect heading formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.headingFont, "Incorrect heading formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect heading formatting") + } + + func testSubheading1() { + let string = "=== Test ===" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading1 formatting") + XCTAssertEqual(openingRange.length, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading1 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading1 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading1 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 9, "Incorrect subheading1 formatting") + XCTAssertEqual(closingRange.length, 3, "Incorrect subheading1 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading1Font, "Incorrect subheading1 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading1 formatting") + } + + func testSubheading2() { + let string = "==== Test ====" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 4, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 10, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading2 formatting") + XCTAssertEqual(openingRange.length, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading2 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading2 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading2 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 10, "Incorrect subheading2 formatting") + XCTAssertEqual(closingRange.length, 4, "Incorrect subheading2 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading2Font, "Incorrect subheading2 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading2 formatting") + } + + func testSubheading3() { + let string = "===== Test =====" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 5, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 11, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading3 formatting") + XCTAssertEqual(openingRange.length, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading3 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading3 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading3 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 11, "Incorrect subheading3 formatting") + XCTAssertEqual(closingRange.length, 5, "Incorrect subheading3 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading3Font, "Incorrect subheading3 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading3 formatting") + } + + func testSubeading4() { + let string = "====== Test ======" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var openingRange = NSRange(location: 0, length: 0) + let openingAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &openingRange) + + var contentRange = NSRange(location: 0, length: 0) + let contentAttributes = mutAttributedString.attributes(at: 6, effectiveRange: &contentRange) + + var closingRange = NSRange(location: 0, length: 0) + let closingAttributes = mutAttributedString.attributes(at: 12, effectiveRange: &closingRange) + + // "==" + XCTAssertEqual(openingRange.location, 0, "Incorrect subheading4 formatting") + XCTAssertEqual(openingRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(openingAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(openingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading4 formatting") + + // " Test " + XCTAssertEqual(contentRange.location, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(contentRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(contentAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(contentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect subheading4 formatting") + + // "==" + XCTAssertEqual(closingRange.location, 12, "Incorrect subheading4 formatting") + XCTAssertEqual(closingRange.length, 6, "Incorrect subheading4 formatting") + XCTAssertEqual(closingAttributes[.font] as! UIFont, fonts.subheading4Font, "Incorrect subheading4 formatting") + XCTAssertEqual(closingAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect subheading4 formatting") + } + + func testInlineHeading() { + let string = "Test == Test == Test" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var range = NSRange(location: 0, length: 0) + let attributes = mutAttributedString.attributes(at: 0, effectiveRange: &range) + + // "Test == Test == Test" + // Inline headings should not format - they must be on their own line. + XCTAssertEqual(range.location, 0, "Incorrect inline heading formatting") + XCTAssertEqual(range.length, 20, "Incorrect inline heading formatting") + XCTAssertEqual(attributes[.font] as! UIFont, fonts.baseFont, "Incorrect inline heading formatting") + XCTAssertEqual(attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect inline heading formatting") + } + func testStrikethrough() { - let string = "Testing. Strikethrough. Testing" - let mutAttributedString = NSMutableAttributedString(string: string) - - for formatter in formatters { - formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) - } - - var base1Range = NSRange(location: 0, length: 0) - let base1Attributes = mutAttributedString.attributes(at: 0, effectiveRange: &base1Range) - - var strikethroughOpenRange = NSRange(location: 0, length: 0) - let strikethroughOpenAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &strikethroughOpenRange) - - var strikethroughContentRange = NSRange(location: 0, length: 0) - let strikethroughContentAttributes = mutAttributedString.attributes(at: 12, effectiveRange: &strikethroughContentRange) - - var strikethroughCloseRange = NSRange(location: 0, length: 0) - let strikethroughCloseAttributes = mutAttributedString.attributes(at: 26, effectiveRange: &strikethroughCloseRange) - - var base2Range = NSRange(location: 0, length: 0) - let base2Attributes = mutAttributedString.attributes(at: 32, effectiveRange: &base2Range) - - // "Testing. " - XCTAssertEqual(base1Range.location, 0, "Incorrect base formatting") - XCTAssertEqual(base1Range.length, 9, "Incorrect base formatting") - XCTAssertEqual(base1Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(base1Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") - - // "" - XCTAssertEqual(strikethroughOpenRange.location, 9, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughOpenRange.length, 3, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughOpenAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughOpenAttributes[.foregroundColor] as! UIColor, colors.greenForegroundColor, "Incorrect strikethrough formatting") - - // "Strikethrough." - XCTAssertEqual(strikethroughContentRange.location, 12, "Incorrect content formatting") - XCTAssertEqual(strikethroughContentRange.length, 14, "Incorrect content formatting") - XCTAssertEqual(strikethroughContentAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect content formatting") - XCTAssertEqual(strikethroughContentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect content formatting") - - // "" - XCTAssertEqual(strikethroughCloseRange.location, 26, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughCloseRange.length, 4, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughCloseAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect strikethrough formatting") - XCTAssertEqual(strikethroughCloseAttributes[.foregroundColor] as! UIColor, colors.greenForegroundColor, "Incorrect strikethrough formatting") - - // " Testing" - XCTAssertEqual(base2Range.location, 30, "Incorrect base formatting") - XCTAssertEqual(base2Range.length, 8, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") + let string = "Testing. Strikethrough. Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) } + + var base1Range = NSRange(location: 0, length: 0) + let base1Attributes = mutAttributedString.attributes(at: 0, effectiveRange: &base1Range) + + var strikethroughOpenRange = NSRange(location: 0, length: 0) + let strikethroughOpenAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &strikethroughOpenRange) + + var strikethroughContentRange = NSRange(location: 0, length: 0) + let strikethroughContentAttributes = mutAttributedString.attributes(at: 12, effectiveRange: &strikethroughContentRange) + + var strikethroughCloseRange = NSRange(location: 0, length: 0) + let strikethroughCloseAttributes = mutAttributedString.attributes(at: 26, effectiveRange: &strikethroughCloseRange) + + var base2Range = NSRange(location: 0, length: 0) + let base2Attributes = mutAttributedString.attributes(at: 32, effectiveRange: &base2Range) + + // "Testing. " + XCTAssertEqual(base1Range.location, 0, "Incorrect base formatting") + XCTAssertEqual(base1Range.length, 9, "Incorrect base formatting") + XCTAssertEqual(base1Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") + XCTAssertEqual(base1Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") + + // "" + XCTAssertEqual(strikethroughOpenRange.location, 9, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughOpenRange.length, 3, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughOpenAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughOpenAttributes[.foregroundColor] as! UIColor, colors.greenForegroundColor, "Incorrect strikethrough formatting") + + // "Strikethrough." + XCTAssertEqual(strikethroughContentRange.location, 12, "Incorrect content formatting") + XCTAssertEqual(strikethroughContentRange.length, 14, "Incorrect content formatting") + XCTAssertEqual(strikethroughContentAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect content formatting") + XCTAssertEqual(strikethroughContentAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect content formatting") + + // "" + XCTAssertEqual(strikethroughCloseRange.location, 26, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughCloseRange.length, 4, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughCloseAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect strikethrough formatting") + XCTAssertEqual(strikethroughCloseAttributes[.foregroundColor] as! UIColor, colors.greenForegroundColor, "Incorrect strikethrough formatting") + + // " Testing" + XCTAssertEqual(base2Range.location, 30, "Incorrect base formatting") + XCTAssertEqual(base2Range.length, 8, "Incorrect base formatting") + XCTAssertEqual(base2Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") + XCTAssertEqual(base2Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") + } } diff --git a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift index e02a1ab081b..c5d0f029efa 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift @@ -214,6 +214,106 @@ final class WKSourceEditorTextFrameworkMediatorTests: XCTestCase { XCTAssertTrue(selectionStates.isHorizontalTemplate) } + func testHeadingSelectionState() throws { + + let text = "== Test ==" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 4)) + XCTAssertTrue(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 0)) + XCTAssertTrue(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading1SelectionState() throws { + + let text = "=== Test ===" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 4, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertTrue(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertTrue(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading2SelectionState() throws { + + let text = "==== Test ====" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 5, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertTrue(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 7, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertTrue(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading3SelectionState() throws { + + let text = "===== Test =====" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 6, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertTrue(selectionStates1.isSubheading3) + XCTAssertFalse(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 8, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertTrue(selectionStates2.isSubheading3) + XCTAssertFalse(selectionStates2.isSubheading4) + } + + func testSubheading4SelectionState() throws { + + let text = "====== Test ======" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates1 = mediator.selectionState(selectedDocumentRange: NSRange(location: 7, length: 4)) + XCTAssertFalse(selectionStates1.isHeading) + XCTAssertFalse(selectionStates1.isSubheading1) + XCTAssertFalse(selectionStates1.isSubheading2) + XCTAssertFalse(selectionStates1.isSubheading3) + XCTAssertTrue(selectionStates1.isSubheading4) + + let selectionStates2 = mediator.selectionState(selectedDocumentRange: NSRange(location: 9, length: 0)) + XCTAssertFalse(selectionStates2.isHeading) + XCTAssertFalse(selectionStates2.isSubheading1) + XCTAssertFalse(selectionStates2.isSubheading2) + XCTAssertFalse(selectionStates2.isSubheading3) + XCTAssertTrue(selectionStates2.isSubheading4) + } + func testStrikethroughSelectionState() throws { let text = "Testing Strikethrough Testing." mediator.textView.attributedText = NSAttributedString(string: text)