-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathGutenbergContentParser.swift
More file actions
177 lines (163 loc) · 5.78 KB
/
Copy pathGutenbergContentParser.swift
File metadata and controls
177 lines (163 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import Foundation
import SwiftSoup
public class GutenbergParsedBlock {
public let name: String
public var elements: Elements
public var blocks: [GutenbergParsedBlock]
public weak var parentBlock: GutenbergParsedBlock?
public let isCloseTag: Bool
public var attributes: [String: Any] {
get {
guard let data = self.attributesData.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments),
let attributes = jsonObject as? [String: Any]
else {
return [:]
}
return attributes
}
set(newValue) {
guard let data = try? JSONSerialization.data(withJSONObject: newValue, options: .sortedKeys),
let attributes = String(data: data, encoding: .utf8)
else {
return
}
self.attributesData = attributes
// Update comment tag data with new attributes
try! self.comment.attr("comment", " \(self.name) \(attributes) ")
}
}
public var content: String {
get {
(try? elements.outerHtml()) ?? ""
}
}
private var comment: SwiftSoup.Comment
private var attributesData: String
public init?(comment: SwiftSoup.Comment, parentBlock: GutenbergParsedBlock? = nil) {
let data = comment.getData().trim()
if let separatorRange = data.range(of: " ") {
self.name = String(data[data.startIndex..<separatorRange.lowerBound])
self.attributesData = String(data[separatorRange.upperBound..<data.endIndex])
} else {
self.name = data
self.attributesData = ""
}
self.comment = comment
self.elements = SwiftSoup.Elements()
self.blocks = []
self.isCloseTag = self.name.hasPrefix("/")
if !self.isCloseTag {
self.parentBlock = parentBlock
parentBlock?.blocks.append(self)
}
}
}
/// Parses content generated in the Gutenberg editor to allow modifications.
///
/// # Parse content
///
/// ```
/// let block = """
/// <!-- wp:block {"id":1} -->
/// <div class="wp-block"><p>Hello world!</p></div>
/// <!-- /wp:block -->
/// """
/// let parser = GutenbergContentParser(for: block)
/// ```
///
/// # Get blocks
///
/// ```
/// let galleryBlocks = parser.blocks.filter { $0.name == "wp:gallery" }
/// let nestedImageBlocks = galleryBlocks[0].blocks.filter { $0.name == "wp:image" }
/// ```
///
/// > Note: All parsed blocks are in the list, including nested blocks.
///
/// ```
/// let allImageBlocks = parser.blocks.filter { $0.name == "wp:gallery" }
/// ```
///
/// # Modify an attribute
///
/// ```
/// let block = parser.blocks[0]
/// block.attributes["newId"] = 1001
/// ```
///
/// # Modify HTML
///
/// ```
/// let block = parser.blocks[0]
/// try! block.elements.select("img").first()?.attr("src", "remote-url")
/// ```
///
/// More information about querying HTML can be found in [SwiftSoap documentation](https://github.com/scinfu/SwiftSoup?tab=readme-ov-file#use-selector-syntax-to-find-elements).
///
/// # Generate HTML content
///
/// ```
/// let contentHTML = parser.html()
/// ```
///
public class GutenbergContentParser {
public var blocks: [GutenbergParsedBlock]
private let htmlDocument: Document?
public init(for content: String) {
self.htmlDocument = try? SwiftSoup.parseBodyFragment(content)
.outputSettings(OutputSettings().prettyPrint(pretty: false))
self.blocks = []
guard let htmlContent = self.htmlDocument?.body() else {
return
}
traverseChildNodes(element: htmlContent)
}
public func html() -> String {
guard let body = self.htmlDocument?.body() else {
return ""
}
// SwiftSoup 2.12+ serializes unchanged nodes from a cached copy of the
// original source, so the attribute mutations the processors make on
// nested elements aren't reflected in the output. Replacing each
// top-level element with a copy marks its subtree dirty and forces a
// re-render, while the surrounding comment and text nodes (the Gutenberg
// block delimiters) are emitted from their original bytes.
for element in body.children().array() {
guard let clone = try? element.copy() as? Element else {
continue
}
try? element.replaceWith(clone)
}
return (try? body.html()) ?? ""
}
private func traverseChildNodes(element: Element, parentBlock: GutenbergParsedBlock? = nil) {
var currentBlock: GutenbergParsedBlock?
element.getChildNodes()
.forEach { node in
switch node {
// Convert comment tag into block
case let comment as SwiftSoup.Comment:
guard let block = GutenbergParsedBlock(comment: comment, parentBlock: parentBlock) else {
return
}
// Identify close tag
if let currrentBlock = currentBlock, block.name == "/\(currrentBlock.name)" {
currentBlock = nil
return
}
self.blocks.append(block)
currentBlock = block
// Insert HTML elements into block being processed
case let element as SwiftSoup.Element:
if let currentBlock {
currentBlock.elements.add(element)
}
if element.childNodeSize() > 0 {
traverseChildNodes(element: element, parentBlock: currentBlock ?? parentBlock)
}
default: break
}
}
}
}