-
-
Notifications
You must be signed in to change notification settings - Fork 399
Expand file tree
/
Copy pathJavaScriptText.swift
More file actions
118 lines (108 loc) · 4.99 KB
/
Copy pathJavaScriptText.swift
File metadata and controls
118 lines (108 loc) · 4.99 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
//
// JavaScriptText.swift
// TableProJavaScriptText
//
import Foundation
/// Shell text written around a name the server chose, each piece escaped for where it stands.
///
/// JSON only requires the C0 controls to be escaped inside a string. JavaScript also ends a line
/// at U+2028 and U+2029, and the editor's statement scanner ends one wherever
/// `Character.isNewline` does, which adds U+0085, VT and FF. Written raw, any of them ended the
/// comment or the string around a name early, and the rest of the name was read as script.
public enum JavaScriptText {
private static let quote = UInt8(ascii: "\"")
private static let backslash = UInt8(ascii: "\\")
/// The escape for a character that ends a line or cannot be seen, or nil for any other.
public static func lineBreakingEscape(_ scalar: Unicode.Scalar) -> String? {
switch scalar {
case "\n": return "\\n"
case "\r": return "\\r"
case "\t": return "\\t"
default:
let value = scalar.value
guard value < 0x20 || (0x7F ... 0x9F).contains(value) || value == 0x2028 || value == 0x2029 else {
return nil
}
return String(format: "\\u%04x", value)
}
}
/// A double-quoted string literal on one line, which JSON and JavaScript both read back as `value`.
public static func stringLiteral(_ value: String) -> String {
var output: [UInt8] = [quote]
output.reserveCapacity(value.utf8.count + 2)
let wasContiguous = value.utf8.withContiguousStorageIfAvailable { appendEscaped($0, to: &output) } != nil
if !wasContiguous {
Array(value.utf8).withUnsafeBufferPointer { appendEscaped($0, to: &output) }
}
output.append(quote)
return String(decoding: output, as: UTF8.self) // swiftlint:disable:this optional_data_string_conversion
}
/// A `//` comment that ends where its own line does.
public static func lineComment(_ text: String) -> String {
var line = "//"
guard !text.isEmpty else { return line }
line.append(" ")
for scalar in text.unicodeScalars {
if let escape = lineBreakingEscape(scalar) {
line.append(escape)
} else {
line.unicodeScalars.append(scalar)
}
}
return line
}
/// Whether `name` can follow a `.` as it is: ASCII letters, digits and `_`, not led by a digit.
///
/// Checked byte by byte, because a `Character` is a whole grapheme cluster, and a Unicode
/// Prepend letter joined to a `(` or `;` answers `isLetter` for the pair.
public static func isPlainIdentifier(_ name: String) -> Bool {
guard let first = name.utf8.first, !isASCIIDigit(first) else { return false }
return name.utf8.allSatisfy { isASCIILetter($0) || isASCIIDigit($0) || $0 == UInt8(ascii: "_") }
}
private static func isASCIILetter(_ byte: UInt8) -> Bool {
(UInt8(ascii: "a") ... UInt8(ascii: "z")).contains(byte) || (UInt8(ascii: "A") ... UInt8(ascii: "Z")).contains(byte)
}
private static func isASCIIDigit(_ byte: UInt8) -> Bool {
(UInt8(ascii: "0") ... UInt8(ascii: "9")).contains(byte)
}
/// Runs on the UTF-8 bytes, because an export calls it once per value and a loop over
/// `unicodeScalars` measured twice as slow. Every other byte is copied as it is.
private static func appendEscaped(_ bytes: UnsafeBufferPointer<UInt8>, to output: inout [UInt8]) {
var index = 0
while index < bytes.count {
let byte = bytes[index]
if byte == quote || byte == backslash {
output.append(backslash)
output.append(byte)
index += 1
} else if let sequence = escapableSequence(in: bytes, at: index),
let escape = lineBreakingEscape(sequence.scalar) {
output.append(contentsOf: escape.utf8)
index += sequence.width
} else {
output.append(byte)
index += 1
}
}
}
/// The character at `index` when its lead byte is one that can spell a character
/// `lineBreakingEscape` answers for: a C0 control or DEL, 0xC2 for U+0080 to U+00BF, or 0xE2
/// for U+2000 to U+2FFF. A Swift string is valid UTF-8, so the continuation bytes are there.
private static func escapableSequence(
in bytes: UnsafeBufferPointer<UInt8>,
at index: Int
) -> (scalar: Unicode.Scalar, width: Int)? {
let lead = bytes[index]
switch lead {
case 0x00 ..< 0x20, 0x7F:
return (Unicode.Scalar(lead), 1)
case 0xC2 where index + 1 < bytes.count:
return (Unicode.Scalar(bytes[index + 1]), 2)
case 0xE2 where index + 2 < bytes.count:
let value = 0x2000 | UInt32(bytes[index + 1] & 0x3F) << 6 | UInt32(bytes[index + 2] & 0x3F)
return Unicode.Scalar(value).map { (scalar: $0, width: 3) }
default:
return nil
}
}
}