diff --git a/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift b/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift index cf60cdc87e2..b9a9338814f 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Base/WKEditorToolbarButton.swift @@ -79,6 +79,17 @@ class WKEditorToolbarButton: WKComponentView { } } + var isEnabled: Bool { + get { + return button.isEnabled + } + set { + button.isEnabled = newValue + updateColors() + accessibilityTraits = newValue ? [.button, .selected] : [.button, .notEnabled] + } + } + func setImage(_ image: UIImage?, for state: UIControl.State) { button.setImage(image, for: state) } diff --git a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift index 38713921171..5dc203a4944 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Accessory Views/Expanding/WKEditorToolbarExpandingView.swift @@ -6,6 +6,10 @@ protocol WKEditorToolbarExpandingViewDelegate: AnyObject { func toolbarExpandingViewDidTapTemplate(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) func toolbarExpandingViewDidTapLink(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) func toolbarExpandingViewDidTapImage(toolbarView: WKEditorToolbarExpandingView) + func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) + func toolbarExpandingViewDidTapOrderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) + func toolbarExpandingViewDidTapIncreaseIndent(toolbarView: WKEditorToolbarExpandingView) + func toolbarExpandingViewDidTapDecreaseIndent(toolbarView: WKEditorToolbarExpandingView) } class WKEditorToolbarExpandingView: WKEditorToolbarView { @@ -114,10 +118,12 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { decreaseIndentionButton.setImage(WKSFSymbolIcon.for(symbol: .decreaseIndent), for: .normal) decreaseIndentionButton.addTarget(self, action: #selector(tappedDecreaseIndentation), for: .touchUpInside) decreaseIndentionButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonDecreaseIndent + decreaseIndentionButton.isEnabled = false increaseIndentionButton.setImage(WKSFSymbolIcon.for(symbol: .increaseIndent), for: .normal) increaseIndentionButton.addTarget(self, action: #selector(tappedIncreaseIndentation), for: .touchUpInside) increaseIndentionButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonInceaseIndent + increaseIndentionButton.isEnabled = false cursorUpButton.setImage(WKIcon.chevronUp, for: .normal) cursorUpButton.addTarget(self, action: #selector(tappedCursorUp), for: .touchUpInside) @@ -148,6 +154,28 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { templateButton.isSelected = selectionState.isHorizontalTemplate linkButton.isSelected = selectionState.isSimpleLink imageButton.isEnabled = !selectionState.isBold && !selectionState.isItalics && !selectionState.isSimpleLink + + unorderedListButton.isSelected = selectionState.isBulletSingleList || selectionState.isBulletMultipleList + unorderedListButton.isEnabled = !selectionState.isNumberSingleList && !selectionState.isNumberMultipleList + + orderedListButton.isSelected = selectionState.isNumberSingleList || selectionState.isNumberMultipleList + orderedListButton.isEnabled = !selectionState.isBulletSingleList && !selectionState.isBulletMultipleList + + decreaseIndentionButton.isEnabled = false + if selectionState.isBulletMultipleList || selectionState.isNumberMultipleList { + decreaseIndentionButton.isEnabled = true + } + + if selectionState.isBulletSingleList || + selectionState.isBulletMultipleList || + selectionState.isNumberSingleList || + selectionState.isNumberMultipleList { + increaseIndentionButton.isEnabled = true + } else { + increaseIndentionButton.isEnabled = false + } + + cursorRightButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current.accessibilityLabelButtonCursorRight } // MARK: - Button Actions @@ -204,15 +232,19 @@ class WKEditorToolbarExpandingView: WKEditorToolbarView { } @objc private func tappedUnorderedList() { + delegate?.toolbarExpandingViewDidTapUnorderedList(toolbarView: self, isSelected: unorderedListButton.isSelected) } @objc private func tappedOrderedList() { + delegate?.toolbarExpandingViewDidTapOrderedList(toolbarView: self, isSelected: orderedListButton.isSelected) } @objc private func tappedDecreaseIndentation() { + delegate?.toolbarExpandingViewDidTapDecreaseIndent(toolbarView: self) } @objc private func tappedIncreaseIndentation() { + delegate?.toolbarExpandingViewDidTapIncreaseIndent(toolbarView: self) } @objc private func tappedCursorUp() { 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 91a696264e2..bcb20840fb6 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 @@ -6,6 +6,11 @@ protocol WKEditorInputViewDelegate: AnyObject { func didTapBold(isSelected: Bool) func didTapItalics(isSelected: Bool) func didTapTemplate(isSelected: Bool) + func didTapBulletList(isSelected: Bool) + func didTapNumberList(isSelected: Bool) + func didTapIncreaseIndent() + func didTapDecreaseIndent() + func didTapHeading(type: WKEditorInputView.HeadingButtonType) func didTapStrikethrough(isSelected: Bool) func didTapLink(isSelected: Bool) } @@ -222,6 +227,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 @@ -284,20 +301,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/Common Views/Input Views/WKEditorToolbarGroupedView.swift b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift index 34c00e0b6c4..0ac283016d0 100644 --- a/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift +++ b/Components/Sources/Components/Components/Editors/Common Views/Input Views/WKEditorToolbarGroupedView.swift @@ -31,10 +31,12 @@ class WKEditorToolbarGroupedView: WKEditorToolbarView { decreaseIndentButton.setImage(WKSFSymbolIcon.for(symbol: .decreaseIndent), for: .normal) decreaseIndentButton.addTarget(self, action: #selector(tappedDecreaseIndent), for: .touchUpInside) decreaseIndentButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current?.accessibilityLabelButtonDecreaseIndent + decreaseIndentButton.isEnabled = false increaseIndentButton.setImage(WKSFSymbolIcon.for(symbol: .increaseIndent), for: .normal) increaseIndentButton.addTarget(self, action: #selector(tappedIncreaseIndent), for: .touchUpInside) increaseIndentButton.accessibilityLabel = WKSourceEditorLocalizedStrings.current?.accessibilityLabelButtonInceaseIndent + increaseIndentButton.isEnabled = false superscriptButton.setImage(WKSFSymbolIcon.for(symbol: .textFormatSuperscript), for: .normal) superscriptButton.addTarget(self, action: #selector(tappedSuperscript), for: .touchUpInside) @@ -56,27 +58,51 @@ class WKEditorToolbarGroupedView: WKEditorToolbarView { } // MARK: - Notifications - + @objc private func updateButtonSelectionState(_ notification: NSNotification) { guard let selectionState = notification.userInfo?[Notification.WKSourceEditorSelectionStateKey] as? WKSourceEditorSelectionState else { return } + unorderedListButton.isSelected = selectionState.isBulletSingleList || selectionState.isBulletMultipleList + unorderedListButton.isEnabled = !selectionState.isNumberSingleList && !selectionState.isNumberMultipleList + + orderedListButton.isSelected = selectionState.isNumberSingleList || selectionState.isNumberMultipleList + orderedListButton.isEnabled = !selectionState.isBulletSingleList && !selectionState.isBulletMultipleList + + decreaseIndentButton.isEnabled = false + if selectionState.isBulletMultipleList || selectionState.isNumberMultipleList { + decreaseIndentButton.isEnabled = true + } + + if selectionState.isBulletSingleList || + selectionState.isBulletMultipleList || + selectionState.isNumberSingleList || + selectionState.isNumberMultipleList { + increaseIndentButton.isEnabled = true + } else { + increaseIndentButton.isEnabled = false + } + strikethroughButton.isSelected = selectionState.isStrikethrough } // MARK: - Button Actions @objc private func tappedIncreaseIndent() { + delegate?.didTapIncreaseIndent() } @objc private func tappedDecreaseIndent() { + delegate?.didTapDecreaseIndent() } @objc private func tappedUnorderedList() { + delegate?.didTapBulletList(isSelected: unorderedListButton.isSelected) } @objc private func tappedOrderedList() { + delegate?.didTapNumberList(isSelected: orderedListButton.isSelected) } @objc private func tappedSuperscript() { 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 63c93b0c8ef..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 @@ -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/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift new file mode 100644 index 00000000000..66ed0f55ca4 --- /dev/null +++ b/Components/Sources/Components/Components/Editors/Source Editor/Formatter Extensions/WKSourceEditorFormatterList+ButtonActions.swift @@ -0,0 +1,120 @@ +import Foundation +import ComponentsObjC + +extension WKSourceEditorFormatterList { + func toggleListBullet(action: WKSourceEditorFormatterButtonAction, in textView: UITextView) { + toggleListItem(action: action, formattingCharacter: "*", in: textView) + } + + func toggleListNumber(action: WKSourceEditorFormatterButtonAction, in textView: UITextView) { + toggleListItem(action: action, formattingCharacter: "#", in: textView) + } + + func tappedIncreaseIndent(currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + guard currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList || + currentSelectionState.isNumberSingleList || + currentSelectionState.isNumberMultipleList else { + assertionFailure("Increase / Decrease indent buttons should have been disabled.") + return + } + + let formattingCharacter = (currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList) ? "*" : "#" + + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + + textView.textStorage.insert(NSAttributedString(string: String(formattingCharacter)), at: lineRange.location) + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: 1), + let newEnd = textView.position(from: selectedRange.end, offset: 1) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + + func tappedDecreaseIndent(currentSelectionState: WKSourceEditorSelectionState, textView: UITextView) { + + guard currentSelectionState.isBulletSingleList || + currentSelectionState.isBulletMultipleList || + currentSelectionState.isNumberSingleList || + currentSelectionState.isNumberMultipleList else { + assertionFailure("Increase / Decrease indent buttons should have been disabled.") + return + } + + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + + guard textView.textStorage.length > lineRange.location else { + return + } + + textView.textStorage.replaceCharacters(in: NSRange(location: lineRange.location, length: 1), with: "") + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: -1), + let newEnd = textView.position(from: selectedRange.end, offset: -1) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + + private func toggleListItem(action: WKSourceEditorFormatterButtonAction, formattingCharacter: Character, in textView: UITextView) { + let nsString = textView.attributedText.string as NSString + let lineRange = nsString.lineRange(for: textView.selectedRange) + switch action { + case .add: + + var numBullets = 0 + for char in textView.textStorage.attributedSubstring(from: lineRange).string { + if char == formattingCharacter { + numBullets += 1 + } + } + + let insertString = numBullets == 0 ? "\(String(formattingCharacter)) " : String(formattingCharacter) + textView.textStorage.insert(NSAttributedString(string: insertString), at: lineRange.location) + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: insertString.count), + let newEnd = textView.position(from: selectedRange.end, offset: insertString.count) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + case .remove: + + var numBullets = 0 + var hasSpace = false + for char in textView.textStorage.attributedSubstring(from: lineRange).string { + if char != formattingCharacter { + if char == " " { + hasSpace = true + } + break + } + + if char == formattingCharacter { + numBullets += 1 + } + } + + let replacementLength = numBullets + (hasSpace ? 1 : 0) + textView.textStorage.replaceCharacters(in: NSRange(location: lineRange.location, length: replacementLength), with: "") + + // reset cursor so it doesn't move + if let selectedRange = textView.selectedTextRange { + if let newStart = textView.position(from: selectedRange.start, offset: -1 * replacementLength), + let newEnd = textView.position(from: selectedRange.end, offset: -1 * replacementLength) { + textView.selectedTextRange = textView.textRange(from: newStart, to: newEnd) + } + } + } + } +} diff --git a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift index f383b31e574..11f3897ed6d 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorTextFrameworkMediator.swift @@ -17,14 +17,32 @@ fileprivate var needsTextKit2: Bool { let isBold: Bool let isItalics: Bool let isHorizontalTemplate: Bool + let isBulletSingleList: 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 let isSimpleLink: Bool let isLinkWithNestedLink: Bool - - init(isBold: Bool, isItalics: Bool, isHorizontalTemplate: Bool, isStrikethrough: Bool, isSimpleLink: Bool, isLinkWithNestedLink: 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, isSimpleLink: Bool, isLinkWithNestedLink: Bool) { self.isBold = isBold self.isItalics = isItalics self.isHorizontalTemplate = isHorizontalTemplate + self.isBulletSingleList = isBulletSingleList + 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 self.isSimpleLink = isSimpleLink self.isLinkWithNestedLink = isLinkWithNestedLink @@ -41,6 +59,8 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { private(set) var formatters: [WKSourceEditorFormatter] = [] 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? private(set) var linkFormatter: WKSourceEditorFormatterLink? @@ -109,18 +129,22 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { let colors = self.colors let fonts = self.fonts - let boldItalicsFormatter = WKSourceEditorFormatterBoldItalics(colors: colors, fonts: fonts) 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) let linkFormatter = WKSourceEditorFormatterLink(colors: colors, fonts: fonts) - self.formatters = [WKSourceEditorFormatterBase(colors: colors, fonts: fonts, textAlignment: viewModel.textAlignment), templateFormatter, boldItalicsFormatter, - strikethroughFormatter, - linkFormatter] + listFormatter, + headingFormatter, + strikethroughFormatter, linkFormatter] self.boldItalicsFormatter = boldItalicsFormatter self.templateFormatter = templateFormatter + self.listFormatter = listFormatter + self.headingFormatter = headingFormatter self.strikethroughFormatter = strikethroughFormatter self.linkFormatter = linkFormatter @@ -162,30 +186,48 @@ final class WKSourceEditorTextFrameworkMediator: NSObject { if needsTextKit2 { guard let textKit2Data = textkit2SelectionData(selectedDocumentRange: selectedDocumentRange) else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isStrikethrough: false, isSimpleLink: false, isLinkWithNestedLink: 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, isSimpleLink: false, isLinkWithNestedLink: false) } let isBold = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBoldIn: textKit2Data.paragraphSelectedRange) ?? false let isItalics = boldItalicsFormatter?.attributedString(textKit2Data.paragraphAttributedString, isItalicsIn: textKit2Data.paragraphSelectedRange) ?? false let isHorizontalTemplate = templateFormatter?.attributedString(textKit2Data.paragraphAttributedString, isHorizontalTemplateIn: textKit2Data.paragraphSelectedRange) ?? false + let isBulletSingleList = listFormatter?.attributedString(textKit2Data.paragraphAttributedString, isBulletSingleIn: textKit2Data.paragraphSelectedRange) ?? false + 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 let isSimpleLink = linkFormatter?.attributedString(textKit2Data.paragraphAttributedString, isSimpleLinkIn: textKit2Data.paragraphSelectedRange) ?? false let isLinkWithNestedLink = linkFormatter?.attributedString(textKit2Data.paragraphAttributedString, isLinkWithNestedLinkIn: textKit2Data.paragraphSelectedRange) ?? false - - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isStrikethrough: isStrikethrough, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink) + + 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, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink) } else { guard let textKit1Storage else { - return WKSourceEditorSelectionState(isBold: false, isItalics: false, isHorizontalTemplate: false, isStrikethrough: false, isSimpleLink: false, isLinkWithNestedLink: 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, isSimpleLink: false, isLinkWithNestedLink: 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 + let isBulletSingleList = listFormatter?.attributedString(textKit1Storage, isBulletSingleIn: selectedDocumentRange) ?? false + 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 let isSimpleLink = linkFormatter?.attributedString(textKit1Storage, isSimpleLinkIn: selectedDocumentRange) ?? false let isLinkWithNestedLink = linkFormatter?.attributedString(textKit1Storage, isLinkWithNestedLinkIn: selectedDocumentRange) ?? false - - return WKSourceEditorSelectionState(isBold: isBold, isItalics: isItalics, isHorizontalTemplate: isHorizontalTemplate, isStrikethrough: isStrikethrough, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink) + + 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, isSimpleLink: isSimpleLink, isLinkWithNestedLink: isLinkWithNestedLink) } } @@ -236,6 +278,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 24316c070a1..9e874c794bd 100644 --- a/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift +++ b/Components/Sources/Components/Components/Editors/Source Editor/WKSourceEditorViewController.swift @@ -322,6 +322,24 @@ extension WKSourceEditorViewController: WKEditorToolbarExpandingViewDelegate { func toolbarExpandingViewDidTapImage(toolbarView: WKEditorToolbarExpandingView) { delegate?.sourceEditorViewControllerDidTapImage() } + + func toolbarExpandingViewDidTapUnorderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListBullet(action: action, in: textView) + } + + func toolbarExpandingViewDidTapOrderedList(toolbarView: WKEditorToolbarExpandingView, isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListNumber(action: action, in: textView) + } + + func toolbarExpandingViewDidTapIncreaseIndent(toolbarView: WKEditorToolbarExpandingView) { + textFrameworkMediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + + func toolbarExpandingViewDidTapDecreaseIndent(toolbarView: WKEditorToolbarExpandingView) { + textFrameworkMediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: selectionState(), textView: textView) + } } // MARK: - WKEditorToolbarHighlightViewDelegate @@ -356,6 +374,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 @@ -372,6 +393,24 @@ extension WKSourceEditorViewController: WKEditorInputViewDelegate { textFrameworkMediator.templateFormatter?.toggleTemplateFormatting(action: action, in: textView) } + func didTapBulletList(isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListBullet(action: action, in: textView) + } + + func didTapNumberList(isSelected: Bool) { + let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add + textFrameworkMediator.listFormatter?.toggleListNumber(action: action, in: textView) + } + + func didTapIncreaseIndent() { + textFrameworkMediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + + func didTapDecreaseIndent() { + textFrameworkMediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: selectionState(), textView: textView) + } + func didTapStrikethrough(isSelected: Bool) { let action: WKSourceEditorFormatterButtonAction = isSelected ? .remove : .add textFrameworkMediator.strikethroughFormatter?.toggleStrikethroughFormatting(action: action, in: textView) 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/WKSourceEditorFormatter.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h index 9a67168c282..3eaffb09d4d 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.h @@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN @interface WKSourceEditorFormatter : NSObject +extern NSString *const WKSourceEditorCustomKeyColorOrange; extern NSString *const WKSourceEditorCustomKeyColorGreen; - (instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts; diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m index 63c589b2461..a8568f44e5f 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatter.m @@ -6,6 +6,8 @@ @implementation WKSourceEditorFormatter #pragma mark - Common Custom Attributed String Keys +// Font and Color custom attributes allow us to easily target already-formatted ranges. This is handy for speedy updates upon theme and text size change, as well as determining keyboard button selection states. +NSString * const WKSourceEditorCustomKeyColorOrange = @"WKSourceEditorKeyColorOrange"; NSString * const WKSourceEditorCustomKeyColorGreen = @"WKSourceEditorKeyColorGreen"; - (nonnull instancetype)initWithColors:(nonnull WKSourceEditorColors *)colors fonts:(nonnull WKSourceEditorFonts *)fonts { diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m index a441ebcce07..ea4e1d64297 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBase.m @@ -34,7 +34,9 @@ - (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]; [attributedString addAttributes:self.attributes range:range]; } diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m index 990d0573a3e..fd12af389ff 100644 --- a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterBoldItalics.m @@ -18,9 +18,6 @@ @interface WKSourceEditorFormatterBoldItalics () @implementation WKSourceEditorFormatterBoldItalics #pragma mark - Custom Attributed String Keys - -// Font and Color custom attributes allow us to easily target already-formatted ranges. This is handy for speedy updates upon theme and text size change, as well as determining keyboard button selection states. -NSString * const WKSourceEditorCustomKeyColorOrange = @"WKSourceEditorKeyColorOrange"; NSString * const WKSourceEditorCustomKeyFontBoldItalics = @"WKSourceEditorKeyFontBoldItalics"; NSString * const WKSourceEditorCustomKeyFontBold = @"WKSourceEditorKeyFontBold"; NSString * const WKSourceEditorCustomKeyFontItalics = @"WKSourceEditorKeyFontItalics"; @@ -62,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/WKSourceEditorFormatterList.h b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.h new file mode 100644 index 00000000000..a42dba04e1f --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.h @@ -0,0 +1,12 @@ +#import "WKSourceEditorFormatter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface WKSourceEditorFormatterList : WKSourceEditorFormatter +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletSingleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletMultipleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberSingleInRange:(NSRange)range; +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberMultipleInRange:(NSRange)range; +@end + +NS_ASSUME_NONNULL_END diff --git a/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m new file mode 100644 index 00000000000..de8ef933334 --- /dev/null +++ b/Components/Sources/ComponentsObjC/WKSourceEditorFormatterList.m @@ -0,0 +1,210 @@ +#import "WKSourceEditorFormatterList.h" +#import "WKSourceEditorColors.h" + +@interface WKSourceEditorFormatterList () + +@property (nonatomic, strong) NSDictionary *orangeAttributes; + +@property (nonatomic, strong) NSDictionary *bulletSingleContentAttributes; +@property (nonatomic, strong) NSDictionary *bulletMultipleContentAttributes; +@property (nonatomic, strong) NSDictionary *numberSingleContentAttributes; +@property (nonatomic, strong) NSDictionary *numberMultipleContentAttributes; + +@property (nonatomic, strong) NSRegularExpression *bulletSingleRegex; +@property (nonatomic, strong) NSRegularExpression *bulletMultipleRegex; +@property (nonatomic, strong) NSRegularExpression *numberSingleRegex; +@property (nonatomic, strong) NSRegularExpression *numberMultipleRegex; + +@end + +@implementation WKSourceEditorFormatterList + +#pragma mark - Custom Attributed String Keys + +NSString * const WKSourceEditorCustomKeyContentBulletSingle = @"WKSourceEditorCustomKeyContentBulletSingle"; +NSString * const WKSourceEditorCustomKeyContentBulletMultiple = @"WKSourceEditorCustomKeyContentBulletMultiple"; +NSString * const WKSourceEditorCustomKeyContentNumberSingle = @"WKSourceEditorCustomKeyContentNumberSingle"; +NSString * const WKSourceEditorCustomKeyContentNumberMultiple = @"WKSourceEditorCustomKeyContentNumberMultiple"; + +#pragma mark - Overrides + +- (instancetype)initWithColors:(WKSourceEditorColors *)colors fonts:(WKSourceEditorFonts *)fonts { + self = [super initWithColors:colors fonts:fonts]; + if (self) { + _orangeAttributes = @{ + NSForegroundColorAttributeName: colors.orangeForegroundColor, + WKSourceEditorCustomKeyColorOrange: [NSNumber numberWithBool:YES] + }; + + _bulletSingleContentAttributes = @{ + WKSourceEditorCustomKeyContentBulletSingle: [NSNumber numberWithBool:YES] + }; + + _bulletMultipleContentAttributes = @{ + WKSourceEditorCustomKeyContentBulletMultiple: [NSNumber numberWithBool:YES] + }; + + _numberSingleContentAttributes = @{ + WKSourceEditorCustomKeyContentNumberSingle: [NSNumber numberWithBool:YES] + }; + + _numberMultipleContentAttributes = @{ + WKSourceEditorCustomKeyContentNumberMultiple: [NSNumber numberWithBool:YES] + }; + + _bulletSingleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(\\*{1})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _bulletMultipleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(\\*{2,})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _numberSingleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(#{1})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + _numberMultipleRegex = [[NSRegularExpression alloc] initWithPattern:@"^(#{2,})(.*)$" options:NSRegularExpressionAnchorsMatchLines error:nil]; + } + return self; +} + +- (void)addSyntaxHighlightingToAttributedString:(nonnull NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + [attributedString removeAttribute:WKSourceEditorCustomKeyContentBulletSingle range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentBulletMultiple range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentNumberSingle range:range]; + [attributedString removeAttribute:WKSourceEditorCustomKeyContentNumberMultiple range:range]; + + [self enumerateAndHighlightAttributedString:attributedString range:range singleRegex:self.bulletSingleRegex multipleRegex:self.bulletMultipleRegex singleContentAttributes:self.bulletSingleContentAttributes singleContentAttributes:self.bulletMultipleContentAttributes]; + [self enumerateAndHighlightAttributedString:attributedString range:range singleRegex:self.numberSingleRegex multipleRegex:self.numberMultipleRegex singleContentAttributes:self.numberSingleContentAttributes singleContentAttributes:self.numberMultipleContentAttributes]; +} + +- (void)updateColors:(WKSourceEditorColors *)colors inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + + // First update orangeAttributes property so that addSyntaxHighlighting has the correct color the next time it is called + NSMutableDictionary *mutAttributes = [[NSMutableDictionary alloc] initWithDictionary:self.orangeAttributes]; + [mutAttributes setObject:colors.orangeForegroundColor forKey:NSForegroundColorAttributeName]; + self.orangeAttributes = [[NSDictionary alloc] initWithDictionary:mutAttributes]; + + // Then update entire attributed string orange color + [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)updateFonts:(WKSourceEditorFonts *)fonts inAttributedString:(NSMutableAttributedString *)attributedString inRange:(NSRange)range { + // No special font handling needed for lists +} + +#pragma mark - Public + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletSingleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentBulletSingle inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isBulletMultipleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentBulletMultiple inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberSingleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentNumberSingle inRange:range]; +} + +- (BOOL)attributedString:(NSMutableAttributedString *)attributedString isNumberMultipleInRange:(NSRange)range { + return [self attributedString:attributedString isContentKey:WKSourceEditorCustomKeyContentNumberMultiple inRange:range]; +} + +#pragma mark - Private + +- (void)enumerateAndHighlightAttributedString: (nonnull NSMutableAttributedString *)attributedString range:(NSRange)range singleRegex:(NSRegularExpression *)singleRegex multipleRegex:(NSRegularExpression *)multipleRegex singleContentAttributes:(NSDictionary *)singleContentAttributes singleContentAttributes:(NSDictionary *)multipleContentAttributes { + + NSMutableArray *multipleRanges = [[NSMutableArray alloc] init]; + + [multipleRegex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange orangeRange = [result rangeAtIndex:1]; + NSRange contentRange = [result rangeAtIndex:2]; + + if (fullMatch.location != NSNotFound) { + [multipleRanges addObject:[NSValue valueWithRange:fullMatch]]; + } + + if (orangeRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:orangeRange]; + } + + if (contentRange.location != NSNotFound) { + [attributedString addAttributes:multipleContentAttributes range:contentRange]; + } + }]; + + [singleRegex enumerateMatchesInString:attributedString.string + options:0 + range:range + usingBlock:^(NSTextCheckingResult *_Nullable result, NSMatchingFlags flags, BOOL *_Nonnull stop) { + NSRange fullMatch = [result rangeAtIndex:0]; + NSRange orangeRange = [result rangeAtIndex:1]; + NSRange contentRange = [result rangeAtIndex:2]; + + BOOL alreadyMultiple = NO; + for (NSValue *value in multipleRanges) { + NSRange multipleRange = value.rangeValue; + if (NSIntersectionRange(multipleRange, fullMatch).length != 0) { + alreadyMultiple = YES; + } + } + + if (alreadyMultiple) { + return; + } + + + if (orangeRange.location != NSNotFound) { + [attributedString addAttributes:self.orangeAttributes range:orangeRange]; + } + + if (contentRange.location != NSNotFound) { + [attributedString addAttributes:singleContentAttributes range:contentRange]; + } + }]; +} + +- (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; + } + + // Edge case, check previous character in case we're at the end of the line and list isn't detected + if ((attributedString.length > range.location - 1)) { + NSDictionary *attrs = [attributedString attributesAtIndex:range.location-1 effectiveRange:nil]; + + if (attrs[contentKey] != nil) { + isContentKey = YES; + } + } + } + + } else { + [attributedString enumerateAttributesInRange:range options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange loopRange, BOOL * _Nonnull stop) { + if ((attrs[contentKey] != nil) && + (loopRange.location == range.location && loopRange.length == range.length)) { + isContentKey = YES; + stop = YES; + } + }]; + } + + return isContentKey; +} + +@end diff --git a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h index e3f0f6651b5..13a96ff2f1b 100644 --- a/Components/Sources/ComponentsObjC/include/ComponentsObjC.h +++ b/Components/Sources/ComponentsObjC/include/ComponentsObjC.h @@ -8,6 +8,8 @@ #import "WKSourceEditorFormatterBase.h" #import "WKSourceEditorFormatterBoldItalics.h" #import "WKSourceEditorFormatterTemplate.h" +#import "WKSourceEditorFormatterList.h" +#import "WKSourceEditorFormatterHeading.h" #import "WKSourceEditorFormatterStrikethrough.h" #import "WKSourceEditorFormatterLink.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/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h new file mode 120000 index 00000000000..ca459437160 --- /dev/null +++ b/Components/Sources/ComponentsObjC/include/WKSourceEditorFormatterList.h @@ -0,0 +1 @@ +../WKSourceEditorFormatterList.h \ No newline at end of file diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift index a62c02d02f1..cc97bb19df2 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterButtonActionTests.swift @@ -163,6 +163,180 @@ final class WKSourceEditorFormatterButtonActionTests: XCTestCase { XCTAssertEqual(mediator.textView.attributedText.string, "One Two Three Four") } + func testListBulletInsertAndRemove() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListBullet(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.toggleListBullet(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListBulletInsertAndIncreaseIndent() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListBullet(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "** Test") + } + + func testListBulletDecreaseIndentAndRemove() throws { + let text = "*** Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 4, length: 0) + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "** Test") + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "* Test") + mediator.listFormatter?.toggleListBullet(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListNumberInsertAndRemove() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListNumber(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + mediator.listFormatter?.toggleListNumber(action: .remove, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "Test") + } + + func testListNumberInsertAndIncreaseIndent() throws { + let text = "Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 2, length: 0) + mediator.listFormatter?.toggleListNumber(action: .add, in: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + mediator.listFormatter?.tappedIncreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "## Test") + } + + func testListNumberDecreaseIndentAndRemove() throws { + let text = "### Test" + mediator.textView.attributedText = NSAttributedString(string: text) + mediator.textView.selectedRange = NSRange(location: 4, length: 0) + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "## Test") + mediator.listFormatter?.tappedDecreaseIndent(currentSelectionState: mediator.selectionState(selectedDocumentRange: mediator.textView.selectedRange), textView: mediator.textView) + XCTAssertEqual(mediator.textView.attributedText.string, "# Test") + 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" mediator.textView.attributedText = NSAttributedString(string: text) diff --git a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift index b7a0745cf4b..4664ceadaeb 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorFormatterTests.swift @@ -10,10 +10,12 @@ final class WKSourceEditorFormatterTests: XCTestCase { var baseFormatter: WKSourceEditorFormatterBase! var boldItalicsFormatter: WKSourceEditorFormatterBoldItalics! var templateFormatter: WKSourceEditorFormatterTemplate! + var listFormatter: WKSourceEditorFormatterList! + var headingFormatter: WKSourceEditorFormatterHeading! var strikethroughFormatter: WKSourceEditorFormatterStrikethrough! var linkFormatter: WKSourceEditorFormatterLink! var formatters: [WKSourceEditorFormatter] { - return [baseFormatter, templateFormatter, boldItalicsFormatter, strikethroughFormatter, linkFormatter] + return [baseFormatter, templateFormatter, boldItalicsFormatter, listFormatter, headingFormatter, strikethroughFormatter, linkFormatter] } override func setUpWithError() throws { @@ -31,10 +33,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) self.linkFormatter = WKSourceEditorFormatterLink(colors: colors, fonts: fonts) } @@ -779,98 +788,316 @@ final class WKSourceEditorFormatterTests: XCTestCase { XCTAssertEqual(refAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect ref 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") + func testListSingleBullet() { + let string = "* Testing" + let mutAttributedString = NSMutableAttributedString(string: string) + + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) } + + var bulletRange = NSRange(location: 0, length: 0) + let bulletAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &bulletRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 1, effectiveRange: &textRange) + + // "*" + XCTAssertEqual(bulletRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(bulletRange.length, 1, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 1, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 8, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } - func testPlainLink() { - let string = "Testing. [[Link with spaces]]. Testing" + func testListSingleNumber() { + let string = "# Testing" let mutAttributedString = NSMutableAttributedString(string: string) for formatter in formatters { formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) } + + var numberRange = NSRange(location: 0, length: 0) + let numberAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &numberRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 1, effectiveRange: &textRange) - var base1Range = NSRange(location: 0, length: 0) - let base1Attributes = mutAttributedString.attributes(at: 0, effectiveRange: &base1Range) + // "*" + XCTAssertEqual(numberRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(numberRange.length, 1, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 1, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 8, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + + func testListMultipleBulletNoSpace() { + let string = "***Testing" + let mutAttributedString = NSMutableAttributedString(string: string) - var linkRange = NSRange(location: 0, length: 0) - let linkRangeAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &linkRange) + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var bulletRange = NSRange(location: 0, length: 0) + let bulletAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &bulletRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &textRange) - var base2Range = NSRange(location: 0, length: 0) - let base2Attributes = mutAttributedString.attributes(at: 29, effectiveRange: &base2Range) + // "*" + XCTAssertEqual(bulletRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(bulletRange.length, 3, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(bulletAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 3, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 7, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } + + func testListMultipleNumberNoSpace() { + let string = "###Testing" + let mutAttributedString = NSMutableAttributedString(string: string) - // "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") + for formatter in formatters { + formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) + } + + var numberRange = NSRange(location: 0, length: 0) + let numberAttributes = mutAttributedString.attributes(at: 0, effectiveRange: &numberRange) + + var textRange = NSRange(location: 0, length: 0) + let textAttributes = mutAttributedString.attributes(at: 3, effectiveRange: &textRange) - // "[[Link with spaces]]" - XCTAssertEqual(linkRange.location, 9, "Incorrect link formatting") - XCTAssertEqual(linkRange.length, 20, "Incorrect link formatting") - XCTAssertEqual(linkRangeAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect link formatting") - XCTAssertEqual(linkRangeAttributes[.foregroundColor] as! UIColor, colors.blueForegroundColor, "Incorrect link formatting") + // "*" + XCTAssertEqual(numberRange.location, 0, "Incorrect list formatting") + XCTAssertEqual(numberRange.length, 3, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(numberAttributes[.foregroundColor] as! UIColor, colors.orangeForegroundColor, "Incorrect list formatting") + + // " Testing" + XCTAssertEqual(textRange.location, 3, "Incorrect list formatting") + XCTAssertEqual(textRange.length, 7, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect list formatting") + XCTAssertEqual(textAttributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect list formatting") + } - // ". Testing" - XCTAssertEqual(base2Range.location, 29, "Incorrect base formatting") - XCTAssertEqual(base2Range.length, 9, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base 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 testPipedLink() { - let string = "Testing. [[Link|Link with spaces]]. Testing" + 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 { @@ -880,11 +1107,17 @@ final class WKSourceEditorFormatterTests: XCTestCase { var base1Range = NSRange(location: 0, length: 0) let base1Attributes = mutAttributedString.attributes(at: 0, effectiveRange: &base1Range) - var linkRange = NSRange(location: 0, length: 0) - let linkAttributes = mutAttributedString.attributes(at: 9, effectiveRange: &linkRange) + 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: 34, effectiveRange: &base2Range) + let base2Attributes = mutAttributedString.attributes(at: 32, effectiveRange: &base2Range) // "Testing. " XCTAssertEqual(base1Range.location, 0, "Incorrect base formatting") @@ -892,52 +1125,28 @@ final class WKSourceEditorFormatterTests: XCTestCase { XCTAssertEqual(base1Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") XCTAssertEqual(base1Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") - // "[[Link|Link with spaces]]" - XCTAssertEqual(linkRange.location, 9, "Incorrect link formatting") - XCTAssertEqual(linkRange.length, 25, "Incorrect link formatting") - XCTAssertEqual(linkAttributes[.font] as! UIFont, fonts.baseFont, "Incorrect link formatting") - XCTAssertEqual(linkAttributes[.foregroundColor] as! UIColor, colors.blueForegroundColor, "Incorrect link 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") - // ". Testing" - XCTAssertEqual(base2Range.location, 34, "Incorrect base formatting") - XCTAssertEqual(base2Range.length, 9, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(base2Attributes[.foregroundColor] as! UIColor, colors.baseForegroundColor, "Incorrect base formatting") - } - - func testLinkFileWithNestedLinks() { - let string = "[[File:Cat with fish.jpg|thumb|left|Cat with [[fish]]|alt=Photo of cat looking at fish]]" - let mutAttributedString = NSMutableAttributedString(string: string) - - for formatter in formatters { - formatter.addSyntaxHighlighting(to: mutAttributedString, in: NSRange(location: 0, length: string.count)) - } + // "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") - var linkRange1 = NSRange(location: 0, length: 0) - let linkAttributes1 = mutAttributedString.attributes(at: 0, effectiveRange: &linkRange1) - - var linkRange2 = NSRange(location: 0, length: 0) - let linkAttributes2 = mutAttributedString.attributes(at: 45, effectiveRange: &linkRange2) + // "" + 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") - var linkRange3 = NSRange(location: 0, length: 0) - let linkAttributes3 = mutAttributedString.attributes(at: 53, effectiveRange: &linkRange3) - - // "[[File:Cat with fish.jpg|thumb|left|Cat with " - XCTAssertEqual(linkRange1.location, 0, "Incorrect link formatting") - XCTAssertEqual(linkRange1.length, 45, "Incorrect link formatting") - XCTAssertEqual(linkAttributes1[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(linkAttributes1[.foregroundColor] as! UIColor, colors.blueForegroundColor, "Incorrect link formatting") - - // "[[fish]]" - XCTAssertEqual(linkRange2.location, 45, "Incorrect link formatting") - XCTAssertEqual(linkRange2.length, 8, "Incorrect link formatting") - XCTAssertEqual(linkAttributes2[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(linkAttributes2[.foregroundColor] as! UIColor, colors.blueForegroundColor, "Incorrect link formatting") - - // "|alt=Photo of cat looking at fish]]" - XCTAssertEqual(linkRange3.location, 53, "Incorrect link formatting") - XCTAssertEqual(linkRange3.length, 35, "Incorrect link formatting") - XCTAssertEqual(linkAttributes3[.font] as! UIFont, fonts.baseFont, "Incorrect base formatting") - XCTAssertEqual(linkAttributes3[.foregroundColor] as! UIColor, colors.blueForegroundColor, "Incorrect link 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 484bc98bf65..dbbf840a4d8 100644 --- a/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift +++ b/Components/Tests/ComponentsTests/WKSourceEditorTextFrameworkMediatorTests.swift @@ -157,6 +157,54 @@ final class WKSourceEditorTextFrameworkMediatorTests: XCTestCase { XCTAssertFalse(selectionStates1.isHorizontalTemplate) } + func testListBulletSingleSelectionState() throws { + + let text = "* Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 2, length: 4)) + XCTAssertTrue(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListBulletMultipleSelectionState() throws { + + let text = "** Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 0)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertTrue(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListNumberSingleSelectionState() throws { + + let text = "# Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 2, length: 4)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertTrue(selectionStates.isNumberSingleList) + XCTAssertFalse(selectionStates.isNumberMultipleList) + } + + func testListNumberMultipleSelectionState() throws { + + let text = "## Test" + mediator.textView.attributedText = NSAttributedString(string: text) + + let selectionStates = mediator.selectionState(selectedDocumentRange: NSRange(location: 3, length: 0)) + XCTAssertFalse(selectionStates.isBulletSingleList) + XCTAssertFalse(selectionStates.isBulletMultipleList) + XCTAssertFalse(selectionStates.isNumberSingleList) + XCTAssertTrue(selectionStates.isNumberMultipleList) + } + func testHorizontalTemplateButtonSelectionStateFormattedRange() throws { let text = "Testing inner formatted {{cite web | url=https://en.wikipedia.org | title = The '''Free''' Encyclopedia}} template example." mediator.textView.attributedText = NSAttributedString(string: text) @@ -166,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)