@kexhq/kex 0.4.0-alpha.2-dev.20d979e → 0.4.0-alpha.2-dev.db076ab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/kex_repl_wasm.data +572 -13
- package/dist/kex_repl_wasm.js +1 -1
- package/dist/kex_repl_wasm.wasm +0 -0
- package/package.json +1 -1
package/dist/kex_repl_wasm.data
CHANGED
|
@@ -1041,6 +1041,7 @@ record Policy do
|
|
|
1041
1041
|
multiplier : Float
|
|
1042
1042
|
maximumDelay : Duration
|
|
1043
1043
|
maximumElapsed : Duration? = None
|
|
1044
|
+
jitterFraction : Float = 0.0
|
|
1044
1045
|
end
|
|
1045
1046
|
|
|
1046
1047
|
# Decides whether an application error is eligible for another attempt.
|
|
@@ -1055,14 +1056,15 @@ type Sleeper = Duration -> Void
|
|
|
1055
1056
|
# @param maximumAttempts [Integer] total calls including the first
|
|
1056
1057
|
# @param delay [Duration] delay before each subsequent call
|
|
1057
1058
|
# @return [Policy] the retry schedule
|
|
1058
|
-
# @example +Retry.fixed(3,
|
|
1059
|
+
# @example +Retry.fixed(3, 100.milliseconds)+.
|
|
1059
1060
|
fixed : Integer -> Duration -> Policy
|
|
1060
1061
|
let fixed(maximumAttempts, delay) = Policy {
|
|
1061
1062
|
maximumAttempts: maximumAttempts,
|
|
1062
1063
|
initialDelay: delay,
|
|
1063
1064
|
multiplier: 1.0,
|
|
1064
1065
|
maximumDelay: delay,
|
|
1065
|
-
maximumElapsed: None
|
|
1066
|
+
maximumElapsed: None,
|
|
1067
|
+
jitterFraction: 0.0
|
|
1066
1068
|
}
|
|
1067
1069
|
|
|
1068
1070
|
# Builds a doubling backoff capped at +maximumDelay+.
|
|
@@ -1077,7 +1079,8 @@ let exponential(maximumAttempts, initialDelay, maximumDelay) = Policy {
|
|
|
1077
1079
|
initialDelay: initialDelay,
|
|
1078
1080
|
multiplier: 2.0,
|
|
1079
1081
|
maximumDelay: maximumDelay,
|
|
1080
|
-
maximumElapsed: None
|
|
1082
|
+
maximumElapsed: None,
|
|
1083
|
+
jitterFraction: 0.0
|
|
1081
1084
|
}
|
|
1082
1085
|
|
|
1083
1086
|
make Policy do
|
|
@@ -1089,13 +1092,26 @@ make Policy do
|
|
|
1089
1092
|
#
|
|
1090
1093
|
# @param maximumElapsed [Duration] maximum cumulative scheduled delay
|
|
1091
1094
|
# @return [Policy] a copied policy with the elapsed bound
|
|
1092
|
-
# @example +Retry.fixed(5,
|
|
1095
|
+
# @example +Retry.fixed(5, 1.seconds).withMaximumElapsed(2.seconds)+.
|
|
1093
1096
|
let withMaximumElapsed(maximumElapsed: Duration) -> Policy = Policy {
|
|
1094
1097
|
maximumAttempts: @maximumAttempts,
|
|
1095
1098
|
initialDelay: @initialDelay,
|
|
1096
1099
|
multiplier: @multiplier,
|
|
1097
1100
|
maximumDelay: @maximumDelay,
|
|
1098
|
-
maximumElapsed: Just(maximumElapsed)
|
|
1101
|
+
maximumElapsed: Just(maximumElapsed),
|
|
1102
|
+
jitterFraction: @jitterFraction
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
# Returns the same schedule with symmetric bounded jitter. A fraction of
|
|
1106
|
+
# +0.25+ selects each actual delay from 75% through 125% of its scheduled
|
|
1107
|
+
# value. Fractions are clamped to +0.0..1.0+.
|
|
1108
|
+
let withJitter(fraction: Float) -> Policy = Policy {
|
|
1109
|
+
maximumAttempts: @maximumAttempts,
|
|
1110
|
+
initialDelay: @initialDelay,
|
|
1111
|
+
multiplier: @multiplier,
|
|
1112
|
+
maximumDelay: @maximumDelay,
|
|
1113
|
+
maximumElapsed: @maximumElapsed,
|
|
1114
|
+
jitterFraction: if fraction < 0.0 then 0.0 else if fraction > 1.0 then 1.0 else fraction end end
|
|
1099
1115
|
}
|
|
1100
1116
|
end
|
|
1101
1117
|
|
|
@@ -1108,7 +1124,7 @@ end
|
|
|
1108
1124
|
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1109
1125
|
# @return [Result<X,E>] the first success or final failure
|
|
1110
1126
|
run : Policy -> Block<Result<X, E>> -> Result<X, E>
|
|
1111
|
-
foul run(policy, operation) =
|
|
1127
|
+
foul run(policy, operation) = runWithRandom(policy, { |error| true }, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1112
1128
|
|
|
1113
1129
|
# Runs with an application-specific error predicate.
|
|
1114
1130
|
#
|
|
@@ -1121,7 +1137,7 @@ foul run(policy, operation) = runWith(policy, { |error| true }, { |delay| Task.s
|
|
|
1121
1137
|
# @return [Result<X,E>] the first success or final/non-retryable failure
|
|
1122
1138
|
# @example +Retry.run(policy, { |error| error.retryable? }, do request() end)+
|
|
1123
1139
|
run : Policy -> Predicate<E> -> Block<Result<X, E>> -> Result<X, E>
|
|
1124
|
-
foul run(policy, predicate, operation) =
|
|
1140
|
+
foul run(policy, predicate, operation) = runWithRandom(policy, predicate, { |delay| Task.sleep(delay) }, { Kex.Intrinsic.Retry.randomUnit() }, operation)
|
|
1125
1141
|
|
|
1126
1142
|
# Runs with injected error classification and sleeping.
|
|
1127
1143
|
#
|
|
@@ -1134,9 +1150,16 @@ foul run(policy, predicate, operation) = runWith(policy, predicate, { |delay| Ta
|
|
|
1134
1150
|
# @param operation [Block<Result<X,E>>] a fresh attempt
|
|
1135
1151
|
# @return [Result<X,E>] the first success or final failure
|
|
1136
1152
|
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1137
|
-
foul runWith(policy, predicate, sleeper, operation) =
|
|
1153
|
+
foul runWith(policy, predicate, sleeper, operation) = runWithRandom(policy, predicate, sleeper, { 0.5 }, operation)
|
|
1138
1154
|
|
|
1139
|
-
|
|
1155
|
+
# Runs with injected sleeping and a random source returning a value in
|
|
1156
|
+
# +0.0..1.0+. Out-of-range test values are clamped. Production +run+ uses a
|
|
1157
|
+
# cryptographically secure backend source; this overload makes jitter specs
|
|
1158
|
+
# deterministic.
|
|
1159
|
+
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1160
|
+
foul runWithRandom(policy, predicate, sleeper, random, operation) = attempt(policy, predicate, sleeper, random, operation, 1, policy.initialDelay, Duration.seconds(0))
|
|
1161
|
+
|
|
1162
|
+
foul attempt(policy: Policy, predicate: Predicate<E>, sleeper: Sleeper, random: Block<Float>, operation: Block<Result<X, E>>, number: Integer, delay: Duration, elapsed: Duration) -> Result<X, E> do
|
|
1140
1163
|
let result = operation()
|
|
1141
1164
|
return match result do
|
|
1142
1165
|
Ok(value) => Ok(value)
|
|
@@ -1144,7 +1167,10 @@ foul attempt(policy: Policy, predicate: Predicate<E>, sleeper: Sleeper, operatio
|
|
|
1144
1167
|
if number >= policy.maximumAttempts || !predicate(error)
|
|
1145
1168
|
return Error(error)
|
|
1146
1169
|
end
|
|
1147
|
-
let
|
|
1170
|
+
let sample = random()
|
|
1171
|
+
let boundedSample = if sample < 0.0 then 0.0 else if sample > 1.0 then 1.0 else sample end end
|
|
1172
|
+
let jittered = Duration { seconds: delay.seconds * (1.0 + ((boundedSample * 2.0 - 1.0) * policy.jitterFraction)) }
|
|
1173
|
+
let nextElapsed = Duration { seconds: elapsed.seconds + jittered.seconds }
|
|
1148
1174
|
let withinElapsed = match policy.maximumElapsed do
|
|
1149
1175
|
None => true
|
|
1150
1176
|
Just(maximum) => nextElapsed.seconds <= maximum.seconds
|
|
@@ -1152,10 +1178,10 @@ foul attempt(policy: Policy, predicate: Predicate<E>, sleeper: Sleeper, operatio
|
|
|
1152
1178
|
if !withinElapsed
|
|
1153
1179
|
return Error(error)
|
|
1154
1180
|
end
|
|
1155
|
-
sleeper(
|
|
1181
|
+
sleeper(jittered)
|
|
1156
1182
|
let nextSeconds = delay.seconds * policy.multiplier
|
|
1157
1183
|
let bounded = if nextSeconds > policy.maximumDelay.seconds then policy.maximumDelay else Duration { seconds: nextSeconds } end
|
|
1158
|
-
return attempt(policy, predicate, sleeper, operation, number + 1, bounded, nextElapsed)
|
|
1184
|
+
return attempt(policy, predicate, sleeper, random, operation, number + 1, bounded, nextElapsed)
|
|
1159
1185
|
end
|
|
1160
1186
|
end
|
|
1161
1187
|
end
|
|
@@ -1181,6 +1207,10 @@ module Retry do
|
|
|
1181
1207
|
# Deterministic seam with an injected sleeper, primarily for specifications.
|
|
1182
1208
|
runWith : Policy -> Predicate<E> -> Sleeper -> Block<Result<X, E>> -> Result<X, E>
|
|
1183
1209
|
foul runWith(policy, predicate, sleeper, operation) = Control.Retry.runWith(policy, predicate, sleeper, operation)
|
|
1210
|
+
|
|
1211
|
+
# Deterministic seam with injected sleeping and random sampling.
|
|
1212
|
+
runWithRandom : Policy -> Predicate<E> -> Sleeper -> Block<Float> -> Block<Result<X, E>> -> Result<X, E>
|
|
1213
|
+
foul runWithRandom(policy, predicate, sleeper, random, operation) = Control.Retry.runWithRandom(policy, predicate, sleeper, random, operation)
|
|
1184
1214
|
end
|
|
1185
1215
|
# A first-in-first-out queue.
|
|
1186
1216
|
#
|
|
@@ -7759,6 +7789,22 @@ record CacheOptions do
|
|
|
7759
7789
|
negativeTtl : Duration = Duration.seconds(30)
|
|
7760
7790
|
end
|
|
7761
7791
|
|
|
7792
|
+
# One DNS server used by a custom resolver.
|
|
7793
|
+
record Nameserver do
|
|
7794
|
+
address : Net.IP.Address
|
|
7795
|
+
port : Net.Port
|
|
7796
|
+
end
|
|
7797
|
+
|
|
7798
|
+
# Isolated resolver configuration. An empty search list only queries the name
|
|
7799
|
+
# as written; search domains are tried in order for single-label names.
|
|
7800
|
+
record ResolverOptions do
|
|
7801
|
+
cache : CacheOptions = CacheOptions {}
|
|
7802
|
+
nameservers : [Nameserver]
|
|
7803
|
+
search : [Name] = []
|
|
7804
|
+
retries : Integer = 2
|
|
7805
|
+
timeout : Duration = 2.seconds
|
|
7806
|
+
end
|
|
7807
|
+
|
|
7762
7808
|
# Lifetime cache counters. +clear+ empties entries but keeps these counters.
|
|
7763
7809
|
record CacheStatistics do
|
|
7764
7810
|
entries : Integer
|
|
@@ -7789,6 +7835,12 @@ module Resolver do
|
|
|
7789
7835
|
# @param cache [CacheOptions] entry and TTL limits
|
|
7790
7836
|
# @return [Result<Resolver, NetError>] a resolver, or +Parse+
|
|
7791
7837
|
foul system(cache: CacheOptions) -> Result<Resolver, NetError> = Kex.Intrinsic.NetDNS.resolver(cache)
|
|
7838
|
+
# Opens an isolated resolver with typed nameservers and query bounds.
|
|
7839
|
+
# @param options [ResolverOptions] nameservers, search domains, retry count,
|
|
7840
|
+
# per-query timeout, and cache bounds
|
|
7841
|
+
# @return [Result<Resolver, NetError>] a resolver, or +Parse+
|
|
7842
|
+
# @example +Resolver.custom(ResolverOptions { nameservers: [Nameserver { address: Net.IP.Address.parse("127.0.0.1").try, port: Net.Port.from(53).try }], timeout: 100.milliseconds }).try+
|
|
7843
|
+
foul custom(options: ResolverOptions) -> Result<Resolver, NetError> = Kex.Intrinsic.NetDNS.resolverWith(options)
|
|
7792
7844
|
end
|
|
7793
7845
|
|
|
7794
7846
|
make Resolver do
|
|
@@ -8029,7 +8081,8 @@ module Client do
|
|
|
8029
8081
|
open : Result<Client, NetError>
|
|
8030
8082
|
foul open() = Kex.Intrinsic.NetHTTPClient.open(ClientOptions {})
|
|
8031
8083
|
# Opens an explicit client owning the supplied pool policy.
|
|
8032
|
-
|
|
8084
|
+
open : ClientOptions -> Result<Client, NetError>
|
|
8085
|
+
foul open(options) = Kex.Intrinsic.NetHTTPClient.open(options)
|
|
8033
8086
|
end
|
|
8034
8087
|
|
|
8035
8088
|
make Client do
|
|
@@ -8288,6 +8341,21 @@ module TCP do
|
|
|
8288
8341
|
host : String
|
|
8289
8342
|
port : Net.Port
|
|
8290
8343
|
end
|
|
8344
|
+
record ConnectOptions do
|
|
8345
|
+
connectTimeout : Duration = 10.seconds
|
|
8346
|
+
noDelay? : Bool = true
|
|
8347
|
+
keepAlive? : Bool = true
|
|
8348
|
+
sendBuffer : Integer = 0
|
|
8349
|
+
receiveBuffer : Integer = 0
|
|
8350
|
+
end
|
|
8351
|
+
record ListenOptions do
|
|
8352
|
+
backlog : Integer = 128
|
|
8353
|
+
reuseAddress? : Bool = true
|
|
8354
|
+
noDelay? : Bool = true
|
|
8355
|
+
keepAlive? : Bool = true
|
|
8356
|
+
sendBuffer : Integer = 0
|
|
8357
|
+
receiveBuffer : Integer = 0
|
|
8358
|
+
end
|
|
8291
8359
|
|
|
8292
8360
|
module Endpoint do
|
|
8293
8361
|
# @return [Endpoint] an endpoint using +name+ exactly as supplied
|
|
@@ -8309,18 +8377,24 @@ module TCP do
|
|
|
8309
8377
|
# @example +TCP.connect(TCP.Endpoint.host("example.test", Port.from(80).try))+.
|
|
8310
8378
|
connect : Endpoint -> Result<TCPConnection, NetError>
|
|
8311
8379
|
foul connect(endpoint) = Kex.Intrinsic.NetTCP.connect(endpoint)
|
|
8380
|
+
connect : Endpoint -> ConnectOptions -> Result<TCPConnection, NetError>
|
|
8381
|
+
foul connect(endpoint, options) = Kex.Intrinsic.NetTCP.connectWith(endpoint, options)
|
|
8312
8382
|
# Binds and starts listening. Port zero selects an ephemeral local port.
|
|
8313
8383
|
# @return [Result<TCPListener, NetError>] a listener or +Connect+
|
|
8314
8384
|
# @example +TCP.listen(TCP.Endpoint.loopback(Port.from(0).try))+.
|
|
8315
8385
|
listen : Endpoint -> Result<TCPListener, NetError>
|
|
8316
8386
|
foul listen(endpoint) = Kex.Intrinsic.NetTCP.listen(endpoint)
|
|
8387
|
+
listen : Endpoint -> ListenOptions -> Result<TCPListener, NetError>
|
|
8388
|
+
foul listen(endpoint, options) = Kex.Intrinsic.NetTCP.listenWith(endpoint, options)
|
|
8317
8389
|
|
|
8318
8390
|
# Sends every byte or reports the failure and transfer progress.
|
|
8319
8391
|
foul sendAll(connection: TCPConnection, data: Binary) -> Result<Integer, NetError> = Kex.Intrinsic.NetTCP.sendAll(connection, data)
|
|
8320
8392
|
# Receives one bounded chunk; EOF is reported as +Closed+.
|
|
8321
8393
|
foul receiveChunk(connection: TCPConnection, limit: Integer) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveChunk(connection, limit)
|
|
8394
|
+
foul receiveChunk(connection: TCPConnection, limit: Integer, timeout: Duration) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveChunkWithin(connection, limit, timeout)
|
|
8322
8395
|
# Receives exactly +count+ bytes or returns a typed EOF/timeout failure.
|
|
8323
8396
|
foul receiveExactly(connection: TCPConnection, count: Integer) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveExactly(connection, count)
|
|
8397
|
+
foul receiveExactly(connection: TCPConnection, count: Integer, timeout: Duration) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveExactlyWithin(connection, count, timeout)
|
|
8324
8398
|
# Receives through the first +delimiter+ without exceeding +limit+ bytes.
|
|
8325
8399
|
#
|
|
8326
8400
|
# The delimiter is included in the returned bytes. An empty delimiter is a
|
|
@@ -8328,12 +8402,15 @@ module TCP do
|
|
|
8328
8402
|
#
|
|
8329
8403
|
# @example +connection.receiveUntil("\r\n\r\n".to(Binary).try, 65536).try+
|
|
8330
8404
|
foul receiveUntil(connection: TCPConnection, delimiter: Binary, limit: Integer) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveUntil(connection, delimiter, limit)
|
|
8405
|
+
foul receiveUntil(connection: TCPConnection, delimiter: Binary, limit: Integer, timeout: Duration) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveUntilWithin(connection, delimiter, limit, timeout)
|
|
8331
8406
|
# Receives through a newline without exceeding +limit+ bytes.
|
|
8332
8407
|
foul receiveLine(connection: TCPConnection, limit: Integer) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveLine(connection, limit)
|
|
8408
|
+
foul receiveLine(connection: TCPConnection, limit: Integer, timeout: Duration) -> Result<Binary, NetError> = Kex.Intrinsic.NetTCP.receiveLineWithin(connection, limit, timeout)
|
|
8333
8409
|
# Half-closes the write side while leaving reads available.
|
|
8334
8410
|
foul shutdownWrite(connection: TCPConnection) -> Result<Void, NetError> = Kex.Intrinsic.NetTCP.shutdownWrite(connection)
|
|
8335
8411
|
# Waits for and returns the next connection.
|
|
8336
8412
|
foul accept(listener: TCPListener) -> Result<TCPConnection, NetError> = Kex.Intrinsic.NetTCP.accept(listener)
|
|
8413
|
+
foul accept(listener: TCPListener, timeout: Duration) -> Result<TCPConnection, NetError> = Kex.Intrinsic.NetTCP.acceptWithin(listener, timeout)
|
|
8337
8414
|
# Idempotently closes a connected stream.
|
|
8338
8415
|
foul close(connection: TCPConnection) -> Void = Kex.Intrinsic.NetTCP.close(connection)
|
|
8339
8416
|
# Idempotently closes a listener and releases its port.
|
|
@@ -8373,6 +8450,14 @@ module UDP do
|
|
|
8373
8450
|
source : Endpoint
|
|
8374
8451
|
data : Binary
|
|
8375
8452
|
end
|
|
8453
|
+
# Curated socket policy. Broadcast is opt-in. The receive timeout applies to
|
|
8454
|
+
# each +receiveFrom+ call; multicast TTL is bounded to the IP hop-limit range.
|
|
8455
|
+
record BindOptions do
|
|
8456
|
+
broadcast? : Bool = false
|
|
8457
|
+
multicastTtl : Integer = 1
|
|
8458
|
+
multicastLoopback? : Bool = true
|
|
8459
|
+
receiveTimeout : Duration = 30.seconds
|
|
8460
|
+
end
|
|
8376
8461
|
module Endpoint do
|
|
8377
8462
|
# @return [Endpoint] an endpoint using +name+ exactly as supplied
|
|
8378
8463
|
host : String -> Net.Port -> Endpoint
|
|
@@ -8387,6 +8472,9 @@ module UDP do
|
|
|
8387
8472
|
# Binds a datagram socket. Port zero selects an ephemeral local port.
|
|
8388
8473
|
bind : Endpoint -> Result<Socket, NetError>
|
|
8389
8474
|
foul bind(endpoint) = Kex.Intrinsic.NetUDP.bind(endpoint)
|
|
8475
|
+
# Binds with explicit broadcast, multicast, and receive-timeout policy.
|
|
8476
|
+
bind : Endpoint -> BindOptions -> Result<Socket, NetError>
|
|
8477
|
+
foul bind(endpoint, options) = Kex.Intrinsic.NetUDP.bindWith(endpoint, options)
|
|
8390
8478
|
|
|
8391
8479
|
# Sends one complete datagram and returns its byte count.
|
|
8392
8480
|
foul sendTo(socket: Socket, endpoint: Endpoint, data: Binary) -> Result<Integer, NetError> = Kex.Intrinsic.NetUDP.sendTo(socket, endpoint, data)
|
|
@@ -8398,6 +8486,11 @@ module UDP do
|
|
|
8398
8486
|
foul closed?(socket: Socket) -> Bool = Kex.Intrinsic.NetUDP.closed?(socket)
|
|
8399
8487
|
# Returns the bound endpoint, including an ephemeral assigned port.
|
|
8400
8488
|
foul localAddress(socket: Socket) -> Result<Endpoint, NetError> = Kex.Intrinsic.NetUDP.localAddress(socket)
|
|
8489
|
+
# Joins an IPv4 multicast group on the selected local interface. The group
|
|
8490
|
+
# must be multicast and the interface must be an IPv4 address.
|
|
8491
|
+
foul joinMulticast(socket: Socket, group: Net.IP.Address, interface: Net.IP.Address) -> Result<Void, NetError> = Kex.Intrinsic.NetUDP.joinMulticast(socket, group, interface)
|
|
8492
|
+
# Leaves a membership previously joined with the same group and interface.
|
|
8493
|
+
foul leaveMulticast(socket: Socket, group: Net.IP.Address, interface: Net.IP.Address) -> Result<Void, NetError> = Kex.Intrinsic.NetUDP.leaveMulticast(socket, group, interface)
|
|
8401
8494
|
|
|
8402
8495
|
end
|
|
8403
8496
|
|
|
@@ -8418,6 +8511,16 @@ module Unix do
|
|
|
8418
8511
|
record Address do
|
|
8419
8512
|
path : String
|
|
8420
8513
|
end
|
|
8514
|
+
record ConnectOptions do
|
|
8515
|
+
connectTimeout : Duration = 10.seconds
|
|
8516
|
+
receiveTimeout : Duration = 30.seconds
|
|
8517
|
+
end
|
|
8518
|
+
record ListenOptions do
|
|
8519
|
+
backlog : Integer = 128
|
|
8520
|
+
removeStale? : Bool = false
|
|
8521
|
+
acceptTimeout : Duration = 30.seconds
|
|
8522
|
+
receiveTimeout : Duration = 30.seconds
|
|
8523
|
+
end
|
|
8421
8524
|
module Address do
|
|
8422
8525
|
# Validates a nonempty absolute Unix-domain socket path.
|
|
8423
8526
|
# @return [Result<Address, NetError>] the address, or +Parse+
|
|
@@ -8427,9 +8530,15 @@ module Unix do
|
|
|
8427
8530
|
# Connects to a filesystem-domain listener.
|
|
8428
8531
|
connect : Address -> Result<UnixConnection, NetError>
|
|
8429
8532
|
foul connect(address) = Kex.Intrinsic.NetUnix.connect(address)
|
|
8533
|
+
connect : Address -> ConnectOptions -> Result<UnixConnection, NetError>
|
|
8534
|
+
foul connect(address, options) = Kex.Intrinsic.NetUnix.connectWith(address, options)
|
|
8430
8535
|
# Binds a new path; an existing filesystem entry is never removed implicitly.
|
|
8431
8536
|
listen : Address -> Result<UnixListener, NetError>
|
|
8432
8537
|
foul listen(address) = Kex.Intrinsic.NetUnix.listen(address)
|
|
8538
|
+
# With +removeStale?+, only an existing filesystem socket is removed;
|
|
8539
|
+
# regular files, directories, and other path types are always preserved.
|
|
8540
|
+
listen : Address -> ListenOptions -> Result<UnixListener, NetError>
|
|
8541
|
+
foul listen(address, options) = Kex.Intrinsic.NetUnix.listenWith(address, options)
|
|
8433
8542
|
|
|
8434
8543
|
# Sends every byte and returns the count.
|
|
8435
8544
|
foul sendAll(connection: UnixConnection, data: Binary) -> Result<Integer, NetError> = Kex.Intrinsic.NetUnix.sendAll(connection, data)
|
|
@@ -11945,6 +12054,31 @@ make String, implement: Enumerable, Foldable do
|
|
|
11945
12054
|
length :> Integer
|
|
11946
12055
|
let length = Kex.Intrinsic.List.length(this)
|
|
11947
12056
|
|
|
12057
|
+
# Returns the number of user-perceived characters — extended grapheme
|
|
12058
|
+
# clusters (Unicode UAX #29) — which is not always +count+.
|
|
12059
|
+
#
|
|
12060
|
+
# +count+ answers the codepoint count, and most text is one codepoint per
|
|
12061
|
+
# grapheme cluster, so the two usually agree. They part ways for a base
|
|
12062
|
+
# character combined with a following mark, an emoji built from more than
|
|
12063
|
+
# one codepoint, and `"\r\n"`, which is one grapheme cluster over two
|
|
12064
|
+
# codepoints — reach for +graphemeCount+ over +count+ wherever "how many
|
|
12065
|
+
# characters does a person see" is the question, such as sizing text for
|
|
12066
|
+
# display or truncating it at a boundary a reader would recognize.
|
|
12067
|
+
#
|
|
12068
|
+
# @return [Integer] the grapheme cluster count
|
|
12069
|
+
#
|
|
12070
|
+
# @example
|
|
12071
|
+
# "hello".graphemeCount # => 5, same as count
|
|
12072
|
+
# "\r\n".graphemeCount # => 1 ("\r\n".count is 2 -- a CR codepoint and an LF codepoint)
|
|
12073
|
+
#
|
|
12074
|
+
# @example A base character plus a combining mark
|
|
12075
|
+
# let acute = String.fromCodepoint(0x301).or("") # combining acute accent
|
|
12076
|
+
# let e = "e" + acute
|
|
12077
|
+
# e.count # => 2 (two codepoints: 'e' and the combining mark)
|
|
12078
|
+
# e.graphemeCount # => 1 (one character on screen)
|
|
12079
|
+
graphemeCount :> Integer
|
|
12080
|
+
let graphemeCount = Kex.Intrinsic.String.graphemeCount(this)
|
|
12081
|
+
|
|
11948
12082
|
# Returns +true+ when the string has no characters.
|
|
11949
12083
|
#
|
|
11950
12084
|
# Note that a string of spaces is not empty — use +blank?+ from the
|
|
@@ -12876,6 +13010,431 @@ warnBetween : Integer -> Integer -> String -> Issue
|
|
|
12876
13010
|
let warnBetween(start, finish, message) do
|
|
12877
13011
|
Warn(Between(start, finish), message)
|
|
12878
13012
|
end
|
|
13013
|
+
# An ERB-shaped template scanner: template text in, a template AST out.
|
|
13014
|
+
#
|
|
13015
|
+
# Opt-in — nothing here is in scope until `using Template`.
|
|
13016
|
+
#
|
|
13017
|
+
# This is the scanning stage only (see the Template proposal for the fuller
|
|
13018
|
+
# design): it turns template source into a flat list of +Node+s — plain text,
|
|
13019
|
+
# and the four kinds of `<% %>` region — plus whatever frontmatter tags sit
|
|
13020
|
+
# ahead of the body. It does not evaluate anything and does not know Kex
|
|
13021
|
+
# syntax; the text inside a hole is kept as-is, for a later stage to parse
|
|
13022
|
+
# and lower into real Kex.
|
|
13023
|
+
#
|
|
13024
|
+
# using Template
|
|
13025
|
+
#
|
|
13026
|
+
# let source = "---\nlayout: page\nparams: [name, library: Bool]\n---\nHi <%= name %>!"
|
|
13027
|
+
# let parsed = Template.scan(source).try
|
|
13028
|
+
# parsed.frontmatter.get("layout") # => Just(Scalar("page"))
|
|
13029
|
+
# parsed.parameters # => [TemplateParam { name: "name", type: "" },
|
|
13030
|
+
# # TemplateParam { name: "library", type: "Bool" }]
|
|
13031
|
+
# parsed.nodes # => [Text("Hi "), Interpolate("name"), Text("!")]
|
|
13032
|
+
#
|
|
13033
|
+
# ## Syntax
|
|
13034
|
+
#
|
|
13035
|
+
# <%= expr %> interpolate (escaping is a later stage's job)
|
|
13036
|
+
# <%== expr %> interpolate raw, no escaping
|
|
13037
|
+
# <% ... %> a Kex control region: `if`/`match` arms, block bodies, `let`
|
|
13038
|
+
# <%# ... %> comment, emits nothing
|
|
13039
|
+
# <%- ... -%> whitespace control: trims the line's leading indent before
|
|
13040
|
+
# the tag, and the newline right after it
|
|
13041
|
+
# <%% a literal `<%`, for a template that generates ERB-shaped
|
|
13042
|
+
# output itself
|
|
13043
|
+
#
|
|
13044
|
+
# ## Frontmatter
|
|
13045
|
+
#
|
|
13046
|
+
# A template file may open with a `---` line, generic `key: value` tags up to
|
|
13047
|
+
# a closing `---` line, and then the body. This is metadata for whatever
|
|
13048
|
+
# reads the template — a title, a layout name, a list of tags — and there is
|
|
13049
|
+
# nothing template-specific about it: it is scanned the same generic way
|
|
13050
|
+
# whatever key is used.
|
|
13051
|
+
#
|
|
13052
|
+
# ---
|
|
13053
|
+
# title: Release notes
|
|
13054
|
+
# tags: [changelog, public]
|
|
13055
|
+
# ---
|
|
13056
|
+
# # <%= title %>
|
|
13057
|
+
#
|
|
13058
|
+
# A template's parameters, when a later stage needs them declared rather than
|
|
13059
|
+
# inferred, are just another frontmatter key — conventionally `params`, a
|
|
13060
|
+
# list of names each with an optional `: Type`:
|
|
13061
|
+
#
|
|
13062
|
+
# ---
|
|
13063
|
+
# params: [name, library: Bool, dependencies: [Dependency]]
|
|
13064
|
+
# ---
|
|
13065
|
+
# # <%= name %>
|
|
13066
|
+
#
|
|
13067
|
+
# Nothing in the scanner treats `params` specially while scanning — it is
|
|
13068
|
+
# metadata like any other key, a `[a, b, c]` list same as `tags` above.
|
|
13069
|
+
# `Parsed#parameters` is a convenience reader for it: it splits each entry on
|
|
13070
|
+
# its first colon into a `TemplateParam { name, type }` (`type` is `""` for a bare
|
|
13071
|
+
# name), so a caller does not need to know the key name, match on `Tag`
|
|
13072
|
+
# itself, or split each entry by hand. `type` is raw text, same as
|
|
13073
|
+
# everywhere else in this module — turning `[Dependency]` into a real,
|
|
13074
|
+
# resolved Kex type is later work, not this module's.
|
|
13075
|
+
module Template
|
|
13076
|
+
using Parsing
|
|
13077
|
+
|
|
13078
|
+
# One piece of a scanned template.
|
|
13079
|
+
#
|
|
13080
|
+
# Hole and control content is carried as raw text — the Kex source that was
|
|
13081
|
+
# between the delimiters, trimmed of surrounding whitespace. Turning that
|
|
13082
|
+
# text into real, type-checked Kex expressions is later work; a `Node` only
|
|
13083
|
+
# records what KIND of region it is and what text it held.
|
|
13084
|
+
type Node = Text(String)
|
|
13085
|
+
| Interpolate(String)
|
|
13086
|
+
| InterpolateRaw(String)
|
|
13087
|
+
| Control(String)
|
|
13088
|
+
| Comment(String)
|
|
13089
|
+
|
|
13090
|
+
# A frontmatter value: a plain scalar, or a `[a, b, c]` list.
|
|
13091
|
+
type Tag = Scalar(String)
|
|
13092
|
+
| Tags([String])
|
|
13093
|
+
|
|
13094
|
+
# Why a template's text could not be scanned, and where.
|
|
13095
|
+
type TemplateError = UnterminatedTag(Integer)
|
|
13096
|
+
| UnterminatedFrontmatter
|
|
13097
|
+
| MalformedFrontmatterLine(String)
|
|
13098
|
+
|
|
13099
|
+
# One entry from a `params: [...]` frontmatter list: a name, and its
|
|
13100
|
+
# optional `: Type` annotation. `type` is raw text — `""` for a bare name,
|
|
13101
|
+
# never a resolved Kex type — the same way a frontmatter `Tag` is text and
|
|
13102
|
+
# not a parsed value.
|
|
13103
|
+
record TemplateParam do
|
|
13104
|
+
name : String
|
|
13105
|
+
type : String
|
|
13106
|
+
end
|
|
13107
|
+
|
|
13108
|
+
# One scanned `<% ... %>` region: its node, and the whitespace trims its
|
|
13109
|
+
# delimiters asked for. Not part of the public API — internal to scanning —
|
|
13110
|
+
# but declared at module scope rather than inside `private do`: nested
|
|
13111
|
+
# there, this record's fields lose their names under BEAM codegen and the
|
|
13112
|
+
# value round-trips as an untagged tuple (`Undefined method: leftTrim for
|
|
13113
|
+
# Tuple`), even though the same code is fine on the tree-walking interpreter.
|
|
13114
|
+
record TagScan do
|
|
13115
|
+
node : Node
|
|
13116
|
+
leftTrim : Bool
|
|
13117
|
+
rightTrim : Bool
|
|
13118
|
+
end
|
|
13119
|
+
|
|
13120
|
+
# A scanned template: its frontmatter tags, and its body as a node list.
|
|
13121
|
+
record Parsed do
|
|
13122
|
+
frontmatter : {String: Tag}
|
|
13123
|
+
nodes : [Node]
|
|
13124
|
+
end
|
|
13125
|
+
|
|
13126
|
+
make Parsed do
|
|
13127
|
+
# The template's declared parameters, out of a `params: [...]` frontmatter
|
|
13128
|
+
# key — `[]` when the template declares none, or when `params` holds a
|
|
13129
|
+
# bare scalar rather than a list.
|
|
13130
|
+
#
|
|
13131
|
+
# @return [[TemplateParam]] the declared parameters, in the order written
|
|
13132
|
+
#
|
|
13133
|
+
# @example
|
|
13134
|
+
# parsed.parameters
|
|
13135
|
+
# # => [TemplateParam { name: "name", type: "" }, TemplateParam { name: "library", type: "Bool" }]
|
|
13136
|
+
let parameters -> [TemplateParam] do
|
|
13137
|
+
let none: [TemplateParam] = []
|
|
13138
|
+
match @frontmatter.get("params") do
|
|
13139
|
+
Just(Tags(names)) => parseTemplateParams(names)
|
|
13140
|
+
_ => none
|
|
13141
|
+
end
|
|
13142
|
+
end
|
|
13143
|
+
end
|
|
13144
|
+
|
|
13145
|
+
# Scans template source into a `Parsed` template, or says where it broke.
|
|
13146
|
+
#
|
|
13147
|
+
# @param source [String] the template file's full text
|
|
13148
|
+
# @return [Result<Parsed, TemplateError>] the scanned template, or why not
|
|
13149
|
+
#
|
|
13150
|
+
# @example
|
|
13151
|
+
# Template.scan("Hi <%= name %>!").map(~nodes)
|
|
13152
|
+
# # => Ok([Text("Hi "), Interpolate("name"), Text("!")])
|
|
13153
|
+
let scan(source: String) -> Result<Parsed, TemplateError> do
|
|
13154
|
+
let cursor = Input { input: source }
|
|
13155
|
+
let (frontmatter, afterFrontmatter) = scanFrontmatter(cursor).try
|
|
13156
|
+
let (nodes, _) = scanBody(afterFrontmatter).try
|
|
13157
|
+
Ok(Parsed { frontmatter: frontmatter, nodes: nodes })
|
|
13158
|
+
end
|
|
13159
|
+
|
|
13160
|
+
private do
|
|
13161
|
+
# True when `cursor` sits at the very start of the input and that line is
|
|
13162
|
+
# exactly `---` — the only place a frontmatter block may open.
|
|
13163
|
+
# Hand-rolled peek chain rather than `Input#string("---")`: `string`'s name
|
|
13164
|
+
# collides with `Char#string` (a zero-arg property) the same way
|
|
13165
|
+
# `takeWhile`/`atEnd?`/`readLine` collide elsewhere in this file — the
|
|
13166
|
+
# prelude-interface build picks the wrong candidate and reports an arity
|
|
13167
|
+
# mismatch even though the receiver type here is unambiguous.
|
|
13168
|
+
let atFrontmatterOpen?(cursor: Input) -> Bool do
|
|
13169
|
+
(cursor.pos == 0 &&
|
|
13170
|
+
cursor.peek == Just('-') && cursor.peekAt(1) == Just('-') && cursor.peekAt(2) == Just('-') &&
|
|
13171
|
+
(cursor.peekAt(3) == Just('\n') || cursor.peekAt(3) == Just('\r') || cursor.peekAt(3) == None))
|
|
13172
|
+
end
|
|
13173
|
+
|
|
13174
|
+
# A cursor advanced past the current character if it is `\r\n` or `\n`;
|
|
13175
|
+
# otherwise unchanged. What a `-%>` closer's trailing-newline trim needs.
|
|
13176
|
+
let skipOneNewline(cursor: Input) -> Input do
|
|
13177
|
+
if cursor.peek == Just('\r') && cursor.peekAt(1) == Just('\n')
|
|
13178
|
+
cursor.advanceBy(2)
|
|
13179
|
+
elif cursor.peek == Just('\n')
|
|
13180
|
+
cursor.advance
|
|
13181
|
+
else
|
|
13182
|
+
cursor
|
|
13183
|
+
end
|
|
13184
|
+
end
|
|
13185
|
+
|
|
13186
|
+
# A cursor advanced to the next `\n` (not past it), and the text skipped.
|
|
13187
|
+
#
|
|
13188
|
+
# Hand-rolled rather than `Input#takeWhile` — see `scanText`'s comment on why.
|
|
13189
|
+
let scanLine(cursor: Input) -> (String, Input) do
|
|
13190
|
+
var cur = cursor
|
|
13191
|
+
var chars: [Char] = []
|
|
13192
|
+
loop do
|
|
13193
|
+
break if cur.peek == None
|
|
13194
|
+
break if cur.peek == Just('\n')
|
|
13195
|
+
let Just(ch) = cur.peek
|
|
13196
|
+
chars.push!(ch)
|
|
13197
|
+
cur.advance!
|
|
13198
|
+
end
|
|
13199
|
+
(chars.join(""), cur)
|
|
13200
|
+
end
|
|
13201
|
+
|
|
13202
|
+
# A line's text with any trailing `\r` dropped, for a source that mixes
|
|
13203
|
+
# `\r\n` and `\n` line endings.
|
|
13204
|
+
let stripTrailingCR(text: String) -> String do
|
|
13205
|
+
text.endsWith?("\r") then text.take(text.count - 1) else text
|
|
13206
|
+
end
|
|
13207
|
+
|
|
13208
|
+
# Strips one layer of matching `"..."` or `'...'` quotes, if present.
|
|
13209
|
+
let unquote(text: String) -> String do
|
|
13210
|
+
let quoted = (text.count >= 2 &&
|
|
13211
|
+
((text.startsWith?("\"") && text.endsWith?("\"")) || (text.startsWith?("'") && text.endsWith?("'"))))
|
|
13212
|
+
quoted then text.drop(1).take(text.count - 2) else text
|
|
13213
|
+
end
|
|
13214
|
+
|
|
13215
|
+
# A frontmatter value's text, parsed as a `[a, b, c]` list or a scalar.
|
|
13216
|
+
# Splits `text` on commas that are not nested inside `[]`, `()`, `<>`, or
|
|
13217
|
+
# `{}` — so a `params` entry's type, `Map<String, Integer>` or
|
|
13218
|
+
# `[Dependency, Other]`, stays one list item rather than splitting on its
|
|
13219
|
+
# own internal comma.
|
|
13220
|
+
let splitTopLevelCommas(text: String) -> [String] do
|
|
13221
|
+
let empty: ([String], String, Integer) = ([], "", 0)
|
|
13222
|
+
let (parts, last, _) = text.reduce(empty) do |acc, ch|
|
|
13223
|
+
let (parts, current, depth) = acc
|
|
13224
|
+
if ch == '[' || ch == '(' || ch == '<' || ch == '{'
|
|
13225
|
+
(parts, "${current}${ch}", depth + 1)
|
|
13226
|
+
elif ch == ']' || ch == ')' || ch == '>' || ch == '}'
|
|
13227
|
+
(parts, "${current}${ch}", depth - 1)
|
|
13228
|
+
elif ch == ',' && depth == 0
|
|
13229
|
+
([...parts, current], "", depth)
|
|
13230
|
+
else
|
|
13231
|
+
(parts, "${current}${ch}", depth)
|
|
13232
|
+
end
|
|
13233
|
+
end
|
|
13234
|
+
last.empty? then parts else [...parts, last]
|
|
13235
|
+
end
|
|
13236
|
+
|
|
13237
|
+
let parseFrontmatterValue(raw: String) -> Tag do
|
|
13238
|
+
if raw.startsWith?("[") && raw.endsWith?("]")
|
|
13239
|
+
let inner = raw.drop(1).take(raw.count - 2).trim
|
|
13240
|
+
let items = inner.empty? then [] else splitTopLevelCommas(inner).map { |item| unquote(item.trim) }
|
|
13241
|
+
Tags(items)
|
|
13242
|
+
else
|
|
13243
|
+
Scalar(unquote(raw))
|
|
13244
|
+
end
|
|
13245
|
+
end
|
|
13246
|
+
|
|
13247
|
+
# One `key: value` frontmatter line, split at its first colon.
|
|
13248
|
+
let parseFrontmatterLine(line: String) -> Result<(String, Tag), TemplateError> do
|
|
13249
|
+
let colon = line.findIndex { |ch| ch == ':' }
|
|
13250
|
+
return Error(MalformedFrontmatterLine(line)) if colon == None
|
|
13251
|
+
# `.or(0)` rather than `let Just(index) = colon`: the guard above already
|
|
13252
|
+
# rules out `None`, but destructuring it and feeding the bound name
|
|
13253
|
+
# straight into another generic call (`take`/`drop`) trips a real
|
|
13254
|
+
# type-checker bug where the prelude-interface build cross-contaminates
|
|
13255
|
+
# unrelated generic inference elsewhere in the stdlib. Unwrapping with a
|
|
13256
|
+
# (dead) default avoids it.
|
|
13257
|
+
let index = colon.or(0)
|
|
13258
|
+
let key = line.take(index).trim
|
|
13259
|
+
return Error(MalformedFrontmatterLine(line)) if key.empty?
|
|
13260
|
+
let value = parseFrontmatterValue(line.drop(index + 1).trim)
|
|
13261
|
+
Ok((key, value))
|
|
13262
|
+
end
|
|
13263
|
+
|
|
13264
|
+
# One `params` list entry, split into a name and its optional `: Type`.
|
|
13265
|
+
# `type` is `""` when `raw` is a bare name.
|
|
13266
|
+
let parseTemplateParam(raw: String) -> TemplateParam do
|
|
13267
|
+
let colon = raw.findIndex { |ch| ch == ':' }
|
|
13268
|
+
if colon == None
|
|
13269
|
+
TemplateParam { name: raw.trim, type: "" }
|
|
13270
|
+
else
|
|
13271
|
+
# `.or(0)` rather than destructuring — see `parseFrontmatterLine`'s
|
|
13272
|
+
# comment on why.
|
|
13273
|
+
let index = colon.or(0)
|
|
13274
|
+
TemplateParam { name: raw.take(index).trim, type: raw.drop(index + 1).trim }
|
|
13275
|
+
end
|
|
13276
|
+
end
|
|
13277
|
+
|
|
13278
|
+
# `names.map(~parseTemplateParam)`, spelled as an indexed loop rather than a
|
|
13279
|
+
# generic `map`/`reduce` call: `[String]`'s element type is itself a type
|
|
13280
|
+
# that implements `Enumerable`, and going through the shared HOF name here
|
|
13281
|
+
# sends the prelude-interface build's overload resolution to `String`'s own
|
|
13282
|
+
# `reduce` (iterating characters) instead of `List`'s — the bound block
|
|
13283
|
+
# parameter then type-checks as `Char`, not `String`. Same collision class
|
|
13284
|
+
# as `Input#takeWhile`/`atEnd?`/`string` elsewhere in this file.
|
|
13285
|
+
let parseTemplateParams(names: [String]) -> [TemplateParam] do
|
|
13286
|
+
var params: [TemplateParam] = []
|
|
13287
|
+
var i = 0
|
|
13288
|
+
loop do
|
|
13289
|
+
break if i >= names.count
|
|
13290
|
+
params.push!(parseTemplateParam(names.at(i).or("")))
|
|
13291
|
+
i = i + 1
|
|
13292
|
+
end
|
|
13293
|
+
params
|
|
13294
|
+
end
|
|
13295
|
+
|
|
13296
|
+
# Reads a leading `---` ... `---` frontmatter block, if the input opens with
|
|
13297
|
+
# one. Answers an empty map and the cursor unmoved when it does not.
|
|
13298
|
+
let scanFrontmatter(cursor: Input) -> Result<({String: Tag}, Input), TemplateError> do
|
|
13299
|
+
return Ok(({}, cursor)) if !atFrontmatterOpen?(cursor)
|
|
13300
|
+
|
|
13301
|
+
let (_, afterOpenLine) = scanLine(cursor)
|
|
13302
|
+
var cur = skipOneNewline(afterOpenLine)
|
|
13303
|
+
var tags: {String: Tag} = {}
|
|
13304
|
+
loop do
|
|
13305
|
+
return Error(UnterminatedFrontmatter) if cur.peek == None
|
|
13306
|
+
let (rawLine, afterLine) = scanLine(cur)
|
|
13307
|
+
let line = stripTrailingCR(rawLine).trim
|
|
13308
|
+
cur = skipOneNewline(afterLine)
|
|
13309
|
+
return Ok((tags, cur)) if line == "---"
|
|
13310
|
+
if !line.empty?
|
|
13311
|
+
let (key, value) = parseFrontmatterLine(line).try
|
|
13312
|
+
tags = tags.put(key, value)
|
|
13313
|
+
end
|
|
13314
|
+
end
|
|
13315
|
+
end
|
|
13316
|
+
|
|
13317
|
+
# True when `cursor` sits at a `<%` tag opener (and it is not the `<%%`
|
|
13318
|
+
# escape for a literal `<%`).
|
|
13319
|
+
let atTagOpen?(cursor: Input) -> Bool do
|
|
13320
|
+
cursor.peek == Just('<') && cursor.peekAt(1) == Just('%') && cursor.peekAt(2) != Just('%')
|
|
13321
|
+
end
|
|
13322
|
+
|
|
13323
|
+
# Reads plain text up to the next tag opener or end of input. `<%%` is
|
|
13324
|
+
# consumed as a literal `<%` and folded into the text rather than ending it.
|
|
13325
|
+
#
|
|
13326
|
+
# Hand-rolled rather than built on `Input#takeWhile`: that method's name
|
|
13327
|
+
# collides with `List#takeWhile`, and calling it through an `Input` receiver
|
|
13328
|
+
# sends overload resolution down the wrong path (`json.kex`'s own scanners
|
|
13329
|
+
# hit the same thing and avoid it the same way).
|
|
13330
|
+
let scanText(cursor: Input) -> (String, Input) do
|
|
13331
|
+
var cur = cursor
|
|
13332
|
+
var chars: [Char] = []
|
|
13333
|
+
loop do
|
|
13334
|
+
break if cur.peek == None
|
|
13335
|
+
break if atTagOpen?(cur)
|
|
13336
|
+
if cur.peek == Just('<') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('%')
|
|
13337
|
+
chars.push!('<')
|
|
13338
|
+
chars.push!('%')
|
|
13339
|
+
cur.advanceBy!(3)
|
|
13340
|
+
else
|
|
13341
|
+
let Just(ch) = cur.peek
|
|
13342
|
+
chars.push!(ch)
|
|
13343
|
+
cur.advance!
|
|
13344
|
+
end
|
|
13345
|
+
end
|
|
13346
|
+
(chars.join(""), cur)
|
|
13347
|
+
end
|
|
13348
|
+
|
|
13349
|
+
# Strips trailing spaces and tabs (not newlines) — the indentation a `<%-`
|
|
13350
|
+
# opener asks to have removed from the text before it.
|
|
13351
|
+
let trimTrailingIndent(text: String) -> String do
|
|
13352
|
+
var chars = text.chars
|
|
13353
|
+
loop do
|
|
13354
|
+
break if chars.empty?
|
|
13355
|
+
let Just(last) = chars.last
|
|
13356
|
+
break if last != ' ' && last != '\t'
|
|
13357
|
+
chars = chars.take(chars.count - 1)
|
|
13358
|
+
end
|
|
13359
|
+
chars.join("")
|
|
13360
|
+
end
|
|
13361
|
+
|
|
13362
|
+
# `<%=`, `<%==`, `<%#`, or plain `<%` — which kind of region this is, and
|
|
13363
|
+
# the cursor past the marker.
|
|
13364
|
+
let classifyTag(cursor: Input) -> (String, Input) do
|
|
13365
|
+
if cursor.peek == Just('=') && cursor.peekAt(1) == Just('=')
|
|
13366
|
+
("raw", cursor.advanceBy(2))
|
|
13367
|
+
elif cursor.peek == Just('=')
|
|
13368
|
+
("interpolate", cursor.advance)
|
|
13369
|
+
elif cursor.peek == Just('#')
|
|
13370
|
+
("comment", cursor.advance)
|
|
13371
|
+
else
|
|
13372
|
+
("control", cursor)
|
|
13373
|
+
end
|
|
13374
|
+
end
|
|
13375
|
+
|
|
13376
|
+
# Reads a tag's body up to its closer, `%>` or `-%>`. Answers the raw text,
|
|
13377
|
+
# whether the closer trims the following newline, and the cursor past it.
|
|
13378
|
+
let scanTagBody(cursor: Input) -> Result<(String, Bool, Input), TemplateError> do
|
|
13379
|
+
let start = cursor.pos
|
|
13380
|
+
var cur = cursor
|
|
13381
|
+
var chars: [Char] = []
|
|
13382
|
+
loop do
|
|
13383
|
+
return Error(UnterminatedTag(start)) if cur.peek == None
|
|
13384
|
+
return Ok((chars.join(""), true, cur.advanceBy(3))) if cur.peek == Just('-') && cur.peekAt(1) == Just('%') && cur.peekAt(2) == Just('>')
|
|
13385
|
+
return Ok((chars.join(""), false, cur.advanceBy(2))) if cur.peek == Just('%') && cur.peekAt(1) == Just('>')
|
|
13386
|
+
let Just(ch) = cur.peek
|
|
13387
|
+
chars.push!(ch)
|
|
13388
|
+
cur.advance!
|
|
13389
|
+
end
|
|
13390
|
+
end
|
|
13391
|
+
|
|
13392
|
+
# The node a tag's kind and trimmed body settle into.
|
|
13393
|
+
let tagNode(kind: String, body: String) -> Node do
|
|
13394
|
+
if kind == "raw"
|
|
13395
|
+
InterpolateRaw(body)
|
|
13396
|
+
elif kind == "interpolate"
|
|
13397
|
+
Interpolate(body)
|
|
13398
|
+
elif kind == "comment"
|
|
13399
|
+
Comment(body)
|
|
13400
|
+
else
|
|
13401
|
+
Control(body)
|
|
13402
|
+
end
|
|
13403
|
+
end
|
|
13404
|
+
|
|
13405
|
+
# Scans one tag starting at `cursor`, which must sit at a `<%` opener.
|
|
13406
|
+
let scanTag(cursor: Input) -> Result<(TagScan, Input), TemplateError> do
|
|
13407
|
+
let opened = cursor.advanceBy(2)
|
|
13408
|
+
let leftTrim = opened.peek == Just('-')
|
|
13409
|
+
let afterTrim = leftTrim then opened.advance else opened
|
|
13410
|
+
let (kind, afterKind) = classifyTag(afterTrim)
|
|
13411
|
+
let (body, rightTrim, rest) = scanTagBody(afterKind).try
|
|
13412
|
+
let scanned = TagScan { node: tagNode(kind, body.trim), leftTrim: leftTrim, rightTrim: rightTrim }
|
|
13413
|
+
Ok((scanned, rest))
|
|
13414
|
+
end
|
|
13415
|
+
|
|
13416
|
+
# Scans the template body — everything after any frontmatter — into nodes.
|
|
13417
|
+
let scanBody(cursor: Input) -> Result<([Node], Input), TemplateError> do
|
|
13418
|
+
var cur = cursor
|
|
13419
|
+
var nodes: [Node] = []
|
|
13420
|
+
loop do
|
|
13421
|
+
let (text, afterText) = scanText(cur)
|
|
13422
|
+
if afterText.peek == None
|
|
13423
|
+
if !text.empty?
|
|
13424
|
+
nodes.push!(Text(text))
|
|
13425
|
+
end
|
|
13426
|
+
return Ok((nodes, afterText))
|
|
13427
|
+
end
|
|
13428
|
+
let (tag, afterTag) = scanTag(afterText).try
|
|
13429
|
+
let keptText = tag.leftTrim then trimTrailingIndent(text) else text
|
|
13430
|
+
if !keptText.empty?
|
|
13431
|
+
nodes.push!(Text(keptText))
|
|
13432
|
+
end
|
|
13433
|
+
nodes.push!(tag.node)
|
|
13434
|
+
cur = tag.rightTrim then skipOneNewline(afterTag) else afterTag
|
|
13435
|
+
end
|
|
13436
|
+
end
|
|
13437
|
+
end
|
|
12879
13438
|
# The built-in testing DSL: +describe+, +it+, +before+, +after+, and
|
|
12880
13439
|
# assertion helpers.
|
|
12881
13440
|
#
|