This repository has been archived by the owner on Oct 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
Node.swift
339 lines (279 loc) · 10.8 KB
/
Node.swift
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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import cmark
/// A CommonMark node.
public class Node: Codable {
class var cmark_node_type: cmark_node_type { return CMARK_NODE_NONE }
/// A pointer to the underlying `cmark_node` for the node.
final let cmark_node: OpaquePointer
/// Whether the underlying `cmark_node` should be freed upon deallocation.
var managed: Bool = false
/**
Creates a node from a `cmark_node` pointer.
- Parameter cmark_node: A `cmark_node` pointer.
*/
required init(_ cmark_node: OpaquePointer) {
self.cmark_node = cmark_node
assert(type(of: self) != Node.self)
assert(cmark_node_get_type(cmark_node) == type(of: self).cmark_node_type)
}
convenience init(nonrecursively: Void) {
self.init()
}
convenience init() {
self.init(cmark_node_new(type(of: self).cmark_node_type))
self.managed = true
}
deinit {
guard managed else { return }
cmark_node_free(cmark_node)
}
/**
Creates and returns the `Node` subclass corresponding to
the type of a `cmark_node` pointer.
- Parameter cmark_node: A `cmark_node` pointer.
- Returns: An instance of a `Node` subclass.
*/
static func create(for cmark_node: OpaquePointer!) -> Node? {
guard let cmark_node = cmark_node else { return nil }
switch cmark_node_get_type(cmark_node) {
case CMARK_NODE_DOCUMENT:
return Document(cmark_node)
case CMARK_NODE_BLOCK_QUOTE:
return BlockQuote(cmark_node)
case CMARK_NODE_LIST:
switch cmark_node_get_list_type(cmark_node) {
case CMARK_BULLET_LIST:
return List(cmark_node)
case CMARK_ORDERED_LIST:
return List(cmark_node)
default:
return nil
}
case CMARK_NODE_ITEM:
return List.Item(cmark_node)
case CMARK_NODE_CODE_BLOCK:
return CodeBlock(cmark_node)
case CMARK_NODE_HTML_BLOCK:
return HTMLBlock(cmark_node)
case CMARK_NODE_PARAGRAPH:
return Paragraph(cmark_node)
case CMARK_NODE_HEADING:
return Heading(cmark_node)
case CMARK_NODE_THEMATIC_BREAK:
return ThematicBreak(cmark_node)
case CMARK_NODE_TEXT:
return Text(cmark_node)
case CMARK_NODE_SOFTBREAK:
return SoftLineBreak(cmark_node)
case CMARK_NODE_LINEBREAK:
return HardLineBreak(cmark_node)
case CMARK_NODE_CODE:
return Code(cmark_node)
case CMARK_NODE_HTML_INLINE:
return RawHTML(cmark_node)
case CMARK_NODE_EMPH:
return Emphasis(cmark_node)
case CMARK_NODE_STRONG:
return Strong(cmark_node)
case CMARK_NODE_LINK:
return Link(cmark_node)
case CMARK_NODE_IMAGE:
return Image(cmark_node)
default:
return nil
}
}
func unlink() {
cmark_node_unlink(self.cmark_node)
self.managed = true
}
/// The line and column range of the element in the document.
public var range: ClosedRange<Document.Position> {
let start = Document.Position(line: numericCast(cmark_node_get_start_line(cmark_node)), column: numericCast(cmark_node_get_start_column(cmark_node)))
let end = Document.Position(line: max(start.line, numericCast(cmark_node_get_end_line(cmark_node))), column: max(start.column, numericCast(cmark_node_get_end_column(cmark_node))))
return start...end
}
/// The parent of the element, if any.
public var parent: Node? {
return Node.create(for: cmark_node_parent(cmark_node))
}
// MARK: - Rendering
/// Formats for rendering a document.
public enum RenderingFormat {
/// CommonMark
case commonmark
/// HTML
case html
/// XML
case xml
/// LaTeX
case latex
/// Manpage
case manpage
}
/// Options for rendering a CommonMark document.
public struct RenderingOptions: OptionSet {
public var rawValue: Int32
public init(rawValue: Int32 = CMARK_OPT_DEFAULT) {
self.rawValue = rawValue
}
/**
Render raw HTML and "unsafe" links.
A link is considered to be "unsafe"
if its scheme is `javascript:`, `vbscript:`, or `file:`,
or if its scheme is `data:`
and the MIME type of the encoded data isn't one of the following:
- `image/png`
- `image/gif`
- `image/jpeg`
- `image/webp`
By default,
raw HTML is replaced by a placeholder HTML comment.
Unsafe links are replaced by empty strings.
- Important: This option has an effect only when rendering HTML.
*/
public static let unsafe = Self(rawValue: CMARK_OPT_UNSAFE)
/**
Render softbreak elements as spaces.
- Important: This option has no effect when rendering XML.
*/
public static let noBreaks = Self(rawValue: CMARK_OPT_NOBREAKS)
/**
Render softbreak elements as hard line breaks.
- Important: This option has no effect when rendering XML.
*/
public static let hardBreaks = Self(rawValue: CMARK_OPT_HARDBREAKS)
/**
Include a `data-sourcepos` attribute on all block elements
to map the rendered output to the source input.
- Important: This option has an effect only when rendering HTML or XML.
*/
public static let includeSourcePosition = Self(rawValue: CMARK_OPT_SOURCEPOS)
}
/**
Render a document into a given format with the specified options.
- Parameters:
- format: The rendering format
- options: The rendering options
- width: The column width used to wrap lines for rendered output
(`.commonmark`, `.man`, and `.latex` formats only).
Must be a positive number.
Pass `0` to prevent line wrapping.
- Returns: The rendered text.
*/
public func render(format: RenderingFormat, options: RenderingOptions = [], width: Int = 0) -> String {
precondition(width >= 0)
let cString: UnsafeMutablePointer<CChar>
switch format {
case .commonmark:
cString = cmark_render_commonmark(cmark_node, options.rawValue, Int32(clamping: width))
case .html:
cString = cmark_render_html(cmark_node, options.rawValue)
case .xml:
cString = cmark_render_xml(cmark_node, options.rawValue)
case .latex:
cString = cmark_render_latex(cmark_node, options.rawValue, Int32(clamping: width))
case .manpage:
cString = cmark_render_man(cmark_node, options.rawValue, Int32(clamping: width))
}
defer {
free(cString)
}
return String(cString: cString)
}
// MARK: - Codable
public required convenience init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let commonmark = try container.decode(String.self)
let document = try Document(commonmark, options: [])
let node: Node
switch Self.cmark_node_type {
case CMARK_NODE_DOCUMENT:
node = document
case CMARK_NODE_BLOCK_QUOTE,
CMARK_NODE_LIST,
CMARK_NODE_ITEM,
CMARK_NODE_CODE_BLOCK,
CMARK_NODE_HTML_BLOCK,
CMARK_NODE_CUSTOM_BLOCK,
CMARK_NODE_PARAGRAPH,
CMARK_NODE_HEADING,
CMARK_NODE_THEMATIC_BREAK:
node = try Self.extractRootBlock(from: document, in: container)
case CMARK_NODE_TEXT,
CMARK_NODE_SOFTBREAK,
CMARK_NODE_LINEBREAK,
CMARK_NODE_CODE,
CMARK_NODE_HTML_INLINE,
CMARK_NODE_CUSTOM_INLINE,
CMARK_NODE_EMPH,
CMARK_NODE_STRONG,
CMARK_NODE_LINK,
CMARK_NODE_IMAGE:
node = try Self.extractRootInline(from: document, in: container)
default:
throw DecodingError.dataCorruptedError(in: container, debugDescription: "unsupported node type")
}
// If the extracted node is not managed, then we most likely
// introduced a memory bug in our extraction logic:
assert(
node.managed,
"Expected extracted node to be managed"
)
// Un-assign memory management duties from old owning node:
node.managed = false
self.init(node.cmark_node)
// Re-assign memory management duties to new owning node:
self.managed = true
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(description)
}
private static func extractRootBlock(from document: Document, in container: SingleValueDecodingContainer) throws -> Self {
// Unlink the children from the document node to prevent dangling pointers to the parent.
let documentChildren = document.removeChildren()
guard let block = documentChildren.first as? Self,
documentChildren.count == 1
else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected single block node")
}
assert(block.managed)
return block
}
private static func extractRootInline(from document: Document, in container: SingleValueDecodingContainer) throws -> Self {
// Unlink the children from the document node to prevent dangling pointers to the parent.
let documentChildren = document.removeChildren()
guard let paragraph = documentChildren.first as? Paragraph,
documentChildren.count == 1
else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected single paragraph node")
}
// Unlink the children from the root node to prevent dangling pointers to the parent.
let paragraphChildren = paragraph.removeChildren()
guard let inline = paragraphChildren.first as? Self,
paragraphChildren.count == 1
else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected single inline node")
}
assert(inline.managed)
return inline
}
}
// MARK: - Equatable
extension Node: Equatable {
public static func == (lhs: Node, rhs: Node) -> Bool {
return lhs.cmark_node == rhs.cmark_node
}
}
// MARK: - Hashable
extension Node: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(cmark_node)
}
}
// MARK: - CustomStringConvertible
extension Node: CustomStringConvertible {
public var description: String {
self.render(format: .commonmark)
}
}