art-standard-lib
Version:
The Standard Library for JavaScript that aught to be.
359 lines (307 loc) • 11.3 kB
text/coffeescript
FoundationMath = require './MathExtensions'
Types = require './TypesExtended'
{wordsRegex} = require './RegExpExtensions'
{intRand} = FoundationMath
{isString, isNumber, isPlainObject, isArray, stringIsPresent} = Types
{compactFlatten} = require './Core'
{isBrowser} = require './Environment'
escapedDoubleQuoteRegex = /[\\]["]/g
{floor} = Math
module.exports = class StringExtensions
###
IN: an array and optionally a string, in any order
joiner: the string
array-to-flatten-and-join: the array
OUT:
compactFlatten(array).join joiner || ""
NOTE: this uses Ruby's default value for joining - the empty array, not ',' which is JavaScripts
###
@compactFlattenJoin: (a, b) ->
array = null
joiner = if isString a
array = b
a
else
array = a
b || ""
compactFlatten(array).join joiner
@base62Characters: base62Characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
@fastHash: (string) ->
# 39 tokens
hash = 2147483647
if string.length > 0
for i in [0...string.length]
hash = ((hash << 5) - hash) + string.charCodeAt(i) & ( (1 << 31) - 1)
hash
###
# CaffeineScript once we have reduce + til support:
@fastHash: (string) ->
# 22 tokens
reduce hash, i til string.length inject 0
hash << 5
- hash
+ string.charCodeAt i
| 0
###
@randomString: randomString = (length = 32, chars = base62Characters, randomNumbers) ->
result = ''
charsLength = chars.length
if randomNumbers
(chars[randomNumbers[i] % charsLength] for i in [0...length] by 1).join ''
else
(chars[intRand charsLength] for i in [0...length] by 1).join ''
@cryptoRandomString: if isBrowser
{crypto} = global
unless crypto
realRequire = eval('require')
crypto = realRequire "crypto"
if crypto
(l = 16, c) -> randomString l, c, crypto.getRandomValues new Uint8Array l
else
console.warn "window.crypto not available, using standard random for cryptoRandomString"
(l = 16, c) -> randomString l, c
else
realRequire = eval('require')
crypto = realRequire "crypto"
(l, c) -> randomString l, c, crypto.randomBytes l
@randomBase62Character: -> base62Characters[intRand 62]
@replaceLast: (str, find, replaceWith) ->
index = str.lastIndexOf find
if index >= 0
str.substring(0, index) + replaceWith + str.substring(index + find.length)
else
str.toString()
@getPadding: getPadding = (length, padding = " ")->
out = ""
for i in [0...length]
out += padding
out
@pad: (str, length, padding, alignRight)->
str = String(str)
return str if str.length >= length
exactPadding = getPadding Math.max(length - str.length, 0), padding
if alignRight
exactPadding + str
else
str + exactPadding
# take a string of anything and produce a javascript legal string
@escapeDoubleQuoteJavascriptString: escapeDoubleQuoteJavascriptString = (str) =>
console.warn "DEPRICATED: escapeDoubleQuoteJavascriptString. USE: escapeJavascriptString"
s = String(str).replace(/[\\"]/g, "\\$&").replace /[\0\b\f\n\r\t\v\u2028\u2029]/g, (x) ->
switch x
when '\0' then "\\0"
when '\b' then "\\b"
when '\f' then "\\f"
when '\n' then "\\n"
when '\r' then "\\r"
when '\t' then "\\t"
when '\v' then "\\v"
when '\u2028' then "\\u2028"
when '\u2029' then "\\u2029"
s = '"' + s + '"'
###
SBD for a while I only had JSON.stringify here, but I hate seeing: "I said, \"hello.\""
when I could be seeing: 'I said, "hello."'
Is this going to break anything? I figure if you really need "" only, just use stringify.
###
@escapeJavascriptString: escapeJavascriptString = (str, withoutQuotes) =>
s = JSON.stringify str
# s = escapeDoubleQuoteJavascriptString str
if withoutQuotes
s.slice 1, -1
else if s.match escapedDoubleQuoteRegex
"'#{s.replace(escapedDoubleQuoteRegex, '"').replace(/'/g, "\\'").slice 1, -1}'"
else
s
@allIndexes: (str, regex) =>
indexes = []
throw new Error "regex must be a global RegExp" unless (regex instanceof RegExp) && regex.global
regex.lastIndex = 0
while result = regex.exec str
indexes.push result.index
lastIndex = result
indexes
@repeat: repeat = if " ".repeat
(str, times) -> str.repeat times # ECMASCRIPT 6
else
(str, count) ->
count == floor count
result = ''
if count > 0 && str.length > 0
while true
result += str if (count & 1) == 1
count >>>= 1
break if count == 0
str += str
result
@rightAlign: (str, width) ->
if str.length >= width
str
else
repeat(" ", width - str.length) + str
# Note: regex must be global
@eachMatch: (str, regex, f) =>
regex.lastIndex = 0
f result while result = regex.exec str
null
standardIndent =
joiner: ', '
openObject: '{'
openArray: '['
closeObject: "}"
closeArray: "]"
@jsStringify: (obj) ->
jsStringifyR obj, ""
jsStringifyR = (o, s) ->
if isPlainObject o
s += "{"
first = true
for k, v of o
if first
first = false
else
s += ","
if /^[a-zA-Z_][a-zA-Z0-9_]*$/.test k
s += k
else
s += JSON.stringify k
s += ":"
s = jsStringifyR v, s
s + "}"
else if isArray o
s += "["
first = true
for el in o
if first
first = false
else
s += ","
s = jsStringifyR el, s
s + "]"
else
s + JSON.stringify o
@consistentJsonStringify: consistentJsonStringify = (object, indent) ->
out = if object == false || object == true || object == null || isNumber object
"" + object
else if isString object
JSON.stringify object
else
indentObject = if indent
if typeof indent == "string"
joiner: ",\n#{indent}"
openObject: "{\n#{indent}"
openArray: "[\n#{indent}"
closeObject: "\n}"
closeArray: "\n]"
totalIndent: indent
indent: indent
else
totalIndent: totalIndent = indent.indent + lastTotalIndent = indent.totalIndent
joiner: ",\n#{totalIndent}"
openObject: "{\n#{totalIndent}"
openArray: "[\n#{totalIndent}"
closeObject: "\n#{lastTotalIndent}}"
closeArray: "\n#{lastTotalIndent}]"
indent: indent.indent
{joiner, openObject, openArray, closeObject, closeArray} = indentObject || standardIndent
if isPlainObject object
openObject + (
for k in (Object.keys object).sort() when object[k] != undefined
JSON.stringify(k) + ": " + consistentJsonStringify object[k], indentObject
).join(joiner) +
closeObject
else if isArray object
openArray +
(consistentJsonStringify v, indentObject for v in object).join(joiner) +
closeArray
else
Neptune.Art.StandardLib.log.error error = "invalid object type for Json. Expecting: null, false, true, number, string, plain-object or array", object
throw new Error error
@splitRuns = (str) ->
return [] if str.length == 0
lastCh = str[0]
chCount = 1
result = []
for i in [1...str.length] by 1
ch = str[i]
if ch == lastCh
chCount++
else
result.push [lastCh, chCount]
chCount = 1
lastCh = ch
result.push [lastCh, chCount]
result
@eachRunAsCharCodes = (str, f) ->
lastCh = str.charCodeAt 0
chCount = 1
for i in [1...str.length] by 1
ch = str.charCodeAt i
if ch == lastCh
chCount++
else
f lastCh, chCount
chCount = 1
lastCh = ch
f lastCh, chCount
null
###
TODO: I think this can be generalized to cover most all ellipsies and word-wrap scenarios:
a) have an options object with options:
maxLength: number # similar to current maxLength
minLength: number # currently implied to be maxLength / 2, in additional customizable, it would also be optional
brokenWordEllipsis: "…" # used when only part of a word is included
moreWordsEllipsis: "…" # used when there are more words, but the last word is whole
wordLengthFunction: (string) -> string.length
# can be replaced with, say, the font pixel-width for a string
# in this way, this function can be used by text-layout
# minLength and maxLength would then be in pixels
breakWords: false # currently, this is effectively true - will break the last word on line in most situations
breakOnlyWord: true # even if breakWords is false, if this is the only word on the line and it doesn't fit, should we break it?
# should this even be an option?
# future:
wordBreakFunction: (word, maxLength) -> shorterWord
# given a word and the maximum length of that word, returns
# a word <= maxLength according to wordLengthFunction
b) Use cases
- TextLayout - uses pixels for length rather than characters
- Art.Engine.Element 'flow' layout
- if the input was an array of "words" and
- wordLengthFunction returns the Element's width...
I think this works. We'd need a way to handle margins though. I think this works:
spaceLength: (leftWord, rightWord) -> 1
- Shortend user display names:
Options:
wordBreakFunction: (word, maxLength) -> word[0]
brokenWordEllipsis: "." or ""
Example Output:
"Shane Delamore", 10 > "Shane D." or
"Shane Delamore", 10 > "Shane D"
Or, just leave breakwords: false and get:
"Shane Delamore", 10 > "Shane"
c) returns both the output string and the "string remaining" - everything not included
d) alternate input: an array of strings already broken up by words - the "remainging" return value would then also be an array of "words"
(this would be for efficiency when doing multi-line layout)
Right now, it works as follows:
The output string is guaranteed to be:
<= maxLength
>= maxLength / 2 in almost all secenarios as long as inputString is >= maxLength / 2
###
@humanFriendlyShorten: (inputString, maxLength) ->
throw new error "maxLength must be > 0" unless maxLength > 0
inputString = inputString.trim()
return inputString unless inputString.length > maxLength
minLength = maxLength / 2
stringParts = inputString.split /\s+/
string = ""
for part in stringParts
if string.length == 0
string = part
else if (string.length < minLength) || string.length + part.length + 2 <= maxLength
string += " #{part}"
else
break
string = string.slice(0, maxLength - 1).trim() if string.length > maxLength
string + "…"
@stripTrailingWhitespace: (a) ->
a.split(/[ ]*\n/).join("\n").split(/[ ]*$/)[0].replace(/\n+$/,'')