#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "math"
module RemiLib
extend self
# :nodoc:
SANITIZE_CHARS = {
'\e' => "ESC",
'\u{07}' => "BEL",
'\0' => "NUL",
'^' => "CFLEX",
'\u{08}' => "BS",
'\u{09}' => "HT",
'\u{0B}' => "VT",
'\u{0C}' => "FF",
'\u{1A}' => "SUB",
'\u{01}' => "SOH",
'\u{02}' => "STX",
'\u{03}' => "ETX",
'\u{04}' => "EOT",
'\u{05}' => "ENQ",
'\u{06}' => "ACK",
'\u{0E}' => "SO",
'\u{0F}' => "SI",
'\u{7F}' => "DEL"
}
# :nodoc:
SANITIZE_EXTRA_CHARS = {
'\u{0A}' => "LF",
'\u{0D}' => "CR"
}
# Sanitizes *str* so that it's somewhat safer to print into a terminal. This
# essentially strips out meta characters (`ESC`, `SUB`, `BEL`, etc.),
# replacing them with textual representations.
#
# If *leaveExtras* is `true`, then this will **NOT** remove additional
# characters such as Carriage Return and Line Feed.
@[AlwaysInline]
def sanitize(str : String, leaveExtras : Bool = false) : String
ret = str.gsub(SANITIZE_CHARS)
ret = ret.gsub(SANITIZE_EXTRA_CHARS) unless leaveExtras
ret
end
# Trims a null-padded string.
#
# If *fromFirst* is `false`, this removes all trailing null characters from a
# string. If *fromFirst* is `true`, all characters (null or not) are removed
# starting from the first null character found.
def trimNulls(str : String, fromFirst : Bool = true) : String
if fromFirst
first = str.index('\u{0}')
if first.nil?
str
else
str.[0...first]
end
else
str.rstrip('\u{0}')
end
end
# Returns a new `String` that contains *str*. If the length of *str* is less
# than *minLength*, then additional null bytes are added until the length is
# equal to *minLength*.
def padString(str : String, minLength : Int) : String
String.build(minLength) do |ret|
ret << str
(minLength - str.size).times { |_| ret << '\u{0}' }
end
end
# Writes *str* to *dest*. If the length of *str* is less than *minLength*,
# then additional null bytes are written to *dest* until the length is equal
# to *minLength*.
def writePaddedString(str : String, dest : IO, length : Int) : Nil
if str.size >= length
dest.write_string(str[0...length].to_slice)
else
dest.write_string(str.to_slice)
(length - str.size).times do |_|
dest.write_byte(0)
end
end
end
# Prints *text* to *str*, wrapping the text as it goes. *indent* is the
# number of *indentChar*s to write at the beginning of each line. *maxWidth*
# is the maximum length of a line of text.
#
# The first line can be shorter by applying an additional offset of
# *firstLineOffset*. Thus the first line will be at most
# `maxWidth - indent - firstLineOffset` long.
def buildWrapped(text : String, str : IO, *, indent : Int = 0, firstLineOffset : Int = 0,
maxWidth : Int = 80, indentChar : Char = ' ') : Nil
pos : Int32 = 0
maxLineWidth : Int32 = 0
inc : Int32 = 0
block : String = ""
indentStr : String = indentChar.to_s
firstLine : Bool = true
while pos < text.size
# We have extra to do if we're not on the first line (or indentFirstLine
# is true)
if firstLine
maxLineWidth = maxWidth - indent - firstLineOffset
else
str << indentStr * indent
maxLineWidth = maxWidth - indent
end
# See if we can write out the entire string on this first line or not
if (pos + maxLineWidth) >= text.size
str << text[pos..].strip
return
end
# Get a block of text
block = text[pos, maxLineWidth]
# If this block doesn't contain a space, just write out the entire block.
# If it wraps, it wraps... we don't handle hyphenation.
if !block.includes?(' ')
str << block.strip
str << '\n'
pos += maxLineWidth
elsif block[0] == ' '
# The only space is the very first character
str << block[1..]
pos += maxLineWidth
elsif block[-1] == ' '
# The space is the last character in the block, so just write the block minus the space
str << block.strip
str << '\n'
pos += block.size
else
if text[pos + block.size] == ' '
# Special case, we're at a word boundary
str << block
str << '\n'
pos += block.size + 1
else
# The space is somewhere in the middle of `block`
# Find the position of the final space in the block
inc = block.rindex(' ') || raise "Space suddenly missing"
str << block[...inc]
str << '\n'
pos += inc + 1
end
end
firstLine = false
end
end
# Prints *text* to a new string, wrapping the text as it goes, then returns
# the new string. *indent* is the number of *indentChar*s to write at the
# beginning of each line. *maxWidth* is the maximum length of a line of text.
#
# The first line can be shorter by applying an _additional_ offset
# of *firstLineOffset*. Thus the first line will be at most
# `maxWidth - indent - firstLineOffset` long.
@[Deprecated("Use #buildWrapped without an IO")]
def makeWrapped(text : String, *, indent : Int = 0, maxWidth : Int = 80,
firstLineOffset : Int = 0, indentChar : Char = ' ') : String
buildWrapped(text, indent: indent, maxWidth: maxWidth, firstLineOffset: firstLineOffset,
indentChar: indentChar)
end
def buildWrapped(text : String, *, indent : Int = 0, maxWidth : Int = 80,
firstLineOffset : Int = 0, indentChar : Char = ' ') : String
String.build do |str|
buildWrapped(text, str, indent: indent, maxWidth: maxWidth, firstLineOffset: firstLineOffset,
indentChar: indentChar)
end
end
# Splits `text` at each space, taking into account quoted strings. This is
# similar to how most (all?) shells split a command line into individual
# arguments.
#
# For example, calling `splitAsArgs(%{This "is a test" string})` would produce
# an array with these elements:
#
# 1. This
# 2. "is a test"
# 3. string
#
# If *removeQuotes* is `true`, then the outer quotes are removed from all
# returned strings.
def splitAsArgs(text : String, *, removeQuotes : Bool = false) : Array(String)
ret : Array(String) = text.split(Regex.new(%q{("[^"]+")|(\S+)}), remove_empty: true)
ret.delete(" ")
if removeQuotes
ret.map! do |str|
if str[0] == '"' && str[-1] == '"'
str[1...-1]
else
str
end
end
end
ret
end
end