-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance native Markdown support #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0444922
ff43f6b
e8c0e65
c7be190
863c3e5
77571c5
1d4d85c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import Foundation | ||
|
|
||
| extension NativeEditorMarkdownParser { | ||
| static func commonMarkBlock( | ||
| in lines: [String], | ||
| startingAt index: Array<String>.Index | ||
| ) -> (block: NativeEditorBlock, endIndex: Array<String>.Index)? { | ||
| indentedCodeBlock(in: lines, startingAt: index) ?? | ||
| setextHeadingBlock(in: lines, startingAt: index) | ||
| } | ||
|
|
||
| private static func indentedCodeBlock( | ||
| in lines: [String], | ||
| startingAt index: Array<String>.Index | ||
| ) -> (block: NativeEditorBlock, endIndex: Array<String>.Index)? { | ||
| let trimmedLine = lines[index].trimmingCharacters(in: .whitespaces) | ||
| if isIndentedListItem(trimmedLine) { | ||
| return nil | ||
| } | ||
| guard let firstLine = indentedCodeLine(from: lines[index]) else { return nil } | ||
|
|
||
| var content = [firstLine] | ||
| var currentIndex = lines.index(after: index) | ||
| while currentIndex < lines.endIndex { | ||
| let line = lines[currentIndex] | ||
| if line.trimmingCharacters(in: .whitespaces).isEmpty { | ||
| content.append("") | ||
| } else if let codeLine = indentedCodeLine(from: line) { | ||
| content.append(codeLine) | ||
| } else { | ||
| break | ||
| } | ||
| currentIndex = lines.index(after: currentIndex) | ||
| } | ||
|
|
||
| while content.last?.isEmpty == true { | ||
| content.removeLast() | ||
| } | ||
| let block = NativeEditorBlock( | ||
| kind: .codeBlock(language: nil), | ||
| text: AttributedString(content.joined(separator: "\n")), | ||
| alignment: .left | ||
| ) | ||
| return (block, currentIndex) | ||
| } | ||
|
|
||
| private static func indentedCodeLine(from line: String) -> String? { | ||
| var columns = 0 | ||
| var index = line.startIndex | ||
| while index < line.endIndex, columns < 4 { | ||
| switch line[index] { | ||
| case " ": columns += 1 | ||
| case "\t": columns = 4 | ||
| default: return nil | ||
| } | ||
| index = line.index(after: index) | ||
| } | ||
| guard columns >= 4 else { return nil } | ||
| return String(line[index...]) | ||
| } | ||
|
|
||
| private static func isIndentedListItem(_ line: String) -> Bool { | ||
| guard let kind = inputRule(from: line)?.kind else { return false } | ||
| switch kind { | ||
| case .bulletListItem, .orderedListItem, .taskListItem: | ||
| return true | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| private static func setextHeadingBlock( | ||
| in lines: [String], | ||
| startingAt index: Array<String>.Index | ||
| ) -> (block: NativeEditorBlock, endIndex: Array<String>.Index)? { | ||
| let underlineIndex = lines.index(after: index) | ||
| let text = lines[index].trimmingCharacters(in: .whitespaces) | ||
| guard underlineIndex < lines.endIndex, | ||
| text.isEmpty == false, | ||
| inputRule(from: text) == nil, | ||
| let level = setextHeadingLevel(from: lines[underlineIndex]) else { | ||
| return nil | ||
| } | ||
|
|
||
| let block = NativeEditorBlock( | ||
| kind: .heading(level: level), | ||
| text: inlineText(from: text), | ||
| alignment: .left | ||
| ) | ||
| return (block, lines.index(after: underlineIndex)) | ||
| } | ||
|
|
||
| private static func setextHeadingLevel(from line: String) -> Int? { | ||
| let underline = line.trimmingCharacters(in: .whitespaces) | ||
| guard underline.isEmpty == false else { return nil } | ||
| if underline.allSatisfy({ $0 == "=" }) { | ||
| return 1 | ||
| } | ||
| if underline.allSatisfy({ $0 == "-" }) { | ||
| return 2 | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| import Foundation | ||
|
|
||
| extension NativeEditorMarkdownParser { | ||
| static func escapedMarkdownPlainText(_ text: String) -> String { | ||
| let escapableCharacters: Set<Character> = ["\\", "`", "*", "_", "[", "]", "<", "~", "!"] | ||
| return text.reduce(into: "") { result, character in | ||
| if escapableCharacters.contains(character) { | ||
| result.append("\\") | ||
| } | ||
| result.append(character) | ||
| } | ||
| } | ||
|
|
||
| static func unescapedMarkdownPlainText(_ text: String) -> String { | ||
| var result = "" | ||
| var index = text.startIndex | ||
| while index < text.endIndex { | ||
| let character = text[index] | ||
| let nextIndex = text.index(after: index) | ||
| if character == "\\", nextIndex < text.endIndex, markdownEscapableCharacters.contains(text[nextIndex]) { | ||
| result.append(text[nextIndex]) | ||
| index = text.index(after: nextIndex) | ||
| } else { | ||
| result.append(character) | ||
| index = nextIndex | ||
| } | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| static func escapedBlockLeadingMarkdown(in text: String) -> String { | ||
| text.split(separator: "\n", omittingEmptySubsequences: false) | ||
| .map(escapingMarkdownBlockPrefix) | ||
| .joined(separator: "\n") | ||
| } | ||
|
|
||
| static func firstUnescapedRange( | ||
| of delimiter: String, | ||
| in markdown: Substring, | ||
| startingAt startIndex: String.Index? = nil | ||
| ) -> Range<String.Index>? { | ||
| var searchStart = startIndex ?? markdown.startIndex | ||
| while searchStart < markdown.endIndex, | ||
| let range = markdown[searchStart...].range(of: delimiter) { | ||
| if isEscapedMarkdownCharacter(at: range.lowerBound, in: markdown) == false { | ||
| return range | ||
| } | ||
| searchStart = range.upperBound | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| static func isEscapedMarkdownCharacter( | ||
| at index: String.Index, | ||
| in markdown: Substring | ||
| ) -> Bool { | ||
| var slashCount = 0 | ||
| var currentIndex = index | ||
| while currentIndex > markdown.startIndex { | ||
| let previousIndex = markdown.index(before: currentIndex) | ||
| guard markdown[previousIndex] == "\\" else { break } | ||
| slashCount += 1 | ||
| currentIndex = previousIndex | ||
| } | ||
| return slashCount.isMultiple(of: 2) == false | ||
| } | ||
|
|
||
| private static func escapingMarkdownBlockPrefix(_ line: Substring) -> String { | ||
| let text = String(line) | ||
| let fixedPrefixes = ["# ", "## ", "### ", "#### ", "##### ", "###### ", "> ", "- ", "+ "] | ||
| let exactMarkers = ["---", "***", "___"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only treats exact Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only treats exact Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only treats exact Useful? React with 👍 / 👎. |
||
| let startsFixedSyntax = fixedPrefixes.contains { text.hasPrefix($0) } | ||
| let startsFence = text.hasPrefix("```") || text.hasPrefix("~~~") | ||
| let setextCandidate = text.trimmingCharacters(in: .whitespaces) | ||
| let isSetextUnderline = setextCandidate.isEmpty == false && | ||
| (setextCandidate.allSatisfy { $0 == "-" } || setextCandidate.allSatisfy { $0 == "=" }) | ||
| let isExactMarker = exactMarkers.contains(text) | ||
|
|
||
| if let orderedListDotIndex = orderedListDotIndex(in: text) { | ||
| var escaped = text | ||
| escaped.insert("\\", at: orderedListDotIndex) | ||
| return escaped | ||
| } | ||
|
|
||
| if isSetextUnderline, let markerIndex = text.firstIndex(where: { $0.isWhitespace == false }) { | ||
| var escaped = text | ||
| escaped.insert("\\", at: markerIndex) | ||
| return escaped | ||
| } | ||
|
|
||
| guard startsFixedSyntax || startsFence || isExactMarker else { return text } | ||
| return "\\\(text)" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When paragraph text starts with an ordered-list marker, this branch emits a leading escape before the digit, e.g. Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When paragraph text starts with an ordered-list marker, this branch emits a leading escape before the digit, e.g. Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When paragraph text starts with an ordered-list marker, this branch emits a leading escape before the digit, e.g. Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| private static func orderedListDotIndex(in text: String) -> String.Index? { | ||
| guard let dotIndex = text.firstIndex(of: "."), | ||
| text.distance(from: text.startIndex, to: dotIndex) <= 4, | ||
| Int(text[..<dotIndex]) != nil else { | ||
| return nil | ||
| } | ||
| let spaceIndex = text.index(after: dotIndex) | ||
| return spaceIndex < text.endIndex && text[spaceIndex] == " " ? dotIndex : nil | ||
| } | ||
|
|
||
| private static let markdownEscapableCharacters: Set<Character> = [ | ||
| "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", ":", ";", | ||
| "<", "=", ">", "?", "@", "[", "\\", "]", "^", "_", "`", "{", "|", "}", "~" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import Foundation | ||
|
|
||
| extension NativeEditorMarkdownParser { | ||
| static func removingLeadingYAMLFrontMatter(from markdown: String) -> String { | ||
| var start = markdown.startIndex | ||
| while start < markdown.endIndex, markdown[start].isWhitespace { | ||
| start = markdown.index(after: start) | ||
| } | ||
|
|
||
| let lines = markdown[start...].split(separator: "\n", omittingEmptySubsequences: false) | ||
| guard lines.first?.trimmingCharacters(in: .whitespacesAndNewlines) == "---", | ||
| let closingIndex = lines.dropFirst().firstIndex(where: { | ||
| $0.trimmingCharacters(in: .whitespacesAndNewlines) == "---" | ||
| }) else { | ||
| return markdown | ||
| } | ||
|
|
||
| let content = lines[lines.index(after: closingIndex)...].joined(separator: "\n") | ||
| guard let contentStart = content.firstIndex(where: { $0.isWhitespace == false }) else { return "" } | ||
| return String(content[contentStart...]) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.