@@ -14,17 +14,389 @@ extension MarkdownCodeLanguage.Swift
14
14
}
15
15
}
16
16
}
17
+
18
+ extension SnippetSliceControl
19
+ {
20
+ enum Keyword
21
+ {
22
+ case end
23
+ case hide
24
+ case show
25
+ case slice( String )
26
+ }
27
+ }
28
+ extension SnippetSliceControl . Keyword
29
+ {
30
+ private
31
+ init ( _ text: Substring )
32
+ {
33
+ switch text
34
+ {
35
+ case " end " : self = . end
36
+ case " hide " : self = . hide
37
+ case " show " : self = . show
38
+ default : self = . slice( String . init ( text) )
39
+ }
40
+ }
41
+
42
+ init ? ( lineComment text: borrowing String , skip: Int )
43
+ {
44
+ guard
45
+ let i: String . Index = text. index ( text. startIndex,
46
+ offsetBy: skip,
47
+ limitedBy: text. endIndex)
48
+ else
49
+ {
50
+ fatalError ( " Encountered a line comment with no leading slashes! " )
51
+ }
52
+
53
+ let text : Substring = ( copy text) [ i... ] . drop ( while: \. isWhitespace)
54
+
55
+ guard
56
+ let j: String . Index = text. firstIndex ( of: " . " ) ,
57
+ case " snippet " = text [ ..< j]
58
+ else
59
+ {
60
+ return nil
61
+ }
62
+
63
+ let k : String . Index = text. index ( after: j)
64
+ if let space: String . Index = text [ k... ] . firstIndex ( where: \. isWhitespace)
65
+ {
66
+ guard text [ text. index ( after: space) ... ] . allSatisfy ( \. isWhitespace)
67
+ else
68
+ {
69
+ return nil
70
+ }
71
+
72
+ self . init ( text [ k ..< space] )
73
+ }
74
+ else
75
+ {
76
+ self . init ( text [ k... ] )
77
+ }
78
+ }
79
+ }
80
+ struct SnippetSliceControl
81
+ {
82
+ let keyword : Keyword
83
+ /// The number of leading spaces before the control comment.
84
+ let indent : Int
85
+ /// The UTF-8 offset of the (first) newline before the control comment, or the beginning
86
+ /// of the file if the control comment is at the beginning of the file.
87
+ let before : AbsolutePosition
88
+ /// The UTF-8 offset of the newline after the control comment, assuming it exists.
89
+ let after : AbsolutePosition
90
+ }
91
+
92
+ struct SnippetSlice
93
+ {
94
+ let id : String
95
+ var ranges : [ Range < AbsolutePosition > ]
96
+
97
+ init ( id: String )
98
+ {
99
+ self . id = id
100
+ self . ranges = [ ]
101
+ }
102
+ }
103
+ extension SnippetParser
104
+ {
105
+ struct Slice
106
+ {
107
+ private
108
+ var slice : SnippetSlice
109
+ private
110
+ var start : AbsolutePosition ?
111
+
112
+ init ( id: String , at position: AbsolutePosition )
113
+ {
114
+ self . slice = . init( id: id)
115
+ self . start = position
116
+ }
117
+ }
118
+ }
119
+ extension SnippetParser . Slice
120
+ {
121
+ mutating
122
+ func show( at position: AbsolutePosition )
123
+ {
124
+ if case nil = self . start
125
+ {
126
+ self . start = position
127
+ }
128
+ else
129
+ {
130
+ // TODO: Emit a warning.
131
+ }
132
+ }
133
+
134
+ mutating
135
+ func hide( at position: AbsolutePosition )
136
+ {
137
+ guard
138
+ let start: AbsolutePosition = self . start
139
+ else
140
+ {
141
+ // TODO: Emit a warning.
142
+ return
143
+ }
144
+ // Two ways this check can fail:
145
+ //
146
+ // 1. Something resembling a control comment appears in the snippet abstract.
147
+ // 2. A snippet slice is hidden instantly after it is shown.
148
+ if start < position
149
+ {
150
+ self . slice. ranges. append ( start ..< position)
151
+ self . start = nil
152
+ }
153
+ }
154
+
155
+ consuming
156
+ func end( at position: AbsolutePosition ) -> SnippetSlice
157
+ {
158
+ self . hide ( at: position)
159
+ return self . slice
160
+ }
161
+ }
162
+ struct SnippetParser
163
+ {
164
+ var complete : [ SnippetSlice ]
165
+ var current : Slice ?
166
+
167
+ init ( start position: AbsolutePosition )
168
+ {
169
+ self . complete = [ ]
170
+ self . current = . init( id: " " , at: position)
171
+ }
172
+ }
173
+ extension SnippetParser
174
+ {
175
+ private mutating
176
+ func handle( control: SnippetSliceControl )
177
+ {
178
+ switch control. keyword
179
+ {
180
+ case . end:
181
+ if let current: Slice = self . current
182
+ {
183
+ self . current = nil
184
+ self . complete. append ( current. end ( at: control. before) )
185
+ }
186
+ else
187
+ {
188
+ // TODO: Emit a warning.
189
+ }
190
+
191
+ case . hide:
192
+ if case nil = self . current? . hide ( at: control. before)
193
+ {
194
+ // TODO: Emit a warning.
195
+ }
196
+
197
+ case . show:
198
+ if case nil = self . current? . show ( at: control. after)
199
+ {
200
+ // TODO: Emit a warning.
201
+ }
202
+
203
+ case . slice( let id) :
204
+ if let current: Slice = self . current
205
+ {
206
+ self . current = nil
207
+ self . complete. append ( current. end ( at: control. before) )
208
+ }
209
+
210
+ self . current = . init( id: id, at: control. after)
211
+ }
212
+ }
213
+ }
214
+ extension SnippetParser
215
+ {
216
+ mutating
217
+ func handle( trivia: Trivia , at position: AbsolutePosition )
218
+ {
219
+ var newline : ( position: AbsolutePosition , indent: Int ) ? = nil
220
+ var current : AbsolutePosition = position
221
+ for piece : TriviaPiece in trivia
222
+ {
223
+ let range : Range < AbsolutePosition > = current ..< current + piece. sourceLength
224
+
225
+ defer
226
+ {
227
+ current = range. upperBound
228
+ }
229
+
230
+ let line : String
231
+ let skip : Int
232
+
233
+ switch piece
234
+ {
235
+ case . newlines, . carriageReturnLineFeeds:
236
+ newline = ( position: range. lowerBound, indent: 0 )
237
+ continue
238
+
239
+ case . spaces( let count) :
240
+ // We only care about leading spaces.
241
+ if let indent: Int = newline? . indent
242
+ {
243
+ newline? . indent = indent + count
244
+ }
245
+ continue
246
+
247
+ case . lineComment( let text) :
248
+ line = text
249
+ skip = 2
250
+
251
+ case . docLineComment( let text) :
252
+ line = text
253
+ skip = 3
254
+
255
+ default :
256
+ newline = nil
257
+ continue
258
+ }
259
+
260
+ guard
261
+ let leading: ( position: AbsolutePosition , indent: Int ) = newline
262
+ else
263
+ {
264
+ // We only care about line comments at the beginning of a line.
265
+ continue
266
+ }
267
+
268
+ if let keyword: SnippetSliceControl . Keyword = . init( lineComment: line, skip: skip)
269
+ {
270
+ // We know that line comments always extend to the end of the line.
271
+ // So the start of the next line is the index after the end of the comment.
272
+ self . handle ( control: . init( keyword: keyword,
273
+ indent: leading. indent,
274
+ before: leading. position,
275
+ after: range. upperBound. advanced ( by: 1 ) ) )
276
+ }
277
+ else
278
+ {
279
+ newline = nil
280
+ }
281
+ }
282
+ }
283
+
284
+ consuming
285
+ func finish( at position: AbsolutePosition ) -> [ SnippetSlice ]
286
+ {
287
+ if let current: Slice = self . current
288
+ {
289
+ self . current = nil
290
+ self . complete. append ( current. end ( at: position) )
291
+ }
292
+
293
+ return complete
294
+ }
295
+ }
296
+
297
+ extension MarkdownCodeLanguage . Swift . Highlighter
298
+ {
299
+ public
300
+ func _parse( snippet text: consuming String )
301
+ {
302
+ text. withUTF8
303
+ {
304
+ ( utf8: UnsafeBufferPointer < UInt8 > ) in
305
+
306
+ let parsed : SourceFileSyntax = Parser . parse ( source: utf8)
307
+ var start : AbsolutePosition = parsed. position
308
+ var text : String = " "
309
+ lines:
310
+ for piece : TriviaPiece in parsed. leadingTrivia
311
+ {
312
+ let line : String
313
+ let skip : Int
314
+ switch piece
315
+ {
316
+ case . lineComment( let text) :
317
+ start += piece. sourceLength
318
+ line = text
319
+ skip = 2
320
+
321
+ case . docLineComment( let text) :
322
+ start += piece. sourceLength
323
+ line = text
324
+ skip = 3
325
+
326
+ case . newlines( 1 ) , . carriageReturnLineFeeds( 1 ) :
327
+ start += piece. sourceLength
328
+ continue
329
+
330
+ case . newlines, . carriageReturnLineFeeds:
331
+ start += piece. sourceLength
332
+ break lines
333
+
334
+ default :
335
+ break lines
336
+ }
337
+
338
+ guard
339
+ let i: String . Index = line. index ( line. startIndex,
340
+ offsetBy: skip,
341
+ limitedBy: line. endIndex)
342
+ else
343
+ {
344
+ fatalError ( " Encountered a line comment with no leading slashes! " )
345
+ }
346
+
347
+ text += line [ i... ] . drop ( while: \. isWhitespace)
348
+ text. append ( " \n " )
349
+ }
350
+
351
+ var parser : SnippetParser = . init( start: start)
352
+ for token : TokenSyntax in parsed. tokens ( viewMode: . sourceAccurate)
353
+ {
354
+ parser. handle ( trivia: token. leadingTrivia, at: token. position)
355
+ parser. handle ( trivia: token. trailingTrivia,
356
+ at: token. endPositionBeforeTrailingTrivia)
357
+ }
358
+
359
+ let slices : [ SnippetSlice ] = parser. finish ( at: parsed. endPosition)
360
+
361
+ for slice : SnippetSlice in slices
362
+ {
363
+ print ( " Snippet ' \( slice. id) ': " )
364
+ print ( " -------------------- " )
365
+ for range : Range < AbsolutePosition > in slice. ranges
366
+ {
367
+ let start : Int = range. lowerBound. utf8Offset
368
+ if start >= utf8. endIndex
369
+ {
370
+ // This could happen if the file ends with a control comment and no
371
+ // final newline.
372
+ continue
373
+ }
374
+
375
+ let end : Int = min ( range. upperBound. utf8Offset, utf8. endIndex)
376
+ if end <= start
377
+ {
378
+ continue
379
+ }
380
+
381
+ print ( String ( decoding: utf8 [ start ..< end] , as: Unicode . UTF8. self) )
382
+ }
383
+ print ( " -------------------- " )
384
+ }
385
+ }
386
+ }
387
+ }
17
388
extension MarkdownCodeLanguage . Swift . Highlighter : MarkdownCodeHighlighter
18
389
{
19
390
public
20
- func emit( _ text: String , into binary: inout MarkdownBinaryEncoder )
391
+ func emit( _ text: consuming String , into binary: inout MarkdownBinaryEncoder )
21
392
{
22
393
// Last I checked, SwiftParser already does this internally in its
23
394
// ``String``-based parsing API. Since we need to load the original
24
395
// source text anyway, we might as well use the UTF-8 buffer-based API.
25
- var text : String = text ; text. withUTF8
396
+ text. withUTF8
26
397
{
27
- guard let base: UnsafePointer < UInt8 > = $0. baseAddress
398
+ guard
399
+ let base: UnsafePointer < UInt8 > = $0. baseAddress
28
400
else
29
401
{
30
402
return // empty string
0 commit comments