mirror of
https://github.com/gonzalezreal/swift-markdown-ui.git
synced 2026-01-18 17:41:20 +01:00
Render HTML line breaks (#221)
This commit is contained in:
25
Sources/MarkdownUI/Parser/HTMLTag.swift
Normal file
25
Sources/MarkdownUI/Parser/HTMLTag.swift
Normal file
@@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
struct HTMLTag {
|
||||
let name: String
|
||||
}
|
||||
|
||||
extension HTMLTag {
|
||||
private enum Constants {
|
||||
static let tagExpression = try! NSRegularExpression(pattern: "<\\/?([a-zA-Z0-9]+)[^>]*>")
|
||||
}
|
||||
|
||||
init?(_ description: String) {
|
||||
guard
|
||||
let match = Constants.tagExpression.firstMatch(
|
||||
in: description,
|
||||
range: NSRange(description.startIndex..., in: description)
|
||||
),
|
||||
let nameRange = Range(match.range(at: 1), in: description)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.name = String(description[nameRange])
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ private struct AttributedStringInlineRenderer {
|
||||
private let baseURL: URL?
|
||||
private let textStyles: InlineTextStyles
|
||||
private var attributes: AttributeContainer
|
||||
private var shouldSkipNextWhitespace = false
|
||||
|
||||
init(baseURL: URL?, textStyles: InlineTextStyles, attributes: AttributeContainer) {
|
||||
self.baseURL = baseURL
|
||||
@@ -55,26 +56,45 @@ private struct AttributedStringInlineRenderer {
|
||||
}
|
||||
|
||||
private mutating func renderText(_ text: String) {
|
||||
var text = text
|
||||
|
||||
if self.shouldSkipNextWhitespace {
|
||||
self.shouldSkipNextWhitespace = false
|
||||
text = text.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
self.result += .init(text, attributes: self.attributes)
|
||||
}
|
||||
|
||||
private mutating func renderSoftBreak() {
|
||||
self.result += .init(" ", attributes: self.attributes)
|
||||
if self.shouldSkipNextWhitespace {
|
||||
self.shouldSkipNextWhitespace = false
|
||||
} else {
|
||||
self.result += .init(" ", attributes: self.attributes)
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func renderLineBreak() {
|
||||
self.result += .init("\n", attributes: self.attributes)
|
||||
}
|
||||
|
||||
mutating func renderCode(_ code: String) {
|
||||
private mutating func renderCode(_ code: String) {
|
||||
self.result += .init(code, attributes: self.textStyles.code.mergingAttributes(self.attributes))
|
||||
}
|
||||
|
||||
mutating func renderHTML(_ html: String) {
|
||||
self.result += .init(html, attributes: self.attributes)
|
||||
private mutating func renderHTML(_ html: String) {
|
||||
let tag = HTMLTag(html)
|
||||
|
||||
switch tag?.name.lowercased() {
|
||||
case "br":
|
||||
self.renderLineBreak()
|
||||
self.shouldSkipNextWhitespace = true
|
||||
default:
|
||||
self.renderText(html)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func renderEmphasis(children: [InlineNode]) {
|
||||
private mutating func renderEmphasis(children: [InlineNode]) {
|
||||
let savedAttributes = self.attributes
|
||||
self.attributes = self.textStyles.emphasis.mergingAttributes(self.attributes)
|
||||
|
||||
@@ -85,7 +105,7 @@ private struct AttributedStringInlineRenderer {
|
||||
self.attributes = savedAttributes
|
||||
}
|
||||
|
||||
mutating func renderStrong(children: [InlineNode]) {
|
||||
private mutating func renderStrong(children: [InlineNode]) {
|
||||
let savedAttributes = self.attributes
|
||||
self.attributes = self.textStyles.strong.mergingAttributes(self.attributes)
|
||||
|
||||
@@ -96,7 +116,7 @@ private struct AttributedStringInlineRenderer {
|
||||
self.attributes = savedAttributes
|
||||
}
|
||||
|
||||
mutating func renderStrikethrough(children: [InlineNode]) {
|
||||
private mutating func renderStrikethrough(children: [InlineNode]) {
|
||||
let savedAttributes = self.attributes
|
||||
self.attributes = self.textStyles.strikethrough.mergingAttributes(self.attributes)
|
||||
|
||||
@@ -107,7 +127,7 @@ private struct AttributedStringInlineRenderer {
|
||||
self.attributes = savedAttributes
|
||||
}
|
||||
|
||||
mutating func renderLink(destination: String, children: [InlineNode]) {
|
||||
private mutating func renderLink(destination: String, children: [InlineNode]) {
|
||||
let savedAttributes = self.attributes
|
||||
self.attributes = self.textStyles.link.mergingAttributes(self.attributes)
|
||||
self.attributes.link = URL(string: destination, relativeTo: self.baseURL)
|
||||
@@ -119,7 +139,7 @@ private struct AttributedStringInlineRenderer {
|
||||
self.attributes = savedAttributes
|
||||
}
|
||||
|
||||
mutating func renderImage(source: String, children: [InlineNode]) {
|
||||
private mutating func renderImage(source: String, children: [InlineNode]) {
|
||||
// AttributedString does not support images
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ private struct TextInlineRenderer {
|
||||
private let textStyles: InlineTextStyles
|
||||
private let images: [String: Image]
|
||||
private let attributes: AttributeContainer
|
||||
private var shouldSkipNextWhitespace = false
|
||||
|
||||
init(
|
||||
baseURL: URL?,
|
||||
@@ -46,20 +47,65 @@ private struct TextInlineRenderer {
|
||||
|
||||
private mutating func render(_ inline: InlineNode) {
|
||||
switch inline {
|
||||
case .text(let content):
|
||||
self.renderText(content)
|
||||
case .softBreak:
|
||||
self.renderSoftBreak()
|
||||
case .html(let content):
|
||||
self.renderHTML(content)
|
||||
case .image(let source, _):
|
||||
if let image = self.images[source] {
|
||||
self.result = self.result + Text(image)
|
||||
}
|
||||
self.renderImage(source)
|
||||
default:
|
||||
self.result =
|
||||
self.result
|
||||
+ Text(
|
||||
inline.renderAttributedString(
|
||||
baseURL: self.baseURL,
|
||||
textStyles: self.textStyles,
|
||||
attributes: self.attributes
|
||||
)
|
||||
)
|
||||
self.defaultRender(inline)
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func renderText(_ text: String) {
|
||||
var text = text
|
||||
|
||||
if self.shouldSkipNextWhitespace {
|
||||
self.shouldSkipNextWhitespace = false
|
||||
text = text.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
|
||||
}
|
||||
|
||||
self.defaultRender(.text(text))
|
||||
}
|
||||
|
||||
private mutating func renderSoftBreak() {
|
||||
if self.shouldSkipNextWhitespace {
|
||||
self.shouldSkipNextWhitespace = false
|
||||
} else {
|
||||
self.defaultRender(.softBreak)
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func renderHTML(_ html: String) {
|
||||
let tag = HTMLTag(html)
|
||||
|
||||
switch tag?.name.lowercased() {
|
||||
case "br":
|
||||
self.defaultRender(.lineBreak)
|
||||
self.shouldSkipNextWhitespace = true
|
||||
default:
|
||||
self.defaultRender(.html(html))
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func renderImage(_ source: String) {
|
||||
if let image = self.images[source] {
|
||||
self.result = self.result + Text(image)
|
||||
}
|
||||
}
|
||||
|
||||
private mutating func defaultRender(_ inline: InlineNode) {
|
||||
self.result =
|
||||
self.result
|
||||
+ Text(
|
||||
inline.renderAttributedString(
|
||||
baseURL: self.baseURL,
|
||||
textStyles: self.textStyles,
|
||||
attributes: self.attributes
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
40
Tests/MarkdownUITests/HTMLTagTests.swift
Normal file
40
Tests/MarkdownUITests/HTMLTagTests.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import MarkdownUI
|
||||
|
||||
final class HTMLTagTests: XCTestCase {
|
||||
func testInvalidTag() {
|
||||
XCTAssertNil(HTMLTag(""))
|
||||
XCTAssertNil(HTMLTag("foo"))
|
||||
XCTAssertNil(HTMLTag("<"))
|
||||
XCTAssertNil(HTMLTag("<>"))
|
||||
}
|
||||
|
||||
func testOpeningTag() {
|
||||
// given
|
||||
let tag = HTMLTag("<sub>")
|
||||
|
||||
// then
|
||||
XCTAssertEqual("sub", tag?.name)
|
||||
}
|
||||
|
||||
func testOpeningTagWithAttributes() {
|
||||
// given
|
||||
let tag = HTMLTag(
|
||||
"<img src=\"img_girl.jpg\" alt=\"Girl in a jacket\" width=\"500\" height=\"600\">"
|
||||
)
|
||||
|
||||
// then
|
||||
XCTAssertEqual("img", tag?.name)
|
||||
}
|
||||
|
||||
func testClosingTag() {
|
||||
let tag = HTMLTag("</sub>")
|
||||
XCTAssertEqual(tag?.name, "sub")
|
||||
}
|
||||
|
||||
func testSelfClosingTag() {
|
||||
XCTAssertEqual("br", HTMLTag("<br />")?.name)
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,10 @@
|
||||
Visit https://github.com.
|
||||
|
||||
Use `git status` to list all new or modified files that haven't yet been committed.
|
||||
|
||||
You can insert a line break<br>
|
||||
using the HTML `<br>`
|
||||
<br> tag.
|
||||
"""#
|
||||
}
|
||||
.border(Color.accentColor)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 93 KiB |
Reference in New Issue
Block a user