Render HTML line breaks (#221)

This commit is contained in:
Guille Gonzalez
2023-04-15 10:46:33 +02:00
committed by GitHub
parent 078d9de879
commit 3830c4c77b
6 changed files with 156 additions and 21 deletions

View 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])
}
}

View File

@@ -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
}
}

View File

@@ -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
)
)
}
}

View 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)
}
}

View File

@@ -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