art-standard-lib
Version:
The Standard Library for JavaScript that aught to be.
375 lines (311 loc) • 14.5 kB
text/coffeescript
{isString, objectName, isPlainObject, isPlainArray, isTypedArray, isFunction, isNumber} = require '../TypesExtended'
{max} = Math
{pad, stripTrailingWhitespace, escapeJavascriptString} = require '../StringExtensions'
{inspect} = require './Inspector'
{objectKeyCount} = require '../ObjectExtensions'
{toInspectedObjects} = require './InspectedObjects'
{w} = require '../ArrayExtensions'
{object} = require '../Iteration'
indentString = ' '
indentLength = indentString.length
newLineWithIndentString = "\n#{indentString}"
formattedInspectObject = (m, maxLineLength, options) ->
{colorize} = options
inspectedLength = 0
forceMultilineOutput = false
shouldBeOnOwnLine = false
keyCount = 0
inspectedValues = for key, value of m
keyCount++
inspected = formattedInspectRecursive value, maxLineLength - indentLength, options
if inspected.match /\n/
inspected = if inspected.match /^\[\]/
"#{inspected}"
else
newLineWithIndentString + inspected.replace /\n/g, newLineWithIndentString
inspected += "\n" unless /\n\s*$/.test inspected
else if ansiSafeStringLength(inspected) > maxLineLength - (key.length + 2)
inspected = "#{newLineWithIndentString}#{inspected}\n"
key = inspect key unless /^[-~!@\#$%^&*_+=|\\<>?\/.$\w\u007f-\uffff]+$/.test key
inspectedLength += ansiSafeStringLength(inspected) + key.length + 2
forceMultilineOutput ||= shouldBeOnOwnLine # if previous entry should be on own line, force all on own line
# This regexp can cause Chrome's RegExp engine to become extraordinarily inefficient.
# SEE: https://jsperf.com/pathological-regexp
# This string causes the provlem with this regexp: """'{"mappings":{"post_search":{"_parent":{"type":"topic"},"properties":{"topicId":{"type":"keyword"},"userId":{"type":"keyword"},"postOrder":{"type":"long"},"createdAt":{"type":"long"},"updatedAt":{"type":"long"},"isChapterStart":{"type":"boolean"},"caption":{"type":"text","analyzer":"english"},"tags":{"type":"text"},"mentions":{"type":"text"},"mediaMimeType":{"type":"keyword"},"mediaAspectRatio":{"type":"short"},"mediaUrl":{"type":"keyword","index":false},"mediaFocus":{"type":"object","index":false},"mediaDimensions":{"type":"object","index":false},"mediaColorInfo":{"type":"object","index":false},"templateUrl":{"type":"keyword","index":false},"templateType":{"type":"keyword"},"templateDropInCount":{"type":"byte"},"templateText":{"type":"text","analyzer":"english"},"templateDropInLocations":{"type":"nested","index":false},"templateUses":{"type":"integer"},"activityCount":{"type":"integer"},"lastActivityAt":{"type":"long"},"messageCount":{"type":"integer","index":false},"participantCount":{"type":"integer","index":false},"lastActiveUserId":{"type":"keyword","index":false},"lastMessageId":{"type":"keyword","index":false}}},"topic_search":{"_parent":{"type":"user"},"properties":{"title":{"type":"text","analyzer":"english"},"createdAt":{"type":"long"},"updatedAt":{"type":"long"},"lastPostCreatedAt":{"type":"long"},"lastPostId":{"type":"keyword"},"lastChapterPostId":{"type":"keyword"},"postCount":{"type":"integer"},"followerCount":{"type":"integer"},"activityCount":{"type":"long"},"messageCount":{"type":"long"},"isProfileTopic":{"type":"boolean"}}},"user_search":{"properties":{"displayName":{"type":"text","analyzer":"english"},"postCount":{"type":"integer"},"topicCount":{"type":"short"},"followerCount":{"type":"integer"},"messageCount":{"type":"integer"},"lastTopicCreatedAt":{"type":"long"},"lastPostCreatedAt":{"type":"long"},"profileTopicId":{"type":"keyword","index":false}}}},"settings":{}}'"""
# HACK - right now I'm just assuming any string longer than 100 chars should be on its own line
shouldBeOnOwnLine = inspected.length > 100 || !inspected.match /^([^,:]|\(.*\)|\{.*\}|\".*\"|\'.*\'|\[.*\])*$/
[key, inspected, value]
objectStart = "{}"
objectStart = colorize.grey objectStart
if keyCount == 0
objectStart
else
index = 0
finalInspectedValues = for [k, v, value] in inspectedValues
key = "#{k}:"
key = colorize.blue key
# if isPlainObject(value) && objectKeyCount(value) == 1
# "#{key} #{v}"
# else
"#{key}\t#{v}"
finalInspectedValues.join if !forceMultilineOutput && maxLineLength >= inspectedLength + (inspectedValues.length - 1) * 2
",\t"
else
"\n"
formattedInspectArray = (m, maxLineLength, options) ->
{colorize} = options
lengthOfInspectedValues = 0
lastWasObject = false
lastWasArray = false
maxArrayLength = options.maxArrayLength || 100
objectsMustBeExplicit = false
oneLinerOk = true
inspectedValuesContainNewlines = false
for value, i in m
if isPlainObject value
oneLinerOk = false if i < m.length - 1 # non-tail object
objectsMustBeExplicit = true if lastWasObject
lastWasObject = true
else
lastWasObject = false
inspectedValues = for value in m.slice 0, maxArrayLength
oneLinerOk = false if lastWasArray
if isInspectableArray value
lastWasArray = true
inspected = formattedInspectRecursive value, maxLineLength - indentLength, options
inspectedHasNewlines = /\n/.test inspected
if objectsMustBeExplicit && isPlainObject(value) && objectKeyCount(value) > 0
objectStart = "{}"
objectStart = colorize.grey objectStart
inspected = if inspectedHasNewlines
"#{objectStart}#{newLineWithIndentString}#{inspected.replace(/\n/g, newLineWithIndentString)}"
else
"#{objectStart} #{inspected}"
if inspectedHasNewlines
oneLinerOk = false
inspected = inspected.replace /\n/g, newLineWithIndentString
inspected += "\n" unless /\n\s*$/.test inspected
lengthOfInspectedValues += ansiSafeStringLength inspected
inspected
lengthOfCommas = (inspectedValues.length - 1) * 2
lengthOfStartBrackets = 3
arrayStart = if isTypedArray m then "{#{objectName m}}" else "[]"
if m.length > maxArrayLength
arrayStart += " <length: #{m.length}>"
suffix = "..."
arrayStart = colorize.grey arrayStart
if oneLinerOk && maxLineLength >= lengthOfStartBrackets + lengthOfCommas + lengthOfInspectedValues
if inspectedValues.length == 0
arrayStart
else
"#{arrayStart} #{inspectedValues.join ",\t"}#{suffix ? ""}"
else
"""
#{arrayStart}
#{inspectedValues.join "\n "}#{if suffix then "\n #{suffix}" else ''}
"""
escapeForBlockString = (str) =>
String str
.replace /[\\\0\b\f\r\t\v\u001b\u2028\u2029]/g, (x) ->
switch x
when '\\' then '\\\\'
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"
when '\u001b' then '\\u001b'
# escape leading and trailing spaces
.replace /^[\n ]+|[\n ]+$|[ ]+(?=\n)/g, (x) ->
escapeJavascriptString x, true
.replace /\ /g, '\\s'
cafScriptWordStringRegExp = ///
^(?=[^'":])
(
[^ \# \s \0-\x20 \x7f ; , () [ \] {} \\ ]
| \# ([^{]|$)
)+
$
///
formattedInspectString = (m, options) ->
out = switch
when cafScriptWordStringRegExp.test m
":#{m}"
when /[^\n\s].*\n(.|\n)*[^\n\s]/.test m
('"""' + newLineWithIndentString +
escapeForBlockString(m).replace /\n/g, newLineWithIndentString
).replace /\ +\n/g, '\n'
# else if m.length > 10 && m.match / /
# TODO: support ""-block output using a word-wrap strategy if we can do-so isomorphically.
# i.e. the output string would generate exactly the input string in Caffeine-Script
else
escapeJavascriptString m
if options.colorize
options.colorize.green out
else out
isInspectableArray = (v) ->
isPlainArray(v) ||
isTypedArray v
formattedInspectRecursive = (m, maxLineLength, options) ->
if isPlainObject m then formattedInspectObject m, maxLineLength, options
else if isInspectableArray m then formattedInspectArray m, maxLineLength, options
else if isString m then formattedInspectString m, options
else
options.colorize.yellow inspect m
###
TODO:
special mode for a chunk of lines that all have this pattern:
/^\s*([a-z]:\t)*[^\t]+$/
Example:
hi: there: my: friends: "my value"
somethingElseIThough: indexAllMyThings: withThis: "foo"
Currently that becomes:
hi: there: my: friends: "my value"
somethingElseIThough: indexAllMyThings: withThis: "foo"
Which is pretty awkward. I want:
hi: there: my: friends: "my value"
somethingElseIThough: indexAllMyThings: withThis: "foo"
Basically, replace all but the last tab with a space.
But only if ALL lines in a chunk are this pattern.
CounterExample:
properties:
autoTags: type: "text", analyzer: "standard"
autoText: type: "text", analyzer: "english"
updatedAt: type: "long"
createdAt: type: "long"
title: type: "text", analyzer: "english"
userId: type: "keyword"
lastPostCreatedAt: type: "long"
lastPostId: type: "keyword"
lastChapterPostId: type: "keyword"
postCount: type: "integer"
followerCount: type: "integer"
activityCount: type: "long"
messageCount: type: "long"
isProfileTopic: type: "boolean"
private: type: "boolean"
Should NOT look like this:
properties:
autoTags: type: "text", analyzer: "standard"
autoText: type: "text", analyzer: "english"
updatedAt: type: "long"
createdAt: type: "long"
title: type: "text", analyzer: "english"
userId: type: "keyword"
lastPostCreatedAt: type: "long"
lastPostId: type: "keyword"
lastChapterPostId: type: "keyword"
postCount: type: "integer"
followerCount: type: "integer"
activityCount: type: "long"
messageCount: type: "long"
isProfileTopic: type: "boolean"
private: type: "boolean"
###
alignTabs = (linesString, maxLineLength = 10000) ->
tabStops = 1
lines = linesString.split "\n"
# if all lines have the same number of columns, then numColumns == that number
# Otherwise, it == 2.
# In that case, all tabs after the first tabs are treated as spaces.
# This a poor man's attempt to smartly align things like:
# {} AttributeName: "chatRoom", AttributeType: "S"
# {} AttributeName: "createdAt", AttributeType: "N"
# {} AttributeName: "id", AttributeType: "S"
# A better test would be if each column had the same label...
maxColumnSizes = []
maxColumnWidth = maxLineLength / 2
for line in lines when (elements = line.split "\t").length > 1
for el, i in elements when i < elements.length - 1 && (i == 0 || ansiSafeStringLength(el) < maxColumnWidth)
maxColumnSizes.push 0 if maxColumnSizes.length == i
maxColumnSizes[i] = max maxColumnSizes[i], ansiSafeStringLength(el) + 1
alignedLines = for line in lines
spaceAvailable = maxLineLength - ansiSafeStringLength line
elements = line.split "\t"
r = if elements.length > 1
for el, i in elements
elLength = ansiSafeStringLength el
if i == elements.length - 1
el
else if maxColumnSizes[i]? && (expandAmount = maxColumnSizes[i] - elLength - 1) <= spaceAvailable
spaceAvailable -= expandAmount
el + pad '', maxColumnSizes[i] - elLength
else
spaceAvailable = 0
"#{el} "
else
elements
r.join ""
alignedLines.join "\n"
ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]/g
stripAnsi = (str) ->
if ansiRegex.test str
str.replace ansiRegex, ''
else
str
ansiSafeStringLength = (str)->
throw new Error "not string" unless isString str
if ansiRegex.test str
str = str.replace ansiRegex, ''
str.length
postWhitespaceFormatting = (maxLineLength, string) ->
lastIndent = 0
sameIndentGroup = []
outLines = []
alignTabsInSameIndentGroup = ->
return unless 0 < sameIndentGroup.length
str = sameIndentGroup.join "\n"
sameIndentGroup = []
outLines.push alignTabs str, maxLineLength
for line in string.split "\n"
line = line.replace /\s+$/g, ''
if lastIndent != indent = ansiSafeStringLength line.match(/^ *-?/)[0]
alignTabsInSameIndentGroup()
# outLines.push "" if indent < lastIndent
sameIndentGroup.push line
lastIndent = indent
alignTabsInSameIndentGroup()
outLines.join '\n'
colorNames = w("red yellow green blue grey")
colorizeFunctions = object colorNames, (c) -> (str) -> str[c] ? str
identity = (s) -> s
passThroughColorizeFunctions = object colorNames, -> identity
module.exports = class FormattedInspect
@ansiRegex: ansiRegex
@stripAnsi: stripAnsi
@ansiSafeStringLength: ansiSafeStringLength
@alignTabs: alignTabs
@_escapeForBlockString: escapeForBlockString
@formattedInspectString: formattedInspectString
@failsafeInspect: failsafeInspect = (toInspect) ->
"typeof: #{typeof toInspect}\n" +
"constructor: #{toInspect?.constructor && toInspect?.constructor?.name}\n" +
switch
when isInspectableArray toInspect then "length: #{toInspect.length}\njoined: [#{toInspect.join(', ')}]"
when toInspect? && typeof toInspect == 'object' then "keys: #{Object.keys(toInspect).join ', '}"
else "toString: #{toInspect}"
@formattedInspect: (toInspect, options = {}) ->
try
if isNumber options
options = maxLineLength: options
else
unless isPlainObject options
console.error invalid: {options}
throw new Error "invalid options object type: #{typeof options}"
options.maxLineLength ?= global.process?.stdout?.columns || 80
{maxLineLength} = options
options.colorize = if options.color
colorizeFunctions
else
passThroughColorizeFunctions
out = postWhitespaceFormatting maxLineLength,
formattedInspectRecursive toInspectedObjects(toInspect), maxLineLength, options
.replace /\n\n$/, "\n"
catch error
out = "Error in formattedInspect: #{error}\n#{failsafeInspect toInspect}"
console.error out, {error, toInspect, options}
out