207 lines
5.7 KiB
Julia
207 lines
5.7 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
import Base: peek
|
|
|
|
macro dotimes(n, body)
|
|
quote
|
|
for i = 1:$(esc(n))
|
|
$(esc(body))
|
|
end
|
|
end
|
|
end
|
|
|
|
const whitespace = " \t\r"
|
|
|
|
"""
|
|
Skip any leading whitespace. Returns io.
|
|
"""
|
|
function skipwhitespace(io::IO; newlines = true)
|
|
while !eof(io) && (Char(peek(io)) in whitespace || (newlines && peek(io) == UInt8('\n')))
|
|
read(io, Char)
|
|
end
|
|
return io
|
|
end
|
|
|
|
"""
|
|
Skip any leading blank lines. Returns the number skipped.
|
|
"""
|
|
function skipblank(io::IO)
|
|
start = position(io)
|
|
i = 0
|
|
while !eof(io)
|
|
c = read(io, Char)
|
|
c == '\n' && (start = position(io); i+=1; continue)
|
|
c == '\r' && (start = position(io); i+=1; continue)
|
|
c in whitespace || break
|
|
end
|
|
seek(io, start)
|
|
return i
|
|
end
|
|
|
|
"""
|
|
Returns true if the line contains only (and, unless allowempty,
|
|
at least one of) the characters given.
|
|
"""
|
|
function linecontains(io::IO, chars; allow_whitespace = true,
|
|
eat = true,
|
|
allowempty = false)
|
|
start = position(io)
|
|
l = readline(io)
|
|
length(l) == 0 && return allowempty
|
|
|
|
result = allowempty
|
|
for c in l
|
|
c in whitespace && (allow_whitespace ? continue : (result = false; break))
|
|
c in chars && (result = true; continue)
|
|
result = false; break
|
|
end
|
|
!(result && eat) && seek(io, start)
|
|
return result
|
|
end
|
|
|
|
blankline(io::IO; eat = true) =
|
|
linecontains(io, "",
|
|
allow_whitespace = true,
|
|
allowempty = true,
|
|
eat = eat)
|
|
|
|
"""
|
|
Test if the stream starts with the given string.
|
|
`eat` specifies whether to advance on success (true by default).
|
|
`padding` specifies whether leading whitespace should be ignored.
|
|
"""
|
|
function startswith(stream::IO, s::AbstractString; eat = true, padding = false, newlines = true)
|
|
start = position(stream)
|
|
padding && skipwhitespace(stream, newlines = newlines)
|
|
result = true
|
|
for char in s
|
|
!eof(stream) && read(stream, Char) == char ||
|
|
(result = false; break)
|
|
end
|
|
!(result && eat) && seek(stream, start)
|
|
return result
|
|
end
|
|
|
|
function startswith(stream::IO, c::Char; eat = true)
|
|
if !eof(stream) && peek(stream) == UInt8(c)
|
|
eat && read(stream, Char)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function startswith(stream::IO, ss::Vector{<:AbstractString}; kws...)
|
|
any(s->startswith(stream, s; kws...), ss)
|
|
end
|
|
|
|
function startswith(stream::IO, r::Regex; eat = true, padding = false)
|
|
@assert Base.startswith(r.pattern, "^")
|
|
start = position(stream)
|
|
padding && skipwhitespace(stream)
|
|
line = readline(stream)
|
|
seek(stream, start)
|
|
m = match(r, line)
|
|
m === nothing && return ""
|
|
eat && @dotimes length(m.match) read(stream, Char)
|
|
return m.match
|
|
end
|
|
|
|
"""
|
|
Executes the block of code, and if the return value is `nothing`,
|
|
returns the stream to its initial position.
|
|
"""
|
|
function withstream(f, stream)
|
|
pos = position(stream)
|
|
result = f()
|
|
(result ≡ nothing || result ≡ false) && seek(stream, pos)
|
|
return result
|
|
end
|
|
|
|
"""
|
|
Consume the standard allowed markdown indent of
|
|
three spaces. Returns false if there are more than
|
|
three present.
|
|
"""
|
|
function eatindent(io::IO, n = 3)
|
|
withstream(io) do
|
|
m = 0
|
|
while startswith(io, ' ') m += 1 end
|
|
return m <= n
|
|
end
|
|
end
|
|
|
|
"""
|
|
Read the stream until startswith(stream, delim)
|
|
The delimiter is consumed but not included.
|
|
Returns nothing and resets the stream if delim is
|
|
not found.
|
|
"""
|
|
function readuntil(stream::IO, delimiter; newlines = false, match = nothing)
|
|
withstream(stream) do
|
|
buffer = IOBuffer()
|
|
count = 0
|
|
while !eof(stream)
|
|
if startswith(stream, delimiter)
|
|
if count == 0
|
|
return String(take!(buffer))
|
|
else
|
|
count -= 1
|
|
write(buffer, delimiter)
|
|
continue
|
|
end
|
|
end
|
|
char = read(stream, Char)
|
|
char == match && (count += 1)
|
|
!newlines && char == '\n' && break
|
|
write(buffer, char)
|
|
end
|
|
end
|
|
end
|
|
|
|
# TODO: refactor this. If we're going to assume
|
|
# the delimiter is a single character + a minimum
|
|
# repeat we may as well just pass that into the
|
|
# function.
|
|
|
|
"""
|
|
Parse a symmetrical delimiter which wraps words.
|
|
i.e. `*word word*` but not `*word * word`.
|
|
`repeat` specifies whether the delimiter can be repeated.
|
|
Escaped delimiters are not yet supported.
|
|
"""
|
|
function parse_inline_wrapper(stream::IO, delimiter::AbstractString; rep = false)
|
|
delimiter, nmin = string(delimiter[1]), length(delimiter)
|
|
withstream(stream) do
|
|
if position(stream) >= 1
|
|
# check the previous byte isn't a delimiter
|
|
skip(stream, -1)
|
|
(read(stream, Char) in delimiter) && return nothing
|
|
end
|
|
n = nmin
|
|
startswith(stream, delimiter^n) || return nothing
|
|
while startswith(stream, delimiter); n += 1; end
|
|
!rep && n > nmin && return nothing
|
|
!eof(stream) && Char(peek(stream)) in whitespace && return nothing
|
|
|
|
buffer = IOBuffer()
|
|
while !eof(stream)
|
|
char = read(stream, Char)
|
|
write(buffer, char)
|
|
if !(char in whitespace || char == '\n' || char in delimiter) && startswith(stream, delimiter^n)
|
|
trailing = 0
|
|
while startswith(stream, delimiter); trailing += 1; end
|
|
trailing == 0 && return String(take!(buffer))
|
|
write(buffer, delimiter ^ (n + trailing))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function showrest(io::IO)
|
|
start = position(io)
|
|
show(readstring(io))
|
|
println()
|
|
seek(io, start)
|
|
end
|