Skip to content

Commit cff2349

Browse files
Add a preprocessor to the PackageToJS plugin
1 parent 561b781 commit cff2349

File tree

3 files changed

+446
-3
lines changed

3 files changed

+446
-3
lines changed

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,8 @@ struct PackagingPlanner {
274274
output: outputDir.appending(path: output).path
275275
) {
276276
var content = try String(contentsOf: inputPath, encoding: .utf8)
277-
for (key, value) in substitutions {
278-
content = content.replacingOccurrences(of: key, with: value)
279-
}
277+
let options = PreprocessOptions(substitutions: substitutions)
278+
content = try preprocess(source: content, options: options)
280279
try content.write(toFile: $0.output, atomically: true, encoding: .utf8)
281280
}
282281
}
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/// Preprocesses a source file
2+
///
3+
/// The preprocessor takes a source file and interprets
4+
/// the following directives:
5+
///
6+
/// - `/* #if <condition> */`
7+
/// - `/* #else */`
8+
/// - `/* #endif */`
9+
/// - `@VARIABLE@`
10+
///
11+
/// The condition is a boolean expression that can use the variables
12+
/// defined in the `options`. Variable names must be `[a-zA-Z0-9_]+`.
13+
/// Contents between `if-else-endif` blocks will be included or excluded
14+
/// based on the condition like C's `#if` directive.
15+
///
16+
/// `@VARIABLE@` will be substituted with the value of the variable.
17+
///
18+
/// The preprocessor will return the preprocessed source code.
19+
func preprocess(source: String, options: PreprocessOptions) throws -> String {
20+
let tokens = try Preprocessor.tokenize(source: source)
21+
let parsed = try Preprocessor.parse(tokens: tokens, source: source, options: options)
22+
return try Preprocessor.preprocess(parsed: parsed, source: source, options: options)
23+
}
24+
25+
struct PreprocessOptions {
26+
/// The variables to replace in the source code
27+
var variables: [String: Bool] = [:]
28+
/// The variables to substitute in the source code
29+
var substitutions: [String: String] = [:]
30+
}
31+
32+
private struct Preprocessor {
33+
enum Token: Equatable {
34+
case `if`(condition: String)
35+
case `else`
36+
case `endif`
37+
case block(String)
38+
}
39+
40+
struct TokenInfo {
41+
let token: Token
42+
let position: String.Index
43+
}
44+
45+
struct PreprocessorError: Error {
46+
let message: String
47+
let source: String
48+
let line: Int
49+
let column: Int
50+
51+
init(message: String, source: String, line: Int, column: Int) {
52+
self.message = message
53+
self.source = source
54+
self.line = line
55+
self.column = column
56+
}
57+
58+
init(message: String, source: String, index: String.Index) {
59+
func consumeLineColumn(from index: String.Index, in source: String) -> (Int, Int) {
60+
var line = 1
61+
var column = 1
62+
for char in source[..<index] {
63+
if char == "\n" {
64+
line += 1
65+
column = 1
66+
} else {
67+
column += 1
68+
}
69+
}
70+
return (line, column)
71+
}
72+
self.message = message
73+
self.source = source
74+
let (line, column) = consumeLineColumn(from: index, in: source)
75+
self.line = line
76+
self.column = column
77+
}
78+
79+
static func expected(
80+
_ expected: CustomStringConvertible, at index: String.Index, in source: String
81+
) -> PreprocessorError {
82+
return PreprocessorError(
83+
message: "Expected \(expected) at \(index)", source: source, index: index)
84+
}
85+
86+
static func unexpected(token: Token, at index: String.Index, in source: String)
87+
-> PreprocessorError
88+
{
89+
return PreprocessorError(
90+
message: "Unexpected token \(token) at \(index)", source: source, index: index)
91+
}
92+
93+
static func eof(at index: String.Index, in source: String) -> PreprocessorError {
94+
return PreprocessorError(
95+
message: "Unexpected end of input", source: source, index: index)
96+
}
97+
}
98+
99+
static func tokenize(source: String) throws -> [TokenInfo] {
100+
var cursor = source.startIndex
101+
var tokens: [TokenInfo] = []
102+
103+
var bufferStart = cursor
104+
105+
func consume(_ count: Int = 1) {
106+
cursor = source.index(cursor, offsetBy: count)
107+
}
108+
109+
func takeIdentifier() throws -> String {
110+
var identifier = ""
111+
var char = try peek()
112+
while ["a"..."z", "A"..."Z", "0"..."9"].contains(where: { $0.contains(char) })
113+
|| char == "_"
114+
{
115+
identifier.append(char)
116+
consume()
117+
char = try peek()
118+
}
119+
return identifier
120+
}
121+
122+
func expect(_ expected: Character) throws {
123+
guard try peek() == expected else {
124+
throw PreprocessorError.expected(expected, at: cursor, in: source)
125+
}
126+
consume()
127+
}
128+
129+
func expect(_ expected: String) throws {
130+
guard
131+
let endIndex = source.index(
132+
cursor, offsetBy: expected.count, limitedBy: source.endIndex)
133+
else {
134+
throw PreprocessorError.eof(at: cursor, in: source)
135+
}
136+
guard source[cursor..<endIndex] == expected else {
137+
throw PreprocessorError.expected(expected, at: cursor, in: source)
138+
}
139+
consume(expected.count)
140+
}
141+
142+
func peek() throws -> Character {
143+
guard cursor < source.endIndex else {
144+
throw PreprocessorError.eof(at: cursor, in: source)
145+
}
146+
return source[cursor]
147+
}
148+
149+
func peek2() throws -> (Character, Character) {
150+
guard cursor < source.endIndex, source.index(after: cursor) < source.endIndex else {
151+
throw PreprocessorError.eof(at: cursor, in: source)
152+
}
153+
let char1 = source[cursor]
154+
let char2 = source[source.index(after: cursor)]
155+
return (char1, char2)
156+
}
157+
158+
func addToken(_ token: Token, at position: String.Index) {
159+
tokens.append(.init(token: token, position: position))
160+
}
161+
162+
func flushBufferToken() {
163+
guard bufferStart < cursor else { return }
164+
addToken(.block(String(source[bufferStart..<cursor])), at: bufferStart)
165+
bufferStart = cursor
166+
}
167+
168+
while cursor < source.endIndex {
169+
guard case .some(("/", "*")) = try? peek2() else {
170+
consume()
171+
continue
172+
}
173+
let directiveStart = cursor
174+
// Push the current buffer to the tokens
175+
flushBufferToken()
176+
177+
consume(2)
178+
// Start of a block comment
179+
guard try peek2() == (" ", "#") else {
180+
continue
181+
}
182+
consume(2)
183+
// Start of a directive
184+
let directiveSource = source[cursor...]
185+
let directives: [String: () throws -> Token] = [
186+
"if": {
187+
try expect(" ")
188+
let condition = try takeIdentifier()
189+
return .if(condition: condition)
190+
},
191+
"else": {
192+
return .else
193+
},
194+
"endif": {
195+
return .endif
196+
},
197+
]
198+
var token: Token?
199+
for (keyword, factory) in directives {
200+
guard directiveSource.hasPrefix(keyword) else {
201+
continue
202+
}
203+
consume(keyword.count)
204+
token = try factory()
205+
try expect(" */")
206+
}
207+
guard let token = token else {
208+
throw PreprocessorError(
209+
message: "Unexpected directive", source: source, index: cursor)
210+
}
211+
addToken(token, at: directiveStart)
212+
bufferStart = cursor
213+
}
214+
flushBufferToken()
215+
return tokens
216+
}
217+
218+
enum ParseResult {
219+
case block(String)
220+
indirect case `if`(
221+
condition: String, then: [ParseResult], else: [ParseResult], position: String.Index)
222+
}
223+
224+
static func parse(tokens: [TokenInfo], source: String, options: PreprocessOptions) throws
225+
-> [ParseResult]
226+
{
227+
var cursor = tokens.startIndex
228+
229+
func consume() {
230+
cursor = tokens.index(after: cursor)
231+
}
232+
233+
func parse() throws -> ParseResult {
234+
switch tokens[cursor].token {
235+
case .block(let content):
236+
consume()
237+
return .block(content)
238+
case .if(let condition):
239+
let ifPosition = tokens[cursor].position
240+
consume()
241+
var then: [ParseResult] = []
242+
var `else`: [ParseResult] = []
243+
while cursor < tokens.endIndex && tokens[cursor].token != .else
244+
&& tokens[cursor].token != .endif
245+
{
246+
then.append(try parse())
247+
}
248+
if case .else = tokens[cursor].token {
249+
consume()
250+
while cursor < tokens.endIndex && tokens[cursor].token != .endif {
251+
`else`.append(try parse())
252+
}
253+
}
254+
guard case .endif = tokens[cursor].token else {
255+
throw PreprocessorError.unexpected(
256+
token: tokens[cursor].token, at: tokens[cursor].position, in: source)
257+
}
258+
consume()
259+
return .if(condition: condition, then: then, else: `else`, position: ifPosition)
260+
case .else, .endif:
261+
throw PreprocessorError.unexpected(
262+
token: tokens[cursor].token, at: tokens[cursor].position, in: source)
263+
}
264+
}
265+
var results: [ParseResult] = []
266+
while cursor < tokens.endIndex {
267+
results.append(try parse())
268+
}
269+
return results
270+
}
271+
272+
static func preprocess(parsed: [ParseResult], source: String, options: PreprocessOptions) throws
273+
-> String
274+
{
275+
var result = ""
276+
277+
func appendBlock(content: String) {
278+
// Apply substitutions
279+
var substitutedContent = content
280+
for (key, value) in options.substitutions {
281+
substitutedContent = substitutedContent.replacingOccurrences(
282+
of: "@" + key + "@", with: value)
283+
}
284+
result.append(substitutedContent)
285+
}
286+
287+
func evaluate(parsed: ParseResult) throws {
288+
switch parsed {
289+
case .block(let content):
290+
appendBlock(content: content)
291+
case .if(let condition, let then, let `else`, let position):
292+
guard let condition = options.variables[condition] else {
293+
throw PreprocessorError.unexpected(
294+
token: .if(condition: condition), at: position, in: source)
295+
}
296+
let blocks = condition ? then : `else`
297+
for block in blocks {
298+
try evaluate(parsed: block)
299+
}
300+
}
301+
}
302+
for parsed in parsed {
303+
try evaluate(parsed: parsed)
304+
}
305+
return result
306+
}
307+
}

0 commit comments

Comments
 (0)