Skip to content

Commit 3c5cb13

Browse files
committed
messy drafts of a swift syntax-aware Snippet parser
1 parent fdf22cd commit 3c5cb13

File tree

15 files changed

+665
-49
lines changed

15 files changed

+665
-49
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
public
22
protocol MarkdownCodeHighlighter
33
{
4-
func emit(_ text:String, into binary:inout MarkdownBinaryEncoder)
4+
func emit(_ text:consuming String, into binary:inout MarkdownBinaryEncoder)
55
}

Sources/MarkdownPluginSwift/MarkdownCodeLanguage.Swift.Highlighter.swift

Lines changed: 375 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,389 @@ extension MarkdownCodeLanguage.Swift
1414
}
1515
}
1616
}
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+
}
17388
extension MarkdownCodeLanguage.Swift.Highlighter:MarkdownCodeHighlighter
18389
{
19390
public
20-
func emit(_ text:String, into binary:inout MarkdownBinaryEncoder)
391+
func emit(_ text:consuming String, into binary:inout MarkdownBinaryEncoder)
21392
{
22393
// Last I checked, SwiftParser already does this internally in its
23394
// ``String``-based parsing API. Since we need to load the original
24395
// source text anyway, we might as well use the UTF-8 buffer-based API.
25-
var text:String = text ; text.withUTF8
396+
text.withUTF8
26397
{
27-
guard let base:UnsafePointer<UInt8> = $0.baseAddress
398+
guard
399+
let base:UnsafePointer<UInt8> = $0.baseAddress
28400
else
29401
{
30402
return // empty string

0 commit comments

Comments
 (0)