initial commit of actions

This commit is contained in:
Dominik Polakovics Polakovics 2026-01-31 18:56:04 +01:00
commit 949ece5785
44660 changed files with 12034344 additions and 0 deletions

View file

@ -0,0 +1,14 @@
ISC License (ISC)
Copyright (c) 2017, Mark Wubben <mark@novemberborn.net> (novemberborn.net)
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.

View file

@ -0,0 +1,70 @@
# concordance
Compare, format, diff and serialize any JavaScript value. Built for Node.js 10
and above.
## Behavior
Concordance recursively describes JavaScript values, whether they're booleans or
complex object structures. It recurses through all enumerable properties, list
items (e.g. arrays) and iterator entries.
The same algorithm is used when comparing, formatting or diffing values. This
means Concordance's behavior is consistent, no matter how you use it.
### Comparison details
* [Object wrappers](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/types%20%26%20grammar/ch3.md#boxing-wrappers)
are compared both as objects and unwrapped values. Thus Concordance always
treats `Object(1)` as different from `1`.
* `-0` is distinct from `0`.
* `NaN` equals `NaN`.
* The `Argument` values can be compared to a regular array.
* `Error` names and messages are always compared, even if these are not
enumerable properties.
* `Function` values are compared by identity only. Names are always formatted
and serialized.
* `Global` objects are considered equal.
* `Map` keys and `Set` items are compared in-order.
* `Object` string properties are compared according to the [traversal order](http://2ality.com/2015/10/property-traversal-order-es6.html).
Symbol properties are compared by identity.
* `Promise` values are compared by identity only.
* `Symbol` values are compared by identity only.
* Recursion stops whenever a circular reference is encountered. If the same
cycle is present in the actual and expected values they're considered equal,
but they're unequal otherwise.
### Formatting details
Concordance strives to format every aspect of a value that is used for
comparisons. Formatting is optimized for human legibility.
Strings enjoy special formatting:
* When used as keys, line break characters are escaped
* Otherwise, multi-line strings are formatted using backticks, and line break
characters are replaced by [control pictures](http://graphemica.com/blocks/control-pictures).
Similarly, line breaks in symbol descriptions are escaped.
### Diffing details
Concordance tries to minimize diff lines. This is difficult with object values,
which may have similar properties but a different constructor. Multi-line
strings are compared line-by-line.
### Serialization details
Concordance can serialize any value for later use. Deserialized values can be
compared to each other or to regular JavaScript values. The deserialized
value should be passed as the **actual** value to the comparison and diffing
methods. Certain value comparisons behave differently when the **actual** value
is deserialized:
* `Argument` values can only be compared to other `Argument` values.
* `Function` values are compared by name.
* `Promise` values are compared by their constructor and additional enumerable
properties, but not by identity.
* `Symbol` values are compared by their string serialization. [Registered](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol#Shared_symbols_in_the_global_symbol_registry)
and [well-known symbols](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol#Well-known_symbols)
will never equal symbols with similar descriptions.

View file

@ -0,0 +1,21 @@
'use strict'
const compare = require('./lib/compare')
const describe = require('./lib/describe')
const diff = require('./lib/diff')
const format = require('./lib/format')
const serialize = require('./lib/serialize')
exports.compare = compare.compare
exports.compareDescriptors = compare.compareDescriptors
exports.describe = describe
exports.diff = diff.diff
exports.diffDescriptors = diff.diffDescriptors
exports.format = format.format
exports.formatDescriptor = format.formatDescriptor
exports.serialize = serialize.serialize
exports.deserialize = serialize.deserialize

View file

@ -0,0 +1,35 @@
'use strict'
class Circular {
constructor () {
this.stack = new Map()
}
add (descriptor) {
if (this.stack.has(descriptor)) throw new Error('Already in stack')
if (descriptor.isItem !== true && descriptor.isMapEntry !== true && descriptor.isProperty !== true) {
this.stack.set(descriptor, this.stack.size + 1)
}
return this
}
delete (descriptor) {
if (this.stack.has(descriptor)) {
if (this.stack.get(descriptor) !== this.stack.size) throw new Error('Not on top of stack')
this.stack.delete(descriptor)
}
return this
}
has (descriptor) {
return this.stack.has(descriptor)
}
get (descriptor) {
return this.stack.has(descriptor)
? this.stack.get(descriptor)
: 0
}
}
module.exports = Circular

View file

@ -0,0 +1,22 @@
'use strict'
class Indenter {
constructor (level, step) {
this.level = level
this.step = step
this.value = step.repeat(level)
}
increase () {
return new Indenter(this.level + 1, this.step)
}
decrease () {
return new Indenter(this.level - 1, this.step)
}
toString () {
return this.value
}
}
module.exports = Indenter

View file

@ -0,0 +1,24 @@
'use strict'
class Registry {
constructor () {
this.counter = 0
this.map = new WeakMap()
}
has (value) {
return this.map.has(value)
}
get (value) {
return this.map.get(value).descriptor
}
alloc (value) {
const index = ++this.counter
const pointer = { descriptor: null, index }
this.map.set(value, pointer)
return pointer
}
}
module.exports = Registry

View file

@ -0,0 +1,103 @@
'use strict'
const Circular = require('./Circular')
const constants = require('./constants')
const describe = require('./describe')
const recursorUtils = require('./recursorUtils')
const shouldCompareDeep = require('./shouldCompareDeep')
const symbolProperties = require('./symbolProperties')
const AMBIGUOUS = constants.AMBIGUOUS
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function shortcircuitPrimitive (value) {
if (value === null || value === undefined || value === true || value === false) return true
const type = typeof value
if (type === 'string' || type === 'symbol') return true
// Don't shortcircuit NaN values
if (type === 'number') return !isNaN(value)
return false
}
function compareDescriptors (lhs, rhs) {
const lhsCircular = new Circular()
const rhsCircular = new Circular()
const lhsStack = []
const rhsStack = []
let topIndex = -1
do {
let result
if (lhsCircular.has(lhs)) {
result = lhsCircular.get(lhs) === rhsCircular.get(rhs)
? DEEP_EQUAL
: UNEQUAL
} else if (rhsCircular.has(rhs)) {
result = UNEQUAL
} else {
result = lhs.compare(rhs)
}
if (result === UNEQUAL) return false
if (result !== DEEP_EQUAL) {
if (!shouldCompareDeep(result, lhs, rhs)) return false
if (result === AMBIGUOUS && lhs.isProperty === true) {
// Replace both sides by a pseudo-descriptor which collects symbol
// properties instead.
lhs = new symbolProperties.Collector(lhs, lhsStack[topIndex].recursor)
rhs = new symbolProperties.Collector(rhs, rhsStack[topIndex].recursor)
// Replace the current recursors so they can continue correctly after
// the collectors have been "compared". This is necessary since the
// collectors eat the first value after the last symbol property.
lhsStack[topIndex].recursor = recursorUtils.unshift(lhsStack[topIndex].recursor, lhs.collectAll())
rhsStack[topIndex].recursor = recursorUtils.unshift(rhsStack[topIndex].recursor, rhs.collectAll())
}
lhsCircular.add(lhs)
rhsCircular.add(rhs)
lhsStack.push({ subject: lhs, recursor: lhs.createRecursor() })
rhsStack.push({ subject: rhs, recursor: rhs.createRecursor() })
topIndex++
}
while (topIndex >= 0) {
lhs = lhsStack[topIndex].recursor()
rhs = rhsStack[topIndex].recursor()
if (lhs !== null && rhs !== null) {
break
}
if (lhs === null && rhs === null) {
const lhsRecord = lhsStack.pop()
const rhsRecord = rhsStack.pop()
lhsCircular.delete(lhsRecord.subject)
rhsCircular.delete(rhsRecord.subject)
topIndex--
} else {
return false
}
}
} while (topIndex >= 0)
return true
}
exports.compareDescriptors = compareDescriptors
function compare (actual, expected, options) {
if (Object.is(actual, expected)) return { pass: true }
// Primitive values should be the same, so if actual or expected is primitive
// then the values will never compare.
if (shortcircuitPrimitive(actual) || shortcircuitPrimitive(expected)) return { pass: false }
actual = describe(actual, options)
expected = describe(expected, options)
const pass = compareDescriptors(actual, expected)
return { actual, expected, pass }
}
exports.compare = compare

View file

@ -0,0 +1,48 @@
'use strict'
const constants = require('../constants')
const object = require('./object')
const AMBIGUOUS = constants.AMBIGUOUS
const UNEQUAL = constants.UNEQUAL
function describe (props) {
return new DescribedArgumentsValue(Object.assign({
// Treat as an array, to allow comparisons with arrays
isArray: true,
isList: true,
}, props, { ctor: 'Arguments' }))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedArgumentsValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('ArgumentsValue')
exports.tag = tag
class ArgumentsValue extends object.ObjectValue {
compare (expected) {
if (expected.isComplex !== true) return UNEQUAL
// When used on the left-hand side of a comparison, argument values may be
// compared to arrays.
if (expected.stringTag === 'Array') return AMBIGUOUS
return super.compare(expected)
}
}
Object.defineProperty(ArgumentsValue.prototype, 'tag', { value: tag })
const DescribedArgumentsValue = object.DescribedMixin(ArgumentsValue)
class DeserializedArgumentsValue extends object.DeserializedMixin(ArgumentsValue) {
compare (expected) {
// Deserialized argument values may only be compared to argument values.
return expected.isComplex === true && expected.stringTag === 'Array'
? UNEQUAL
: super.compare(expected)
}
}

View file

@ -0,0 +1,29 @@
'use strict'
const typedArray = require('./typedArray')
function describe (props) {
return new DescribedArrayBufferValue(Object.assign({
buffer: Buffer.from(props.value),
// Set isArray and isList so the property recursor excludes the byte accessors
isArray: true,
isList: true,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedArrayBufferValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('ArrayBufferValue')
exports.tag = tag
// ArrayBuffers can be represented as regular Buffers, allowing them to be
// treated as TypedArrays for the purposes of this package.
class ArrayBufferValue extends typedArray.TypedArrayValue {}
Object.defineProperty(ArrayBufferValue.prototype, 'tag', { value: tag })
const DescribedArrayBufferValue = typedArray.DescribedMixin(ArrayBufferValue)
const DeserializedArrayBufferValue = typedArray.DeserializedMixin(ArrayBufferValue)

View file

@ -0,0 +1,51 @@
'use strict'
const stringPrimitive = require('../primitiveValues/string').tag
const recursorUtils = require('../recursorUtils')
const object = require('./object')
function describe (props) {
return new DescribedBoxedValue(props)
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedBoxedValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('BoxedValue')
exports.tag = tag
class BoxedValue extends object.ObjectValue {}
Object.defineProperty(BoxedValue.prototype, 'tag', { value: tag })
class DescribedBoxedValue extends object.DescribedMixin(BoxedValue) {
constructor (props) {
super(props)
this.unboxed = props.unboxed
}
createListRecursor () {
return recursorUtils.NOOP_RECURSOR
}
createPropertyRecursor () {
if (this.unboxed.tag !== stringPrimitive) return super.createPropertyRecursor()
// Just so that createPropertyRecursor() skips the index-based character
// properties.
try {
this.isList = true
return super.createPropertyRecursor()
} finally {
this.isList = false
}
}
createRecursor () {
return recursorUtils.unshift(super.createRecursor(), this.unboxed)
}
}
const DeserializedBoxedValue = object.DeserializedMixin(BoxedValue)

View file

@ -0,0 +1,29 @@
'use strict'
const typedArray = require('./typedArray')
function describe (props) {
return new DescribedDataViewValue(Object.assign({
buffer: typedArray.getBuffer(props.value),
// Set isArray and isList so the property recursor excludes the byte accessors
isArray: true,
isList: true,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedDataViewValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('DataViewValue')
exports.tag = tag
// DataViews can be represented as regular Buffers, allowing them to be treated
// as TypedArrays for the purposes of this package.
class DataViewValue extends typedArray.TypedArrayValue {}
Object.defineProperty(DataViewValue.prototype, 'tag', { value: tag })
const DescribedDataViewValue = typedArray.DescribedMixin(DataViewValue)
const DeserializedDataViewValue = typedArray.DeserializedMixin(DataViewValue)

View file

@ -0,0 +1,89 @@
'use strict'
const dateTime = require('date-time')
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const object = require('./object')
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (props) {
const date = props.value
const invalid = isNaN(date.valueOf())
return new DescribedDateValue(Object.assign({}, props, { invalid }))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedDateValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('DateValue')
exports.tag = tag
function formatDate (date) {
// Always format in UTC. The local timezone shouldn't be used since it's most
// likely different from that of CI servers.
return dateTime({
date,
local: false,
showTimeZone: true,
showMilliseconds: true,
})
}
class DateValue extends object.ObjectValue {
constructor (props) {
super(props)
this.invalid = props.invalid
}
compare (expected) {
const result = super.compare(expected)
if (result !== SHALLOW_EQUAL) return result
return (this.invalid && expected.invalid) || Object.is(this.value.getTime(), expected.value.getTime())
? SHALLOW_EQUAL
: UNEQUAL
}
formatShallow (theme, indent) {
const string = formatUtils.formatCtorAndStringTag(theme, this) + ' ' +
(this.invalid ? theme.date.invalid : formatUtils.wrap(theme.date.value, formatDate(this.value))) + ' ' +
theme.object.openBracket
return super.formatShallow(theme, indent).customize({
finalize (innerLines) {
return innerLines.isEmpty
? lineBuilder.single(string + theme.object.closeBracket)
: lineBuilder.first(string)
.concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags())
.append(lineBuilder.last(indent + theme.object.closeBracket))
},
maxDepth () {
return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket)
},
})
}
serialize () {
const iso = this.invalid ? null : this.value.toISOString()
return [this.invalid, iso, super.serialize()]
}
}
Object.defineProperty(DateValue.prototype, 'tag', { value: tag })
const DescribedDateValue = object.DescribedMixin(DateValue)
class DeserializedDateValue extends object.DeserializedMixin(DateValue) {
constructor (state, recursor) {
super(state[2], recursor)
this.invalid = state[0]
this.value = new Date(this.invalid ? NaN : state[1])
}
}

View file

@ -0,0 +1,133 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const isEnumerable = require('../isEnumerable')
const lineBuilder = require('../lineBuilder')
const NOOP_RECURSOR = require('../recursorUtils').NOOP_RECURSOR
const object = require('./object')
const UNEQUAL = constants.UNEQUAL
function describe (props) {
const error = props.value
return new DescribedErrorValue(Object.assign({
nameIsEnumerable: isEnumerable(error, 'name'),
name: error.name,
messageIsEnumerable: isEnumerable(error, 'message'),
message: error.message,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedErrorValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('ErrorValue')
exports.tag = tag
class ErrorValue extends object.ObjectValue {
constructor (props) {
super(props)
this.name = props.name
}
compare (expected) {
return this.tag === expected.tag && this.name === expected.name
? super.compare(expected)
: UNEQUAL
}
formatShallow (theme, indent) {
const name = this.name || this.ctor
let string = name
? formatUtils.wrap(theme.error.name, name)
: formatUtils.wrap(theme.object.stringTag, this.stringTag)
if (this.ctor && this.ctor !== name) {
string += ' ' + formatUtils.wrap(theme.error.ctor, this.ctor)
}
if (this.stringTag && this.stringTag !== this.ctor && this.name && !this.name.includes(this.stringTag)) {
string += ' ' + formatUtils.wrap(theme.object.secondaryStringTag, this.stringTag)
}
string += ' ' + theme.object.openBracket
return super.formatShallow(theme, indent).customize({
finalize (innerLines) {
return innerLines.isEmpty
? lineBuilder.single(string + theme.object.closeBracket)
: lineBuilder.first(string)
.concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags())
.append(lineBuilder.last(indent + theme.object.closeBracket))
},
maxDepth () {
return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket)
},
})
}
serialize () {
return [this.name, super.serialize()]
}
}
Object.defineProperty(ErrorValue.prototype, 'tag', { value: tag })
class DescribedErrorValue extends object.DescribedMixin(ErrorValue) {
constructor (props) {
super(props)
this.nameIsEnumerable = props.nameIsEnumerable
this.messageIsEnumerable = props.messageIsEnumerable
this.message = props.message
}
createPropertyRecursor () {
const recursor = super.createPropertyRecursor()
let skipName = this.nameIsEnumerable
let emitMessage = !this.messageIsEnumerable
let size = recursor.size
if (skipName && size > 0) {
size -= 1
}
if (emitMessage) {
size += 1
}
if (size === 0) return NOOP_RECURSOR
let done = false
const next = () => {
if (done) return null
const property = recursor.next()
if (property) {
if (skipName && property.key.value === 'name') {
skipName = false
return next()
}
return property
}
if (emitMessage) {
emitMessage = false
return this.describeProperty('message', this.describeAny(this.message))
}
done = true
return null
}
return { size, next }
}
}
class DeserializedErrorValue extends object.DeserializedMixin(ErrorValue) {
constructor (state, recursor) {
super(state[1], recursor)
this.name = state[0]
}
}

View file

@ -0,0 +1,122 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const isEnumerable = require('../isEnumerable')
const lineBuilder = require('../lineBuilder')
const NOOP_RECURSOR = require('../recursorUtils').NOOP_RECURSOR
const object = require('./object')
const UNEQUAL = constants.UNEQUAL
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
function describe (props) {
const fn = props.value
return new DescribedFunctionValue(Object.assign({
nameIsEnumerable: isEnumerable(fn, 'name'),
name: typeof fn.name === 'string' ? fn.name : null,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedFunctionValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('FunctionValue')
exports.tag = tag
class FunctionValue extends object.ObjectValue {
constructor (props) {
super(props)
this.name = props.name
}
formatShallow (theme, indent) {
const string = formatUtils.wrap(theme.function.stringTag, this.stringTag) +
(this.name ? ' ' + formatUtils.wrap(theme.function.name, this.name) : '') +
' ' + theme.object.openBracket
return super.formatShallow(theme, indent).customize({
finalize (innerLines) {
return innerLines.isEmpty
? lineBuilder.single(string + theme.object.closeBracket)
: lineBuilder.first(string)
.concat(innerLines.withFirstPrefixed(indent.increase()).stripFlags())
.append(lineBuilder.last(indent + theme.object.closeBracket))
},
maxDepth () {
return lineBuilder.single(string + ' ' + theme.maxDepth + ' ' + theme.object.closeBracket)
},
})
}
}
Object.defineProperty(FunctionValue.prototype, 'tag', { value: tag })
class DescribedFunctionValue extends object.DescribedMixin(FunctionValue) {
constructor (props) {
super(props)
this.nameIsEnumerable = props.nameIsEnumerable
}
compare (expected) {
if (this.tag !== expected.tag) return UNEQUAL
if (this.name !== expected.name) return UNEQUAL
if (this.value && expected.value && this.value !== expected.value) return UNEQUAL
return super.compare(expected)
}
createPropertyRecursor () {
const recursor = super.createPropertyRecursor()
const skipName = this.nameIsEnumerable
if (!skipName) return recursor
let size = recursor.size
if (skipName) {
size -= 1
}
if (size === 0) return NOOP_RECURSOR
const next = () => {
const property = recursor.next()
if (property) {
if (skipName && property.key.value === 'name') {
return next()
}
return property
}
return null
}
return { size, next }
}
serialize () {
return [this.name, super.serialize()]
}
}
class DeserializedFunctionValue extends object.DeserializedMixin(FunctionValue) {
constructor (state, recursor) {
super(state[1], recursor)
this.name = state[0]
}
compare (expected) {
if (this.tag !== expected.tag) return UNEQUAL
if (this.name !== expected.name) return UNEQUAL
if (this.stringTag !== expected.stringTag) return UNEQUAL
return SHALLOW_EQUAL
}
serialize () {
return [this.name, super.serialize()]
}
}

View file

@ -0,0 +1,33 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe () {
return new GlobalValue()
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('GlobalValue')
exports.tag = tag
class GlobalValue {
compare (expected) {
return this.tag === expected.tag
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
return lineBuilder.single(
formatUtils.wrap(theme.global, 'Global') + ' ' + theme.object.openBracket + theme.object.closeBracket)
}
}
Object.defineProperty(GlobalValue.prototype, 'isComplex', { value: true })
Object.defineProperty(GlobalValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,78 @@
'use strict'
const constants = require('../constants')
const recursorUtils = require('../recursorUtils')
const object = require('./object')
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (props) {
return new DescribedMapValue(Object.assign({
size: props.value.size,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedMapValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('MapValue')
exports.tag = tag
class MapValue extends object.ObjectValue {
constructor (props) {
super(props)
this.size = props.size
}
compare (expected) {
const result = super.compare(expected)
if (result !== SHALLOW_EQUAL) return result
return this.size === expected.size
? SHALLOW_EQUAL
: UNEQUAL
}
prepareDiff (expected) {
// Maps should be compared, even if they have a different number of entries.
return { compareResult: super.compare(expected) }
}
serialize () {
return [this.size, super.serialize()]
}
}
Object.defineProperty(MapValue.prototype, 'tag', { value: tag })
class DescribedMapValue extends object.DescribedMixin(MapValue) {
createIterableRecursor () {
const size = this.size
if (size === 0) return recursorUtils.NOOP_RECURSOR
let index = 0
let entries
const next = () => {
if (index === size) return null
if (!entries) {
entries = Array.from(this.value)
}
const entry = entries[index++]
return this.describeMapEntry(this.describeAny(entry[0]), this.describeAny(entry[1]))
}
return { size, next }
}
}
class DeserializedMapValue extends object.DeserializedMixin(MapValue) {
constructor (state, recursor) {
super(state[1], recursor)
this.size = state[0]
}
}

View file

@ -0,0 +1,254 @@
'use strict'
const constants = require('../constants')
const ObjectFormatter = require('../formatUtils').ObjectFormatter
const getObjectKeys = require('../getObjectKeys')
const hasLength = require('../hasLength')
const stats = require('../metaDescriptors/stats')
const recursorUtils = require('../recursorUtils')
const DEEP_EQUAL = constants.DEEP_EQUAL
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (props) {
const isArray = props.stringTag === 'Array'
const object = props.value
return new DescribedObjectValue(Object.assign({
isArray,
isIterable: object[Symbol.iterator] !== undefined,
isList: isArray || hasLength(object),
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedObjectValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('ObjectValue')
exports.tag = tag
class ObjectValue {
constructor (props) {
this.ctor = props.ctor
this.pointer = props.pointer
this.stringTag = props.stringTag
this.isArray = props.isArray === true
this.isIterable = props.isIterable === true
this.isList = props.isList === true
}
compare (expected) {
if (this.tag !== expected.tag) return UNEQUAL
if (this.stringTag !== expected.stringTag || !this.hasSameCtor(expected)) return UNEQUAL
return SHALLOW_EQUAL
}
hasSameCtor (expected) {
return this.ctor === expected.ctor
}
formatShallow (theme, indent) {
return new ObjectFormatter(this, theme, indent)
}
serialize () {
return [
this.ctor, this.pointer, this.stringTag,
this.isArray, this.isIterable, this.isList,
]
}
}
Object.defineProperty(ObjectValue.prototype, 'isComplex', { value: true })
Object.defineProperty(ObjectValue.prototype, 'tag', { value: tag })
exports.ObjectValue = ObjectValue
const DescribedObjectValue = DescribedMixin(ObjectValue)
const DeserializedObjectValue = DeserializedMixin(ObjectValue)
function DescribedMixin (base) {
return class extends base {
constructor (props) {
super(props)
this.value = props.value
this.describeAny = props.describeAny
this.describeItem = props.describeItem
this.describeMapEntry = props.describeMapEntry
this.describeProperty = props.describeProperty
this.iterableState = null
this.listState = null
this.propertyState = null
}
compare (expected) {
return this.value === expected.value
? DEEP_EQUAL
: super.compare(expected)
}
createPropertyRecursor () {
const objectKeys = getObjectKeys(this.value, this.isList ? this.value.length : 0)
const size = objectKeys.size
if (size === 0) return recursorUtils.NOOP_RECURSOR
let index = 0
const next = () => {
if (index === size) return null
const key = objectKeys.keys[index++]
return this.describeProperty(key, this.describeAny(this.value[key]))
}
return { size, next }
}
createListRecursor () {
if (!this.isList) return recursorUtils.NOOP_RECURSOR
const size = this.value.length
if (size === 0) return recursorUtils.NOOP_RECURSOR
let index = 0
const next = () => {
if (index === size) return null
const current = index
index++
return this.describeItem(current, this.describeAny(this.value[current]))
}
return { size, next }
}
createIterableRecursor () {
if (this.isArray || !this.isIterable) return recursorUtils.NOOP_RECURSOR
const iterator = this.value[Symbol.iterator]()
let first = iterator.next()
let done = false
let size = -1
if (first.done) {
if (first.value === undefined) {
size = 0
done = true
} else {
size = 1
}
}
let index = 0
const next = () => {
if (done) return null
while (!done) {
const current = first || iterator.next()
if (current === first) {
first = null
}
if (current.done) {
done = true
}
const item = current.value
if (done && item === undefined) return null
if (this.isList && this.value[index] === item) {
index++
} else {
return this.describeItem(index++, this.describeAny(item))
}
}
}
return { size, next }
}
createRecursor () {
let recursedProperty = false
let recursedList = false
let recursedIterable = false
let recursor = null
return () => {
let retval = null
do {
if (recursor !== null) {
retval = recursor.next()
if (retval === null) {
recursor = null
}
}
while (recursor === null && (!recursedList || !recursedProperty || !recursedIterable)) {
// Prioritize recursing lists
if (!recursedList) {
const replay = recursorUtils.replay(this.listState, () => this.createListRecursor())
this.listState = replay.state
recursor = replay.recursor
recursedList = true
if (recursor !== recursorUtils.NOOP_RECURSOR) {
retval = stats.describeListRecursor(recursor)
}
} else if (!recursedProperty) {
const replay = recursorUtils.replay(this.propertyState, () => this.createPropertyRecursor())
this.propertyState = replay.state
recursor = replay.recursor
recursedProperty = true
if (recursor !== recursorUtils.NOOP_RECURSOR) {
retval = stats.describePropertyRecursor(recursor)
}
} else if (!recursedIterable) {
const replay = recursorUtils.replay(this.iterableState, () => this.createIterableRecursor())
this.iterableState = replay.state
recursor = replay.recursor
recursedIterable = true
if (recursor !== recursorUtils.NOOP_RECURSOR) {
retval = stats.describeIterableRecursor(recursor)
}
}
}
} while (recursor !== null && retval === null)
return retval
}
}
}
}
exports.DescribedMixin = DescribedMixin
function DeserializedMixin (base) {
return class extends base {
constructor (state, recursor) {
super({
ctor: state[0],
pointer: state[1],
stringTag: state[2],
isArray: state[3],
isIterable: state[4],
isList: state[5],
})
this.deserializedRecursor = recursor
this.replayState = null
}
createRecursor () {
if (!this.deserializedRecursor) return () => null
const replay = recursorUtils.replay(this.replayState, () => ({ size: -1, next: this.deserializedRecursor }))
this.replayState = replay.state
return replay.recursor.next
}
hasSameCtor (expected) {
return this.ctor === expected.ctor
}
}
}
exports.DeserializedMixin = DeserializedMixin

View file

@ -0,0 +1,40 @@
'use strict'
const constants = require('../constants')
const object = require('./object')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (props) {
return new DescribedPromiseValue(props)
}
exports.describe = describe
function deserialize (props) {
return new DeserializedPromiseValue(props)
}
exports.deserialize = deserialize
const tag = Symbol('PromiseValue')
exports.tag = tag
class PromiseValue extends object.ObjectValue {}
Object.defineProperty(PromiseValue.prototype, 'tag', { value: tag })
class DescribedPromiseValue extends object.DescribedMixin(PromiseValue) {
compare (expected) {
// When comparing described promises, require them to be the exact same
// object.
return super.compare(expected) === DEEP_EQUAL
? DEEP_EQUAL
: UNEQUAL
}
}
class DeserializedPromiseValue extends object.DeserializedMixin(PromiseValue) {
compare (expected) {
// Deserialized promises can never be compared using object references.
return super.compare(expected)
}
}

View file

@ -0,0 +1,90 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const object = require('./object')
const UNEQUAL = constants.UNEQUAL
function describe (props) {
const regexp = props.value
return new DescribedRegexpValue(Object.assign({
flags: getSortedFlags(regexp),
source: regexp.source,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedRegexpValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('RegexpValue')
exports.tag = tag
function getSortedFlags (regexp) {
const flags = regexp.flags || String(regexp).slice(regexp.source.length + 2)
return flags.split('').sort().join('')
}
class RegexpValue extends object.ObjectValue {
constructor (props) {
super(props)
this.flags = props.flags
this.source = props.source
}
compare (expected) {
return this.tag === expected.tag && this.flags === expected.flags && this.source === expected.source
? super.compare(expected)
: UNEQUAL
}
formatShallow (theme, indent) {
const ctor = this.ctor || this.stringTag
const regexp = formatUtils.wrap(theme.regexp.source, this.source) + formatUtils.wrap(theme.regexp.flags, this.flags)
return super.formatShallow(theme, indent).customize({
finalize: innerLines => {
if (ctor === 'RegExp' && innerLines.isEmpty) return lineBuilder.single(regexp)
const innerIndentation = indent.increase()
const header = lineBuilder.first(formatUtils.formatCtorAndStringTag(theme, this) + ' ' + theme.object.openBracket)
.concat(lineBuilder.line(innerIndentation + regexp))
if (!innerLines.isEmpty) {
header.append(lineBuilder.line(innerIndentation + theme.regexp.separator))
header.append(innerLines.withFirstPrefixed(innerIndentation).stripFlags())
}
return header.append(lineBuilder.last(indent + theme.object.closeBracket))
},
maxDepth: () => {
return lineBuilder.single(
formatUtils.formatCtorAndStringTag(theme, this) + ' ' +
theme.object.openBracket + ' ' +
regexp + ' ' +
theme.maxDepth + ' ' +
theme.object.closeBracket)
},
})
}
serialize () {
return [this.flags, this.source, super.serialize()]
}
}
Object.defineProperty(RegexpValue.prototype, 'tag', { value: tag })
const DescribedRegexpValue = object.DescribedMixin(RegexpValue)
class DeserializedRegexpValue extends object.DeserializedMixin(RegexpValue) {
constructor (state, recursor) {
super(state[2], recursor)
this.flags = state[0]
this.source = state[1]
}
}

View file

@ -0,0 +1,78 @@
'use strict'
const constants = require('../constants')
const recursorUtils = require('../recursorUtils')
const object = require('./object')
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (props) {
return new DescribedSetValue(Object.assign({
size: props.value.size,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedSetValue(state, recursor)
}
exports.deserialize = deserialize
const tag = Symbol('SetValue')
exports.tag = tag
class SetValue extends object.ObjectValue {
constructor (props) {
super(props)
this.size = props.size
}
compare (expected) {
const result = super.compare(expected)
if (result !== SHALLOW_EQUAL) return result
return this.size === expected.size
? SHALLOW_EQUAL
: UNEQUAL
}
prepareDiff (expected) {
// Sets should be compared, even if they have a different number of items.
return { compareResult: super.compare(expected) }
}
serialize () {
return [this.size, super.serialize()]
}
}
Object.defineProperty(SetValue.prototype, 'tag', { value: tag })
class DescribedSetValue extends object.DescribedMixin(SetValue) {
createIterableRecursor () {
const size = this.size
if (size === 0) return recursorUtils.NOOP_RECURSOR
let index = 0
let members
const next = () => {
if (index === size) return null
if (!members) {
members = Array.from(this.value)
}
const value = members[index]
return this.describeItem(index++, this.describeAny(value))
}
return { size, next }
}
}
class DeserializedSetValue extends object.DeserializedMixin(SetValue) {
constructor (state, recursor) {
super(state[1], recursor)
this.size = state[0]
}
}

View file

@ -0,0 +1,161 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const propertyStatsTag = require('../metaDescriptors/stats').propertyTag
const recursorUtils = require('../recursorUtils')
const object = require('./object')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function getBuffer (value) {
const buffer = Buffer.from(value.buffer)
return value.byteLength !== value.buffer.byteLength
? buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)
: buffer
}
exports.getBuffer = getBuffer
function describe (props) {
return new DescribedTypedArrayValue(Object.assign({
buffer: getBuffer(props.value),
// Set isArray and isList so the property recursor excludes the byte accessors
isArray: true,
isList: true,
}, props))
}
exports.describe = describe
function deserialize (state, recursor) {
return new DeserializedTypedArrayValue(state, recursor)
}
exports.deserialize = deserialize
function deserializeBytes (buffer) {
return new Bytes(buffer)
}
exports.deserializeBytes = deserializeBytes
const bytesTag = Symbol('Bytes')
exports.bytesTag = bytesTag
const tag = Symbol('TypedArrayValue')
exports.tag = tag
class Bytes {
constructor (buffer) {
this.buffer = buffer
}
compare (expected) {
return expected.tag === bytesTag && this.buffer.equals(expected.buffer)
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme, indent) {
const indentation = indent
const lines = lineBuilder.buffer()
// Display 4-byte words, 8 per line
let string = ''
let isFirst = true
for (let offset = 0; offset < this.buffer.length; offset += 4) {
if (offset > 0) {
if (offset % 32 === 0) {
if (isFirst) {
lines.append(lineBuilder.first(string))
isFirst = false
} else {
lines.append(lineBuilder.line(string))
}
string = String(indentation)
} else {
string += ' '
}
}
string += formatUtils.wrap(theme.typedArray.bytes, this.buffer.toString('hex', offset, offset + 4))
}
return isFirst
? lineBuilder.single(string)
: lines.append(lineBuilder.last(string))
}
serialize () {
return this.buffer
}
}
Object.defineProperty(Bytes.prototype, 'tag', { value: bytesTag })
class TypedArrayValue extends object.ObjectValue {
constructor (props) {
super(props)
this.buffer = props.buffer
}
formatShallow (theme, indent) {
return super.formatShallow(theme, indent).customize({
shouldFormat (subject) {
if (subject.tag === propertyStatsTag) return subject.size > 1
if (subject.isProperty === true) return subject.key.value !== 'byteLength'
if (subject.tag === bytesTag) return subject.buffer.byteLength > 0
return true
},
})
}
}
Object.defineProperty(TypedArrayValue.prototype, 'tag', { value: tag })
exports.TypedArrayValue = TypedArrayValue
function DescribedMixin (base) {
return class extends object.DescribedMixin(base) {
// The list isn't recursed. Instead a Bytes instance is returned by the main
// recursor.
createListRecursor () {
return recursorUtils.NOOP_RECURSOR
}
createPropertyRecursor () {
const recursor = super.createPropertyRecursor()
const size = recursor.size + 1
let done = false
const next = () => {
if (done) return null
const property = recursor.next()
if (property) return property
done = true
return this.describeProperty('byteLength', this.describeAny(this.buffer.byteLength))
}
return { size, next }
}
createRecursor () {
return recursorUtils.unshift(super.createRecursor(), new Bytes(this.buffer))
}
}
}
exports.DescribedMixin = DescribedMixin
const DescribedTypedArrayValue = DescribedMixin(TypedArrayValue)
function DeserializedMixin (base) {
return class extends object.DeserializedMixin(base) {
constructor (state, recursor) {
super(state, recursor)
// Get the Bytes descriptor from the recursor. It contains the buffer.
const bytesDescriptor = this.createRecursor()()
this.buffer = bytesDescriptor.buffer
}
}
}
exports.DeserializedMixin = DeserializedMixin
const DeserializedTypedArrayValue = DeserializedMixin(TypedArrayValue)

View file

@ -0,0 +1,13 @@
'use strict'
const AMBIGUOUS = Symbol('AMBIGUOUS')
const DEEP_EQUAL = Symbol('DEEP_EQUAL')
const SHALLOW_EQUAL = Symbol('SHALLOW_EQUAL')
const UNEQUAL = Symbol('UNEQUAL')
module.exports = {
AMBIGUOUS,
DEEP_EQUAL,
SHALLOW_EQUAL,
UNEQUAL,
}

View file

@ -0,0 +1,171 @@
'use strict'
const Registry = require('./Registry')
const argumentsValue = require('./complexValues/arguments')
const arrayBufferValue = require('./complexValues/arrayBuffer')
const boxedValue = require('./complexValues/boxed')
const dataViewValue = require('./complexValues/dataView')
const dateValue = require('./complexValues/date')
const errorValue = require('./complexValues/error')
const functionValue = require('./complexValues/function')
const globalValue = require('./complexValues/global')
const mapValue = require('./complexValues/map')
const objectValue = require('./complexValues/object')
const promiseValue = require('./complexValues/promise')
const regexpValue = require('./complexValues/regexp')
const setValue = require('./complexValues/set')
const typedArrayValue = require('./complexValues/typedArray')
const getCtor = require('./getCtor')
const getStringTag = require('./getStringTag')
const itemDescriptor = require('./metaDescriptors/item')
const mapEntryDescriptor = require('./metaDescriptors/mapEntry')
const propertyDescriptor = require('./metaDescriptors/property')
const pluginRegistry = require('./pluginRegistry')
const bigIntValue = require('./primitiveValues/bigInt')
const booleanValue = require('./primitiveValues/boolean')
const nullValue = require('./primitiveValues/null')
const numberValue = require('./primitiveValues/number')
const stringValue = require('./primitiveValues/string')
const symbolValue = require('./primitiveValues/symbol')
const undefinedValue = require('./primitiveValues/undefined')
const SpecializedComplexes = new Map([
['Arguments', argumentsValue.describe],
['ArrayBuffer', arrayBufferValue.describe],
['DataView', dataViewValue.describe],
['Date', dateValue.describe],
['Error', errorValue.describe],
['Float32Array', typedArrayValue.describe],
['Float64Array', typedArrayValue.describe],
['Function', functionValue.describe],
['GeneratorFunction', functionValue.describe],
['global', globalValue.describe],
['Int16Array', typedArrayValue.describe],
['Int32Array', typedArrayValue.describe],
['Int8Array', typedArrayValue.describe],
['Map', mapValue.describe],
['Promise', promiseValue.describe],
['RegExp', regexpValue.describe],
['Set', setValue.describe],
['Uint16Array', typedArrayValue.describe],
['Uint32Array', typedArrayValue.describe],
['Uint8Array', typedArrayValue.describe],
['Uint8ClampedArray', typedArrayValue.describe],
])
function describePrimitive (value) {
if (value === null) return nullValue.describe()
if (value === undefined) return undefinedValue.describe()
if (value === true || value === false) return booleanValue.describe(value)
const type = typeof value
if (type === 'bigint') return bigIntValue.describe(value)
if (type === 'number') return numberValue.describe(value)
if (type === 'string') return stringValue.describe(value)
if (type === 'symbol') return symbolValue.describe(value)
return null
}
function unboxComplex (tag, complex) {
// Try to unbox by calling `valueOf()`. `describePrimitive()` will return
// `null` if the resulting value is not a primitive, in which case it's
// ignored.
if (typeof complex.valueOf === 'function') {
const value = complex.valueOf()
if (value !== complex) return describePrimitive(value)
}
return null
}
function registerPlugins (plugins) {
if (!Array.isArray(plugins) || plugins.length === 0) return () => null
const tryFns = pluginRegistry.getTryDescribeValues(plugins)
return (value, stringTag, ctor) => {
for (const tryDescribeValue of tryFns) {
const describeValue = tryDescribeValue(value, stringTag, ctor)
if (describeValue) return describeValue
}
return null
}
}
function describeComplex (value, registry, tryPlugins, describeAny, describeItem, describeMapEntry, describeProperty) {
if (registry.has(value)) return registry.get(value)
const stringTag = getStringTag(value)
const ctor = getCtor(stringTag, value)
const pointer = registry.alloc(value)
let unboxed
let describeValue = tryPlugins(value, stringTag, ctor)
if (describeValue === null) {
if (SpecializedComplexes.has(stringTag)) {
describeValue = SpecializedComplexes.get(stringTag)
} else {
unboxed = unboxComplex(stringTag, value)
if (unboxed !== null) {
describeValue = boxedValue.describe
} else {
describeValue = objectValue.describe
}
}
}
const descriptor = describeValue({
ctor,
describeAny,
describeItem,
describeMapEntry,
describeProperty,
pointer: pointer.index,
stringTag,
unboxed,
value,
})
pointer.descriptor = descriptor
return descriptor
}
const describeItem = (index, valueDescriptor) => {
return valueDescriptor.isPrimitive === true
? itemDescriptor.describePrimitive(index, valueDescriptor)
: itemDescriptor.describeComplex(index, valueDescriptor)
}
const describeMapEntry = (keyDescriptor, valueDescriptor) => {
return mapEntryDescriptor.describe(keyDescriptor, valueDescriptor)
}
function describe (value, options) {
const primitive = describePrimitive(value)
if (primitive !== null) return primitive
const registry = new Registry()
const tryPlugins = registerPlugins(options && options.plugins)
const curriedComplex = c => {
return describeComplex(c, registry, tryPlugins, describeAny, describeItem, describeMapEntry, describeProperty)
}
const describeAny = any => {
const descriptor = describePrimitive(any)
return descriptor !== null
? descriptor
: curriedComplex(any)
}
const describeProperty = (key, valueDescriptor) => {
const keyDescriptor = describePrimitive(key)
return valueDescriptor.isPrimitive === true
? propertyDescriptor.describePrimitive(keyDescriptor, valueDescriptor)
: propertyDescriptor.describeComplex(keyDescriptor, valueDescriptor)
}
return curriedComplex(value)
}
module.exports = describe

View file

@ -0,0 +1,391 @@
'use strict'
const Circular = require('./Circular')
const Indenter = require('./Indenter')
const constants = require('./constants')
const describe = require('./describe')
const lineBuilder = require('./lineBuilder')
const recursorUtils = require('./recursorUtils')
const shouldCompareDeep = require('./shouldCompareDeep')
const symbolProperties = require('./symbolProperties')
const themeUtils = require('./themeUtils')
const AMBIGUOUS = constants.AMBIGUOUS
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const NOOP = Symbol('NOOP')
const alwaysFormat = () => true
function compareComplexShape (lhs, rhs) {
let result = lhs.compare(rhs)
if (result === DEEP_EQUAL) return DEEP_EQUAL
if (result === UNEQUAL || !shouldCompareDeep(result, lhs, rhs)) return UNEQUAL
let collectedSymbolProperties = false
let lhsRecursor = lhs.createRecursor()
let rhsRecursor = rhs.createRecursor()
do {
lhs = lhsRecursor()
rhs = rhsRecursor()
if (lhs === null && rhs === null) return SHALLOW_EQUAL
if (lhs === null || rhs === null) return UNEQUAL
result = lhs.compare(rhs)
if (result === UNEQUAL) return UNEQUAL
if (
result === AMBIGUOUS &&
lhs.isProperty === true && !collectedSymbolProperties &&
shouldCompareDeep(result, lhs, rhs)
) {
collectedSymbolProperties = true
const lhsCollector = new symbolProperties.Collector(lhs, lhsRecursor)
const rhsCollector = new symbolProperties.Collector(rhs, rhsRecursor)
lhsRecursor = recursorUtils.sequence(
lhsCollector.createRecursor(),
recursorUtils.unshift(lhsRecursor, lhsCollector.collectAll()))
rhsRecursor = recursorUtils.sequence(
rhsCollector.createRecursor(),
recursorUtils.unshift(rhsRecursor, rhsCollector.collectAll()))
}
} while (true)
}
function diffDescriptors (lhs, rhs, options) {
const theme = themeUtils.normalize(options)
const invert = options ? options.invert === true : false
const lhsCircular = new Circular()
const rhsCircular = new Circular()
const maxDepth = (options && options.maxDepth) || 0
let indent = new Indenter(0, ' ')
const lhsStack = []
const rhsStack = []
let topIndex = -1
const buffer = lineBuilder.buffer()
const diffStack = []
let diffIndex = -1
const isCircular = descriptor => lhsCircular.has(descriptor) || rhsCircular.has(descriptor)
const format = (builder, subject, circular, depthOffset = 0) => {
if (diffIndex >= 0 && !diffStack[diffIndex].shouldFormat(subject)) return
if (circular.has(subject)) {
diffStack[diffIndex].formatter.append(builder.single(theme.circular))
return
}
const formatStack = []
let formatIndex = -1
do {
if (circular.has(subject)) {
formatStack[formatIndex].formatter.append(builder.single(theme.circular), subject)
} else {
let didFormat = false
if (typeof subject.formatDeep === 'function') {
let formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), indent)
if (formatted !== null) {
didFormat = true
if (formatIndex === -1) {
formatted = builder.setDefaultGutter(formatted)
if (diffIndex === -1) {
buffer.append(formatted)
} else {
diffStack[diffIndex].formatter.append(formatted, subject)
}
} else {
formatStack[formatIndex].formatter.append(formatted, subject)
}
}
}
if (!didFormat && typeof subject.formatShallow === 'function') {
const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent)
const recursor = subject.createRecursor()
if (formatter.increaseIndent && maxDepth > 0 && indent.level === (maxDepth + depthOffset)) {
const isEmpty = recursor() === null
let formatted = !isEmpty && typeof formatter.maxDepth === 'function'
? formatter.maxDepth()
: formatter.finalize()
if (formatIndex === -1) {
formatted = builder.setDefaultGutter(formatted)
diffStack[diffIndex].formatter.append(formatted, subject)
} else {
formatStack[formatIndex].formatter.append(formatted, subject)
}
} else {
formatStack.push({
formatter,
recursor,
decreaseIndent: formatter.increaseIndent,
shouldFormat: formatter.shouldFormat || alwaysFormat,
subject,
})
formatIndex++
if (formatter.increaseIndent) indent = indent.increase()
circular.add(subject)
}
}
}
while (formatIndex >= 0) {
do {
subject = formatStack[formatIndex].recursor()
} while (subject && !formatStack[formatIndex].shouldFormat(subject))
if (subject) {
break
}
const record = formatStack.pop()
formatIndex--
if (record.decreaseIndent) indent = indent.decrease()
circular.delete(record.subject)
let formatted = record.formatter.finalize()
if (formatIndex === -1) {
formatted = builder.setDefaultGutter(formatted)
if (diffIndex === -1) {
buffer.append(formatted)
} else {
diffStack[diffIndex].formatter.append(formatted, record.subject)
}
} else {
formatStack[formatIndex].formatter.append(formatted, record.subject)
}
}
} while (formatIndex >= 0)
}
do {
let compareResult = NOOP
if (lhsCircular.has(lhs)) {
compareResult = lhsCircular.get(lhs) === rhsCircular.get(rhs)
? DEEP_EQUAL
: UNEQUAL
} else if (rhsCircular.has(rhs)) {
compareResult = UNEQUAL
}
let firstPassSymbolProperty = false
if (lhs.isProperty === true) {
compareResult = lhs.compare(rhs)
if (compareResult === AMBIGUOUS) {
const parent = lhsStack[topIndex].subject
firstPassSymbolProperty = parent.isSymbolPropertiesCollector !== true && parent.isSymbolPropertiesComparable !== true
}
}
let didFormat = false
let mustRecurse = false
if (compareResult !== DEEP_EQUAL && !firstPassSymbolProperty && typeof lhs.prepareDiff === 'function') {
const lhsRecursor = topIndex === -1 ? null : lhsStack[topIndex].recursor
const rhsRecursor = topIndex === -1 ? null : rhsStack[topIndex].recursor
const instructions = lhs.prepareDiff(
rhs,
lhsRecursor,
rhsRecursor,
compareComplexShape,
isCircular)
if (instructions !== null) {
if (topIndex >= 0) {
if (typeof instructions.lhsRecursor === 'function') {
lhsStack[topIndex].recursor = instructions.lhsRecursor
}
if (typeof instructions.rhsRecursor === 'function') {
rhsStack[topIndex].recursor = instructions.rhsRecursor
}
}
if (instructions.compareResult) {
compareResult = instructions.compareResult
}
if (instructions.mustRecurse === true) {
mustRecurse = true
} else {
if (instructions.actualIsExtraneous === true) {
format(lineBuilder.actual, lhs, lhsCircular)
didFormat = true
} else if (instructions.multipleAreExtraneous === true) {
for (const extraneous of instructions.descriptors) {
format(lineBuilder.actual, extraneous, lhsCircular)
}
didFormat = true
} else if (instructions.expectedIsMissing === true) {
format(lineBuilder.expected, rhs, rhsCircular)
didFormat = true
} else if (instructions.multipleAreMissing === true) {
for (const missing of instructions.descriptors) {
format(lineBuilder.expected, missing, rhsCircular)
}
didFormat = true
} else if (instructions.isUnequal === true) {
format(lineBuilder.actual, lhs, lhsCircular)
format(lineBuilder.expected, rhs, rhsCircular)
didFormat = true
} else if (!instructions.compareResult) {
// TODO: Throw a useful, custom error
throw new Error('Illegal result of prepareDiff()')
}
}
}
}
if (!didFormat) {
if (compareResult === NOOP) {
compareResult = lhs.compare(rhs)
}
if (!mustRecurse) {
mustRecurse = shouldCompareDeep(compareResult, lhs, rhs)
}
if (compareResult === DEEP_EQUAL) {
format(lineBuilder, lhs, lhsCircular)
} else if (mustRecurse) {
if (compareResult === AMBIGUOUS && lhs.isProperty === true) {
// Replace both sides by a pseudo-descriptor which collects symbol
// properties instead.
lhs = new symbolProperties.Collector(lhs, lhsStack[topIndex].recursor)
rhs = new symbolProperties.Collector(rhs, rhsStack[topIndex].recursor)
// Replace the current recursors so they can continue correctly after
// the collectors have been "compared". This is necessary since the
// collectors eat the first value after the last symbol property.
lhsStack[topIndex].recursor = recursorUtils.unshift(lhsStack[topIndex].recursor, lhs.collectAll())
rhsStack[topIndex].recursor = recursorUtils.unshift(rhsStack[topIndex].recursor, rhs.collectAll())
}
if (typeof lhs.diffShallow === 'function') {
const formatter = lhs.diffShallow(rhs, themeUtils.applyModifiers(lhs, theme), indent)
diffStack.push({
formatter,
origin: lhs,
decreaseIndent: formatter.increaseIndent,
exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth,
shouldFormat: formatter.shouldFormat || alwaysFormat,
})
diffIndex++
if (formatter.increaseIndent) indent = indent.increase()
} else if (typeof lhs.formatShallow === 'function') {
const formatter = lhs.formatShallow(themeUtils.applyModifiers(lhs, theme), indent)
diffStack.push({
formatter,
decreaseIndent: formatter.increaseIndent,
exceedsMaxDepth: formatter.increaseIndent && maxDepth > 0 && indent.level >= maxDepth,
shouldFormat: formatter.shouldFormat || alwaysFormat,
subject: lhs,
})
diffIndex++
if (formatter.increaseIndent) indent = indent.increase()
}
lhsCircular.add(lhs)
rhsCircular.add(rhs)
lhsStack.push({ diffIndex, subject: lhs, recursor: lhs.createRecursor() })
rhsStack.push({ diffIndex, subject: rhs, recursor: rhs.createRecursor() })
topIndex++
} else {
const diffed = typeof lhs.diffDeep === 'function'
? lhs.diffDeep(rhs, themeUtils.applyModifiers(lhs, theme), indent, invert)
: null
if (diffed === null) {
format(lineBuilder.actual, lhs, lhsCircular)
format(lineBuilder.expected, rhs, rhsCircular)
} else {
if (diffIndex === -1) {
buffer.append(diffed)
} else {
diffStack[diffIndex].formatter.append(diffed, lhs)
}
}
}
}
while (topIndex >= 0) {
lhs = lhsStack[topIndex].recursor()
rhs = rhsStack[topIndex].recursor()
if (lhs !== null && rhs !== null) {
break
}
if (lhs === null && rhs === null) {
const lhsRecord = lhsStack.pop()
const rhsRecord = rhsStack.pop()
lhsCircular.delete(lhsRecord.subject)
rhsCircular.delete(rhsRecord.subject)
topIndex--
if (lhsRecord.diffIndex === diffIndex) {
const record = diffStack.pop()
diffIndex--
if (record.decreaseIndent) indent = indent.decrease()
let formatted = record.formatter.finalize()
if (record.exceedsMaxDepth && !formatted.hasGutter) {
// The record exceeds the max depth, but contains no actual diff.
// Discard the potentially deep formatting and format just the
// original subject.
const subject = lhsRecord.subject
const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent)
const isEmpty = subject.createRecursor()() === null
formatted = !isEmpty && typeof formatter.maxDepth === 'function'
? formatter.maxDepth()
: formatter.finalize()
}
if (diffIndex === -1) {
buffer.append(formatted)
} else {
diffStack[diffIndex].formatter.append(formatted, record.subject)
}
}
} else {
let builder, circular, stack, subject
if (lhs === null) {
builder = lineBuilder.expected
circular = rhsCircular
stack = rhsStack
subject = rhs
} else {
builder = lineBuilder.actual
circular = lhsCircular
stack = lhsStack
subject = lhs
}
do {
format(builder, subject, circular, indent.level)
subject = stack[topIndex].recursor()
} while (subject !== null)
}
}
} while (topIndex >= 0)
return buffer.toString({ diff: true, invert, theme })
}
exports.diffDescriptors = diffDescriptors
function diff (actual, expected, options) {
return diffDescriptors(describe(actual, options), describe(expected, options), options)
}
exports.diff = diff

View file

@ -0,0 +1,303 @@
'use strict'
const flattenDeep = require('lodash/flattenDeep')
// Indexes are hexadecimal to make reading the binary output easier.
const valueTypes = {
zero: 0x00,
int8: 0x01, // Note that the hex value equals the number of bytes required
int16: 0x02, // to store the integer.
int24: 0x03,
int32: 0x04,
int40: 0x05,
int48: 0x06,
numberString: 0x07,
negativeZero: 0x08,
notANumber: 0x09,
infinity: 0x0A,
negativeInfinity: 0x0B,
bigInt: 0x0C,
undefined: 0x0D,
null: 0x0E,
true: 0x0F,
false: 0x10,
utf8: 0x11,
bytes: 0x12,
list: 0x13,
descriptor: 0x14,
}
const descriptorSymbol = Symbol('descriptor')
exports.descriptorSymbol = descriptorSymbol
function encodeInteger (type, value) {
const encoded = Buffer.alloc(type)
encoded.writeIntLE(value, 0, type)
return [type, encoded]
}
function encodeValue (value) {
if (Object.is(value, 0)) return valueTypes.zero
if (Object.is(value, -0)) return valueTypes.negativeZero
if (Object.is(value, NaN)) return valueTypes.notANumber
if (value === Infinity) return valueTypes.infinity
if (value === -Infinity) return valueTypes.negativeInfinity
if (value === undefined) return valueTypes.undefined
if (value === null) return valueTypes.null
if (value === true) return valueTypes.true
if (value === false) return valueTypes.false
const type = typeof value
if (type === 'number') {
if (Number.isInteger(value)) {
// The integer types are signed, so int8 can only store 7 bits, int16
// only 15, etc.
if (value >= -0x80 && value < 0x80) return encodeInteger(valueTypes.int8, value)
if (value >= -0x8000 && value < 0x8000) return encodeInteger(valueTypes.int16, value)
if (value >= -0x800000 && value < 0x800000) return encodeInteger(valueTypes.int24, value)
if (value >= -0x80000000 && value < 0x80000000) return encodeInteger(valueTypes.int32, value)
if (value >= -0x8000000000 && value < 0x8000000000) return encodeInteger(valueTypes.int40, value)
if (value >= -0x800000000000 && value < 0x800000000000) return encodeInteger(valueTypes.int48, value)
// Fall through to encoding the value as a number string.
}
const encoded = Buffer.from(String(value), 'utf8')
return [valueTypes.numberString, encodeValue(encoded.length), encoded]
}
if (type === 'string') {
const encoded = Buffer.from(value, 'utf8')
return [valueTypes.utf8, encodeValue(encoded.length), encoded]
}
if (type === 'bigint') {
const encoded = Buffer.from(String(value), 'utf8')
return [valueTypes.bigInt, encodeValue(encoded.length), encoded]
}
if (Buffer.isBuffer(value)) {
return [valueTypes.bytes, encodeValue(value.byteLength), value]
}
if (Array.isArray(value)) {
return [
value[descriptorSymbol] === true ? valueTypes.descriptor : valueTypes.list,
encodeValue(value.length),
value.map(x => encodeValue(x)),
]
}
const hex = `0x${type.toString(16).toUpperCase()}`
throw new TypeError(`Unexpected value with type ${hex}`)
}
function decodeValue (buffer, byteOffset) {
const type = buffer.readUInt8(byteOffset)
byteOffset += 1
if (type === valueTypes.zero) return { byteOffset, value: 0 }
if (type === valueTypes.negativeZero) return { byteOffset, value: -0 }
if (type === valueTypes.notANumber) return { byteOffset, value: NaN }
if (type === valueTypes.infinity) return { byteOffset, value: Infinity }
if (type === valueTypes.negativeInfinity) return { byteOffset, value: -Infinity }
if (type === valueTypes.undefined) return { byteOffset, value: undefined }
if (type === valueTypes.null) return { byteOffset, value: null }
if (type === valueTypes.true) return { byteOffset, value: true }
if (type === valueTypes.false) return { byteOffset, value: false }
if (
type === valueTypes.int8 || type === valueTypes.int16 || type === valueTypes.int24 ||
type === valueTypes.int32 || type === valueTypes.int40 || type === valueTypes.int48
) {
const value = buffer.readIntLE(byteOffset, type)
byteOffset += type
return { byteOffset, value }
}
if (type === valueTypes.numberString || type === valueTypes.utf8 || type === valueTypes.bytes || type === valueTypes.bigInt) {
const length = decodeValue(buffer, byteOffset)
const start = length.byteOffset
const end = start + length.value
if (type === valueTypes.numberString) {
const value = Number(buffer.toString('utf8', start, end))
return { byteOffset: end, value }
}
if (type === valueTypes.utf8) {
const value = buffer.toString('utf8', start, end)
return { byteOffset: end, value }
}
if (type === valueTypes.bigInt) {
const value = BigInt(buffer.toString('utf8', start, end)) // eslint-disable-line no-undef
return { byteOffset: end, value }
}
const value = buffer.slice(start, end)
return { byteOffset: end, value }
}
if (type === valueTypes.list || type === valueTypes.descriptor) {
const length = decodeValue(buffer, byteOffset)
byteOffset = length.byteOffset
const value = new Array(length.value)
if (type === valueTypes.descriptor) {
value[descriptorSymbol] = true
}
for (let index = 0; index < length.value; index++) {
const item = decodeValue(buffer, byteOffset)
byteOffset = item.byteOffset
value[index] = item.value
}
return { byteOffset, value }
}
const hex = `0x${type.toString(16).toUpperCase()}`
throw new TypeError(`Could not decode type ${hex}`)
}
function buildBuffer (numberOrArray) {
if (typeof numberOrArray === 'number') {
const byte = Buffer.alloc(1)
byte.writeUInt8(numberOrArray)
return byte
}
const array = flattenDeep(numberOrArray)
const buffers = new Array(array.length)
let byteLength = 0
for (const [index, element] of array.entries()) {
if (typeof element === 'number') {
byteLength += 1
const byte = Buffer.alloc(1)
byte.writeUInt8(element)
buffers[index] = byte
} else {
byteLength += element.byteLength
buffers[index] = element
}
}
return Buffer.concat(buffers, byteLength)
}
function encode (serializerVersion, rootRecord, usedPlugins) {
const buffers = []
let byteOffset = 0
const versionHeader = Buffer.alloc(2)
versionHeader.writeUInt16LE(serializerVersion)
buffers.push(versionHeader)
byteOffset += versionHeader.byteLength
const rootOffset = Buffer.alloc(4)
buffers.push(rootOffset)
byteOffset += rootOffset.byteLength
const numPlugins = buildBuffer(encodeValue(usedPlugins.size))
buffers.push(numPlugins)
byteOffset += numPlugins.byteLength
for (const name of usedPlugins.keys()) {
const plugin = usedPlugins.get(name)
const record = buildBuffer([
encodeValue(name),
encodeValue(plugin.serializerVersion),
])
buffers.push(record)
byteOffset += record.byteLength
}
const queue = [rootRecord]
const pointers = [rootOffset]
while (queue.length > 0) {
pointers.shift().writeUInt32LE(byteOffset, 0)
const record = queue.shift()
const recordHeader = buildBuffer([
encodeValue(record.pluginIndex),
encodeValue(record.id),
encodeValue(record.children.length),
])
buffers.push(recordHeader)
byteOffset += recordHeader.byteLength
// Add pointers before encoding the state. This allows, if it ever becomes
// necessary, for records to be extracted from a buffer without having to
// parse the (variable length) state field.
for (const child of record.children) {
queue.push(child)
const pointer = Buffer.alloc(4)
pointers.push(pointer)
buffers.push(pointer)
byteOffset += 4
}
const state = buildBuffer(encodeValue(record.state))
buffers.push(state)
byteOffset += state.byteLength
}
return Buffer.concat(buffers, byteOffset)
}
exports.encode = encode
function decodePlugins (buffer) {
const $numPlugins = decodeValue(buffer, 0)
let byteOffset = $numPlugins.byteOffset
const usedPlugins = new Map()
const lastIndex = $numPlugins.value
for (let index = 1; index <= lastIndex; index++) {
const $name = decodeValue(buffer, byteOffset)
const name = $name.value
byteOffset = $name.byteOffset
const serializerVersion = decodeValue(buffer, byteOffset).value
usedPlugins.set(index, { name, serializerVersion })
}
return usedPlugins
}
exports.decodePlugins = decodePlugins
function decodeRecord (buffer, byteOffset) {
const $pluginIndex = decodeValue(buffer, byteOffset)
const pluginIndex = $pluginIndex.value
byteOffset = $pluginIndex.byteOffset
const $id = decodeValue(buffer, byteOffset)
const id = $id.value
byteOffset = $id.byteOffset
const $numPointers = decodeValue(buffer, byteOffset)
const numPointers = $numPointers.value
byteOffset = $numPointers.byteOffset
const pointerAddresses = new Array(numPointers)
for (let index = 0; index < numPointers; index++) {
pointerAddresses[index] = buffer.readUInt32LE(byteOffset)
byteOffset += 4
}
const state = decodeValue(buffer, byteOffset).value
return { id, pluginIndex, state, pointerAddresses }
}
exports.decodeRecord = decodeRecord
function extractVersion (buffer) {
return buffer.readUInt16LE(0)
}
exports.extractVersion = extractVersion
function decode (buffer) {
const rootOffset = buffer.readUInt32LE(2)
const pluginBuffer = buffer.slice(6, rootOffset)
const rootRecord = decodeRecord(buffer, rootOffset)
return { pluginBuffer, rootRecord }
}
exports.decode = decode

View file

@ -0,0 +1,101 @@
'use strict'
const Circular = require('./Circular')
const Indenter = require('./Indenter')
const describe = require('./describe')
const lineBuilder = require('./lineBuilder')
const themeUtils = require('./themeUtils')
const alwaysFormat = () => true
const fixedIndent = new Indenter(0, ' ')
function formatDescriptor (subject, options) {
const theme = themeUtils.normalize(options)
if (subject.isPrimitive === true) {
const formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), fixedIndent)
return formatted.toString({ diff: false })
}
const circular = new Circular()
const maxDepth = (options && options.maxDepth) || 0
let indent = fixedIndent
const buffer = lineBuilder.buffer()
const stack = []
let topIndex = -1
do {
if (circular.has(subject)) {
stack[topIndex].formatter.append(lineBuilder.single(theme.circular), subject)
} else {
let didFormat = false
if (typeof subject.formatDeep === 'function') {
const formatted = subject.formatDeep(themeUtils.applyModifiers(subject, theme), indent)
if (formatted !== null) {
didFormat = true
if (topIndex === -1) {
buffer.append(formatted)
} else {
stack[topIndex].formatter.append(formatted, subject)
}
}
}
if (!didFormat && typeof subject.formatShallow === 'function') {
const formatter = subject.formatShallow(themeUtils.applyModifiers(subject, theme), indent)
const recursor = subject.createRecursor()
if (formatter.increaseIndent && maxDepth > 0 && indent.level === maxDepth) {
const isEmpty = recursor() === null
const formatted = !isEmpty && typeof formatter.maxDepth === 'function'
? formatter.maxDepth()
: formatter.finalize()
stack[topIndex].formatter.append(formatted, subject)
} else {
stack.push({
formatter,
recursor,
decreaseIndent: formatter.increaseIndent,
shouldFormat: formatter.shouldFormat || alwaysFormat,
subject,
})
topIndex++
if (formatter.increaseIndent) indent = indent.increase()
circular.add(subject)
}
}
}
while (topIndex >= 0) {
do {
subject = stack[topIndex].recursor()
} while (subject && !stack[topIndex].shouldFormat(subject))
if (subject) {
break
}
const record = stack.pop()
topIndex--
if (record.decreaseIndent) indent = indent.decrease()
circular.delete(record.subject)
const formatted = record.formatter.finalize()
if (topIndex === -1) {
buffer.append(formatted)
} else {
stack[topIndex].formatter.append(formatted, record.subject)
}
}
} while (topIndex >= 0)
return buffer.toString({ diff: false })
}
exports.formatDescriptor = formatDescriptor
function format (value, options) {
return formatDescriptor(describe(value, options), options)
}
exports.format = format

View file

@ -0,0 +1,123 @@
'use strict'
const lineBuilder = require('./lineBuilder')
function wrap (fromTheme, value) {
return fromTheme.open + value + fromTheme.close
}
exports.wrap = wrap
function formatCtorAndStringTag (theme, object) {
if (!object.ctor) return wrap(theme.object.stringTag, object.stringTag)
let retval = wrap(theme.object.ctor, object.ctor)
if (object.stringTag && object.stringTag !== object.ctor && object.stringTag !== 'Object') {
retval += ' ' + wrap(theme.object.secondaryStringTag, object.stringTag)
}
return retval
}
exports.formatCtorAndStringTag = formatCtorAndStringTag
class ObjectFormatter {
constructor (object, theme, indent) {
this.object = object
this.theme = theme
this.indent = indent
this.increaseIndent = true
this.innerLines = lineBuilder.buffer()
this.pendingStats = null
}
append (formatted, origin) {
if (origin.isStats === true) {
this.pendingStats = formatted
} else {
if (this.pendingStats !== null) {
if (!this.innerLines.isEmpty) {
this.innerLines.append(this.pendingStats)
}
this.pendingStats = null
}
this.innerLines.append(formatted)
}
}
finalize () {
const variant = this.object.isList
? this.theme.list
: this.theme.object
const ctor = this.object.ctor
const stringTag = this.object.stringTag
const prefix = (ctor === 'Array' || ctor === 'Object') && ctor === stringTag
? ''
: formatCtorAndStringTag(this.theme, this.object) + ' '
if (this.innerLines.isEmpty) {
return lineBuilder.single(prefix + variant.openBracket + variant.closeBracket)
}
return lineBuilder.first(prefix + variant.openBracket)
.concat(this.innerLines.withFirstPrefixed(this.indent.increase()).stripFlags())
.append(lineBuilder.last(this.indent + variant.closeBracket))
}
maxDepth () {
const variant = this.object.isList
? this.theme.list
: this.theme.object
return lineBuilder.single(
formatCtorAndStringTag(this.theme, this.object) + ' ' + variant.openBracket +
' ' + this.theme.maxDepth + ' ' + variant.closeBracket)
}
shouldFormat () {
return true
}
customize (methods) {
if (methods.finalize) {
this.finalize = () => methods.finalize(this.innerLines)
}
if (methods.maxDepth) {
this.maxDepth = methods.maxDepth
}
if (methods.shouldFormat) {
this.shouldFormat = methods.shouldFormat
}
return this
}
}
exports.ObjectFormatter = ObjectFormatter
class SingleValueFormatter {
constructor (theme, finalizeFn, increaseIndent) {
this.theme = theme
this.finalizeFn = finalizeFn
this.hasValue = false
this.increaseIndent = increaseIndent === true
this.value = null
}
append (formatted) {
if (this.hasValue) throw new Error('Formatter buffer can only take one formatted value.')
this.hasValue = true
this.value = formatted
}
finalize () {
if (!this.hasValue) throw new Error('Formatter buffer never received a formatted value.')
return this.finalizeFn(this.value)
}
maxDepth () {
return this.finalizeFn(lineBuilder.single(this.theme.maxDepth))
}
}
exports.SingleValueFormatter = SingleValueFormatter

View file

@ -0,0 +1,43 @@
'use strict'
const hop = Object.prototype.hasOwnProperty
function getCtor (stringTag, value) {
if (value.constructor) {
const name = value.constructor.name
return typeof name === 'string' && name !== ''
? name
: null
}
if (value.constructor === undefined) {
if (stringTag !== 'Object' || value instanceof Object) return null
// Values without a constructor, that do not inherit from `Object`, but are
// tagged as objects, may come from `Object.create(null)`. Or they can come
// from a different realm, e.g.:
//
// ```
// require('vm').runInNewContext(`
// const Foo = function () {}
// Foo.prototype.constructor = undefined
// return new Foo()
// `)
// ```
//
// Treat such objects as if they came from `Object.create(null)` (in the
// current realm) only if they do not have inherited properties. This allows
// these objects to be compared with object literals.
//
// This means `Object.create(null)` is not differentiated from `{}`.
// Using `const` prevents Crankshaft optimizations
for (var p in value) { // eslint-disable-line no-var
if (!hop.call(value, p)) return null
}
return stringTag
}
return null
}
module.exports = getCtor

View file

@ -0,0 +1,33 @@
'use strict'
function getObjectKeys (obj, excludeListItemAccessorsBelowLength) {
const keys = []
let size = 0
// Sort property names, they should never be order-sensitive
const nameCandidates = Object.getOwnPropertyNames(obj).sort()
// Comparators should verify symbols in an order-insensitive manner if
// possible.
const symbolCandidates = Object.getOwnPropertySymbols(obj)
for (const name of nameCandidates) {
let accept = true
if (excludeListItemAccessorsBelowLength > 0) {
const index = Number(name)
accept = !Number.isInteger(index) || index < 0 || index >= excludeListItemAccessorsBelowLength
}
if (accept && Object.getOwnPropertyDescriptor(obj, name).enumerable) {
keys[size++] = name
}
}
for (const symbol of symbolCandidates) {
if (Object.getOwnPropertyDescriptor(obj, symbol).enumerable) {
keys[size++] = symbol
}
}
return { keys, size }
}
module.exports = getObjectKeys

View file

@ -0,0 +1,30 @@
'use strict'
const ts = Object.prototype.toString
function getStringTag (value) {
return ts.call(value).slice(8, -1)
}
const fts = Function.prototype.toString
const promiseCtorString = fts.call(Promise)
const isPromise = value => {
if (!value.constructor) return false
try {
return fts.call(value.constructor) === promiseCtorString
} catch {
return false
}
}
if (getStringTag(Promise.resolve()) === 'Promise') {
module.exports = getStringTag
} else {
const getStringTagWithPromiseWorkaround = value => {
const stringTag = getStringTag(value)
return stringTag === 'Object' && isPromise(value)
? 'Promise'
: stringTag
}
module.exports = getStringTagWithPromiseWorkaround
}

View file

@ -0,0 +1,15 @@
'use strict'
const isLength = require('lodash/isLength')
const hop = Object.prototype.hasOwnProperty
function hasLength (obj) {
return (
Array.isArray(obj) ||
(hop.call(obj, 'length') &&
isLength(obj.length) &&
(obj.length === 0 || '0' in obj))
)
}
module.exports = hasLength

View file

@ -0,0 +1,7 @@
'use strict'
function isEnumerable (obj, key) {
const desc = Object.getOwnPropertyDescriptor(obj, key)
return desc && desc.enumerable
}
module.exports = isEnumerable

View file

@ -0,0 +1,309 @@
'use strict'
const ACTUAL = Symbol('lineBuilder.gutters.ACTUAL')
const EXPECTED = Symbol('lineBuilder.gutters.EXPECTED')
function translateGutter (theme, invert, gutter) {
if (invert) {
if (gutter === ACTUAL) return theme.diffGutters.expected
if (gutter === EXPECTED) return theme.diffGutters.actual
} else {
if (gutter === ACTUAL) return theme.diffGutters.actual
if (gutter === EXPECTED) return theme.diffGutters.expected
}
return theme.diffGutters.padding
}
class Line {
constructor (isFirst, isLast, gutter, stringValue) {
this.isFirst = isFirst
this.isLast = isLast
this.gutter = gutter
this.stringValue = stringValue
}
* [Symbol.iterator] () {
yield this
}
get isEmpty () {
return false
}
get hasGutter () {
return this.gutter !== null
}
get isSingle () {
return this.isFirst && this.isLast
}
append (other) {
return this.concat(other)
}
concat (other) {
return new Collection()
.append(this)
.append(other)
}
toString (options) {
if (options.diff === false) return this.stringValue
return translateGutter(options.theme, options.invert, this.gutter) + this.stringValue
}
mergeWithInfix (infix, other) {
if (other.isLine !== true) {
return new Collection()
.append(this)
.mergeWithInfix(infix, other)
}
return new Line(this.isFirst, other.isLast, other.gutter, this.stringValue + infix + other.stringValue)
}
withFirstPrefixed (prefix) {
if (!this.isFirst) return this
return new Line(true, this.isLast, this.gutter, prefix + this.stringValue)
}
withLastPostfixed (postfix) {
if (!this.isLast) return this
return new Line(this.isFirst, true, this.gutter, this.stringValue + postfix)
}
stripFlags () {
return new Line(false, false, this.gutter, this.stringValue)
}
decompose () {
return new Collection()
.append(this)
.decompose()
}
}
Object.defineProperty(Line.prototype, 'isLine', { value: true })
class Collection {
constructor () {
this.buffer = []
}
* [Symbol.iterator] () {
for (const appended of this.buffer) {
for (const line of appended) yield line
}
}
get isEmpty () {
return this.buffer.length === 0
}
get hasGutter () {
for (const line of this) {
if (line.hasGutter) return true
}
return false
}
get isSingle () {
const iterator = this[Symbol.iterator]()
iterator.next()
return iterator.next().done === true
}
append (lineOrLines) {
if (!lineOrLines.isEmpty) this.buffer.push(lineOrLines)
return this
}
concat (other) {
return new Collection()
.append(this)
.append(other)
}
toString (options) {
let lines = this
if (options.invert) {
lines = new Collection()
let buffer = new Collection()
let prev = null
for (const line of this) {
if (line.gutter === ACTUAL) {
if (prev !== null && prev.gutter !== ACTUAL && !buffer.isEmpty) {
lines.append(buffer)
buffer = new Collection()
}
buffer.append(line)
} else if (line.gutter === EXPECTED) {
lines.append(line)
} else {
if (!buffer.isEmpty) {
lines.append(buffer)
buffer = new Collection()
}
lines.append(line)
}
prev = line
}
lines.append(buffer)
}
return Array.from(lines, line => line.toString(options)).join('\n')
}
mergeWithInfix (infix, from) {
if (from.isEmpty) throw new Error('Cannot merge, `from` is empty.')
const otherLines = Array.from(from)
if (!otherLines[0].isFirst) throw new Error('Cannot merge, `from` has no first line.')
const merged = new Collection()
let seenLast = false
for (const line of this) {
if (seenLast) throw new Error('Cannot merge line, the last line has already been seen.')
if (!line.isLast) {
merged.append(line)
continue
}
seenLast = true
for (const other of otherLines) {
if (other.isFirst) {
merged.append(line.mergeWithInfix(infix, other))
} else {
merged.append(other)
}
}
}
return merged
}
withFirstPrefixed (prefix) {
return new Collection()
.append(Array.from(this, line => line.withFirstPrefixed(prefix)))
}
withLastPostfixed (postfix) {
return new Collection()
.append(Array.from(this, line => line.withLastPostfixed(postfix)))
}
stripFlags () {
return new Collection()
.append(Array.from(this, line => line.stripFlags()))
}
decompose () {
const first = { actual: new Collection(), expected: new Collection() }
const last = { actual: new Collection(), expected: new Collection() }
const remaining = new Collection()
for (const line of this) {
if (line.isFirst && line.gutter === ACTUAL) {
first.actual.append(line)
} else if (line.isFirst && line.gutter === EXPECTED) {
first.expected.append(line)
} else if (line.isLast && line.gutter === ACTUAL) {
last.actual.append(line)
} else if (line.isLast && line.gutter === EXPECTED) {
last.expected.append(line)
} else {
remaining.append(line)
}
}
return { first, last, remaining }
}
}
Object.defineProperty(Collection.prototype, 'isCollection', { value: true })
function setDefaultGutter (iterable, gutter) {
return new Collection()
.append(Array.from(iterable, line => {
return line.gutter === null
? new Line(line.isFirst, line.isLast, gutter, line.stringValue)
: line
}))
}
module.exports = {
buffer () {
return new Collection()
},
first (stringValue) {
return new Line(true, false, null, stringValue)
},
last (stringValue) {
return new Line(false, true, null, stringValue)
},
line (stringValue) {
return new Line(false, false, null, stringValue)
},
single (stringValue) {
return new Line(true, true, null, stringValue)
},
setDefaultGutter (lineOrCollection) {
return lineOrCollection
},
actual: {
first (stringValue) {
return new Line(true, false, ACTUAL, stringValue)
},
last (stringValue) {
return new Line(false, true, ACTUAL, stringValue)
},
line (stringValue) {
return new Line(false, false, ACTUAL, stringValue)
},
single (stringValue) {
return new Line(true, true, ACTUAL, stringValue)
},
setDefaultGutter (lineOrCollection) {
return setDefaultGutter(lineOrCollection, ACTUAL)
},
},
expected: {
first (stringValue) {
return new Line(true, false, EXPECTED, stringValue)
},
last (stringValue) {
return new Line(false, true, EXPECTED, stringValue)
},
line (stringValue) {
return new Line(false, false, EXPECTED, stringValue)
},
single (stringValue) {
return new Line(true, true, EXPECTED, stringValue)
},
setDefaultGutter (lineOrCollection) {
return setDefaultGutter(lineOrCollection, EXPECTED)
},
},
}

View file

@ -0,0 +1,254 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const recursorUtils = require('../recursorUtils')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describeComplex (index, value) {
return new ComplexItem(index, value)
}
exports.describeComplex = describeComplex
function deserializeComplex (index, recursor) {
const value = recursor()
return new ComplexItem(index, value)
}
exports.deserializeComplex = deserializeComplex
function describePrimitive (index, value) {
return new PrimitiveItem(index, value)
}
exports.describePrimitive = describePrimitive
function deserializePrimitive (state) {
const index = state[0]
const value = state[1]
return new PrimitiveItem(index, value)
}
exports.deserializePrimitive = deserializePrimitive
const complexTag = Symbol('ComplexItem')
exports.complexTag = complexTag
const primitiveTag = Symbol('PrimitiveItem')
exports.primitiveTag = primitiveTag
class ComplexItem {
constructor (index, value) {
this.index = index
this.value = value
}
createRecursor () {
return recursorUtils.singleValue(this.value)
}
compare (expected) {
return expected.tag === complexTag && this.index === expected.index
? this.value.compare(expected.value)
: UNEQUAL
}
formatShallow (theme, indent) {
const increaseValueIndent = theme.item.increaseValueIndent === true
return new formatUtils.SingleValueFormatter(theme, value => {
if (typeof theme.item.customFormat === 'function') {
return theme.item.customFormat(theme, indent, value)
}
return value.withLastPostfixed(theme.item.after)
}, increaseValueIndent)
}
prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) {
// Circular values cannot be compared. They must be treated as being unequal when diffing.
if (isCircular(this.value) || isCircular(expected.value)) return { compareResult: UNEQUAL }
// Try to line up this or remaining items with the expected items.
const lhsFork = recursorUtils.fork(lhsRecursor)
const rhsFork = recursorUtils.fork(rhsRecursor)
const initialExpected = expected
let expectedIsMissing = false
while (!expectedIsMissing && expected !== null && expected.isItem === true) {
if (expected.tag === complexTag) {
expectedIsMissing = compareComplexShape(this.value, expected.value) !== UNEQUAL
}
expected = rhsFork.shared()
}
let actualIsExtraneous = false
if (initialExpected.tag === complexTag) {
let actual = this
while (!actualIsExtraneous && actual !== null && actual.isItem === true) {
if (actual.tag === complexTag) {
actualIsExtraneous = compareComplexShape(actual.value, initialExpected.value) !== UNEQUAL
}
actual = lhsFork.shared()
}
} else if (initialExpected.tag === primitiveTag) {
let actual = this
while (!actualIsExtraneous && actual !== null && actual.isItem === true) {
if (actual.tag === primitiveTag) {
actualIsExtraneous = initialExpected.value.compare(actual.value) === DEEP_EQUAL
}
actual = lhsFork.shared()
}
}
if (actualIsExtraneous && !expectedIsMissing) {
return {
actualIsExtraneous: true,
lhsRecursor: lhsFork.recursor,
rhsRecursor: recursorUtils.map(
recursorUtils.unshift(rhsFork.recursor, initialExpected),
next => {
if (next.isItem !== true) return next
next.index++
return next
}),
}
}
if (expectedIsMissing && !actualIsExtraneous) {
return {
expectedIsMissing: true,
lhsRecursor: recursorUtils.map(
recursorUtils.unshift(lhsFork.recursor, this),
next => {
if (next.isItem !== true) return next
next.index++
return next
}),
rhsRecursor: rhsFork.recursor,
}
}
const mustRecurse = this.tag === complexTag && initialExpected.tag === complexTag &&
this.value.compare(initialExpected.value) !== UNEQUAL
return {
mustRecurse,
isUnequal: !mustRecurse,
lhsRecursor: lhsFork.recursor,
rhsRecursor: rhsFork.recursor,
}
}
serialize () {
return this.index
}
}
Object.defineProperty(ComplexItem.prototype, 'isItem', { value: true })
Object.defineProperty(ComplexItem.prototype, 'tag', { value: complexTag })
class PrimitiveItem {
constructor (index, value) {
this.index = index
this.value = value
}
compare (expected) {
return expected.tag === primitiveTag && this.index === expected.index
? this.value.compare(expected.value)
: UNEQUAL
}
formatDeep (theme, indent) {
const increaseValueIndent = theme.item.increaseValueIndent === true
const valueIndent = increaseValueIndent ? indent.increase() : indent
// Since the value is formatted directly, modifiers are not applied. Apply
// modifiers to the item descriptor instead.
const formatted = this.value.formatDeep(theme, valueIndent)
if (typeof theme.item.customFormat === 'function') {
return theme.item.customFormat(theme, indent, formatted)
}
return formatted.withLastPostfixed(theme.item.after)
}
prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) {
const compareResult = this.compare(expected)
// Short-circuit when values are deeply equal.
if (compareResult === DEEP_EQUAL) return { compareResult }
// Short-circut when values can be diffed directly.
if (
expected.tag === primitiveTag &&
this.value.tag === expected.value.tag && typeof this.value.diffDeep === 'function'
) {
return { compareResult }
}
// Try to line up this or remaining items with the expected items.
const rhsFork = recursorUtils.fork(rhsRecursor)
const initialExpected = expected
do {
if (expected === null || expected.isItem !== true) {
return {
actualIsExtraneous: true,
rhsRecursor: recursorUtils.map(
recursorUtils.unshift(rhsFork.recursor, initialExpected),
next => {
if (next.isItem !== true) return next
next.index++
return next
}),
}
}
if (this.value.compare(expected.value) === DEEP_EQUAL) {
return {
expectedIsMissing: true,
lhsRecursor: recursorUtils.map(
recursorUtils.unshift(lhsRecursor, this),
next => {
if (next.isItem !== true) return next
next.index++
return next
}),
rhsRecursor: rhsFork.recursor,
}
}
expected = rhsFork.shared()
} while (true)
}
diffDeep (expected, theme, indent, invert) {
// Verify a diff can be returned.
if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null
const increaseValueIndent = theme.property.increaseValueIndent === true
const valueIndent = increaseValueIndent ? indent.increase() : indent
// Since the value is diffed directly, modifiers are not applied. Apply
// modifiers to the item descriptor instead.
const diff = this.value.diffDeep(expected.value, theme, valueIndent, invert)
if (diff === null) return null
if (typeof theme.item.customFormat === 'function') {
return theme.item.customFormat(theme, indent, diff)
}
return diff.withLastPostfixed(theme.item.after)
}
serialize () {
return [this.index, this.value]
}
}
Object.defineProperty(PrimitiveItem.prototype, 'isItem', { value: true })
Object.defineProperty(PrimitiveItem.prototype, 'tag', { value: primitiveTag })

View file

@ -0,0 +1,223 @@
'use strict'
const constants = require('../constants')
const lineBuilder = require('../lineBuilder')
const recursorUtils = require('../recursorUtils')
const themeUtils = require('../themeUtils')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
function describe (keyDescriptor, valueDescriptor) {
const keyIsPrimitive = keyDescriptor.isPrimitive === true
const valueIsPrimitive = valueDescriptor.isPrimitive === true
return new MapEntry(keyDescriptor, valueDescriptor, keyIsPrimitive, valueIsPrimitive)
}
exports.describe = describe
function deserialize (state, recursor) {
const keyIsPrimitive = state[0]
const valueIsPrimitive = state[1]
const keyDescriptor = recursor()
const valueDescriptor = recursor()
return new MapEntry(keyDescriptor, valueDescriptor, keyIsPrimitive, valueIsPrimitive)
}
exports.deserialize = deserialize
const tag = Symbol('MapEntry')
exports.tag = tag
function mergeWithKey (theme, key, values) {
const lines = lineBuilder.buffer()
const keyRemainder = lineBuilder.buffer()
for (const line of key) {
if (!line.isLast && !line.hasGutter) {
lines.append(line)
} else {
keyRemainder.append(line)
}
}
for (const value of values) {
lines.append(keyRemainder.mergeWithInfix(theme.mapEntry.separator, value).withLastPostfixed(theme.mapEntry.after))
}
return lines
}
class MapEntry {
constructor (key, value, keyIsPrimitive, valueIsPrimitive) {
this.key = key
this.value = value
this.keyIsPrimitive = keyIsPrimitive
this.valueIsPrimitive = valueIsPrimitive
}
createRecursor () {
let emitKey = true
let emitValue = true
return () => {
if (emitKey) {
emitKey = false
return this.key
}
if (emitValue) {
emitValue = false
return this.value
}
return null
}
}
compare (expected) {
if (this.tag !== expected.tag) return UNEQUAL
if (this.keyIsPrimitive !== expected.keyIsPrimitive) return UNEQUAL
if (this.valueIsPrimitive !== expected.valueIsPrimitive) return UNEQUAL
if (!this.keyIsPrimitive) return SHALLOW_EQUAL
const keyResult = this.key.compare(expected.key)
if (keyResult !== DEEP_EQUAL) return keyResult
if (!this.valueIsPrimitive) return SHALLOW_EQUAL
return this.value.compare(expected.value)
}
formatDeep (theme, indent) {
// Verify the map entry can be formatted directly.
if (!this.keyIsPrimitive || typeof this.value.formatDeep !== 'function') return null
// Since formatShallow() would result in theme modifiers being applied
// before the key and value are formatted, do the same here.
const value = this.value.formatDeep(themeUtils.applyModifiersToOriginal(this.value, theme), indent)
if (value === null) return null
const key = this.key.formatDeep(themeUtils.applyModifiersToOriginal(this.key, theme), indent)
return mergeWithKey(theme, key, [value])
}
formatShallow (theme, indent) {
let key = null
const values = []
return {
append: (formatted, origin) => {
if (this.key === origin) {
key = formatted
} else {
values.push(formatted)
}
},
finalize () {
return mergeWithKey(theme, key, values)
},
}
}
diffDeep (expected, theme, indent, invert) {
// Verify a diff can be returned.
if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null
// Only use this logic to format value diffs when the keys are primitive and equal.
if (!this.keyIsPrimitive || !expected.keyIsPrimitive || this.key.compare(expected.key) !== DEEP_EQUAL) {
return null
}
// Since formatShallow() would result in theme modifiers being applied
// before the key and value are formatted, do the same here.
const diff = this.value.diffDeep(expected.value, themeUtils.applyModifiersToOriginal(this.value, theme), indent, invert)
if (diff === null) return null
const key = this.key.formatDeep(themeUtils.applyModifiersToOriginal(this.key, theme), indent, '')
return mergeWithKey(theme, key, [diff])
}
prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) {
// Circular values cannot be compared. They must be treated as being unequal when diffing.
if (isCircular(this.value) || isCircular(expected.value)) return { compareResult: UNEQUAL }
const compareResult = this.compare(expected)
const keysAreEqual = this.tag === expected.tag && this.key.compare(expected.key) === DEEP_EQUAL
// Short-circuit when keys and/or values are deeply equal.
if (compareResult === DEEP_EQUAL || keysAreEqual) return { compareResult }
// Try to line up this or remaining map entries with the expected entries.
const lhsFork = recursorUtils.fork(lhsRecursor)
const rhsFork = recursorUtils.fork(rhsRecursor)
const initialExpected = expected
let expectedIsMissing = false
while (!expectedIsMissing && expected !== null && this.tag === expected.tag) {
if (expected.keyIsPrimitive) {
expectedIsMissing = this.key.compare(expected.key) !== UNEQUAL
} else {
expectedIsMissing = compareComplexShape(this.key, expected.key) !== UNEQUAL
}
expected = rhsFork.shared()
}
let actualIsExtraneous = false
if (this.tag === initialExpected.tag) {
if (initialExpected.keyIsPrimitive) {
let actual = this
while (!actualIsExtraneous && actual !== null && this.tag === actual.tag) {
if (actual.keyIsPrimitive) {
actualIsExtraneous = initialExpected.key.compare(actual.key) === DEEP_EQUAL
}
actual = lhsFork.shared()
}
} else {
let actual = this
while (!actualIsExtraneous && actual !== null && this.tag === actual.tag) {
if (!actual.keyIsPrimitive) {
actualIsExtraneous = compareComplexShape(actual.key, initialExpected.key) !== UNEQUAL
}
actual = lhsFork.shared()
}
}
}
if (actualIsExtraneous && !expectedIsMissing) {
return {
actualIsExtraneous: true,
lhsRecursor: lhsFork.recursor,
rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected),
}
}
if (expectedIsMissing && !actualIsExtraneous) {
return {
expectedIsMissing: true,
lhsRecursor: recursorUtils.unshift(lhsFork.recursor, this),
rhsRecursor: rhsFork.recursor,
}
}
let mustRecurse = false
if (!this.keyIsPrimitive && !initialExpected.keyIsPrimitive) {
if (this.valueIsPrimitive || initialExpected.valueIsPrimitive) {
mustRecurse = this.value.compare(initialExpected.value) !== UNEQUAL
} else {
mustRecurse = compareComplexShape(this.value, initialExpected.value) !== UNEQUAL
}
}
return {
mustRecurse,
isUnequal: !mustRecurse,
lhsRecursor: lhsFork.recursor,
rhsRecursor: rhsFork.recursor,
}
}
serialize () {
return [this.keyIsPrimitive, this.valueIsPrimitive]
}
}
Object.defineProperty(MapEntry.prototype, 'isMapEntry', { value: true })
Object.defineProperty(MapEntry.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,31 @@
'use strict'
const UNEQUAL = require('../constants').UNEQUAL
function describe (index) {
return new Pointer(index)
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('Pointer')
exports.tag = tag
class Pointer {
constructor (index) {
this.index = index
}
// Pointers cannot be compared, and are not expected to be part of the
// comparisons.
compare (expected) {
return UNEQUAL
}
serialize () {
return this.index
}
}
Object.defineProperty(Pointer.prototype, 'isPointer', { value: true })
Object.defineProperty(Pointer.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,190 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const symbolPrimitive = require('../primitiveValues/symbol').tag
const recursorUtils = require('../recursorUtils')
const AMBIGUOUS = constants.AMBIGUOUS
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describeComplex (key, value) {
return new ComplexProperty(key, value)
}
exports.describeComplex = describeComplex
function deserializeComplex (key, recursor) {
const value = recursor()
return new ComplexProperty(key, value)
}
exports.deserializeComplex = deserializeComplex
function describePrimitive (key, value) {
return new PrimitiveProperty(key, value)
}
exports.describePrimitive = describePrimitive
function deserializePrimitive (state) {
const key = state[0]
const value = state[1]
return new PrimitiveProperty(key, value)
}
exports.deserializePrimitive = deserializePrimitive
const complexTag = Symbol('ComplexProperty')
exports.complexTag = complexTag
const primitiveTag = Symbol('PrimitiveProperty')
exports.primitiveTag = primitiveTag
class Property {
constructor (key) {
this.key = key
}
compareKeys (expected) {
const result = this.key.compare(expected.key)
// Return AMBIGUOUS if symbol keys are unequal. It's likely that properties
// are compared in order of declaration, which is not the desired strategy.
// Returning AMBIGUOUS allows compare() and diff() to recognize this
// situation and sort the symbol properties before comparing them.
return result === UNEQUAL && this.key.tag === symbolPrimitive && expected.key.tag === symbolPrimitive
? AMBIGUOUS
: result
}
prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape, isCircular) {
// Circular values cannot be compared. They must be treated as being unequal when diffing.
if (isCircular(this.value) || isCircular(expected.value)) return { compareResult: UNEQUAL }
// Try to line up this or remaining properties with the expected properties.
const rhsFork = recursorUtils.fork(rhsRecursor)
const initialExpected = expected
do {
if (expected === null || expected.isProperty !== true) {
return {
actualIsExtraneous: true,
rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected),
}
} else if (this.key.compare(expected.key) === DEEP_EQUAL) {
if (expected === initialExpected) {
return null
} else {
return {
expectedIsMissing: true,
lhsRecursor: recursorUtils.unshift(lhsRecursor, this),
rhsRecursor: rhsFork.recursor,
}
}
}
expected = rhsFork.shared()
} while (true)
}
}
Object.defineProperty(Property.prototype, 'isProperty', { value: true })
class ComplexProperty extends Property {
constructor (key, value) {
super(key)
this.value = value
}
createRecursor () {
return recursorUtils.singleValue(this.value)
}
compare (expected) {
if (expected.isProperty !== true) return UNEQUAL
const keyResult = this.compareKeys(expected)
if (keyResult !== DEEP_EQUAL) return keyResult
return this.tag === expected.tag
? this.value.compare(expected.value)
: UNEQUAL
}
formatShallow (theme, indent) {
const increaseValueIndent = theme.property.increaseValueIndent === true
return new formatUtils.SingleValueFormatter(theme, value => {
if (typeof theme.property.customFormat === 'function') {
return theme.property.customFormat(theme, indent, this.key, value)
}
return value
.withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator)
.withLastPostfixed(theme.property.after)
}, increaseValueIndent)
}
serialize () {
return this.key
}
}
Object.defineProperty(ComplexProperty.prototype, 'tag', { value: complexTag })
class PrimitiveProperty extends Property {
constructor (key, value) {
super(key)
this.value = value
}
compare (expected) {
if (expected.isProperty !== true) return UNEQUAL
const keyResult = this.compareKeys(expected)
if (keyResult !== DEEP_EQUAL) return keyResult
return this.tag !== expected.tag
? UNEQUAL
: this.value.compare(expected.value)
}
formatDeep (theme, indent) {
const increaseValueIndent = theme.property.increaseValueIndent === true
const valueIndent = increaseValueIndent ? indent.increase() : indent
// Since the key and value are formatted directly, modifiers are not
// applied. Apply modifiers to the property descriptor instead.
const formatted = this.value.formatDeep(theme, valueIndent)
if (typeof theme.property.customFormat === 'function') {
return theme.property.customFormat(theme, indent, this.key, formatted)
}
return formatted
.withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator)
.withLastPostfixed(theme.property.after)
}
diffDeep (expected, theme, indent, invert) {
// Verify a diff can be returned.
if (this.tag !== expected.tag || typeof this.value.diffDeep !== 'function') return null
// Only use this logic to diff values when the keys are the same.
if (this.key.compare(expected.key) !== DEEP_EQUAL) return null
const increaseValueIndent = theme.property.increaseValueIndent === true
const valueIndent = increaseValueIndent ? indent.increase() : indent
// Since the key and value are diffed directly, modifiers are not
// applied. Apply modifiers to the property descriptor instead.
const diff = this.value.diffDeep(expected.value, theme, valueIndent, invert)
if (diff === null) return null
if (typeof theme.property.customFormat === 'function') {
return theme.property.customFormat(theme, indent, this.key, diff)
}
return diff
.withFirstPrefixed(this.key.formatAsKey(theme) + theme.property.separator)
.withLastPostfixed(theme.property.after)
}
serialize () {
return [this.key, this.value]
}
}
Object.defineProperty(PrimitiveProperty.prototype, 'tag', { value: primitiveTag })

View file

@ -0,0 +1,136 @@
'use strict'
const constants = require('../constants')
const lineBuilder = require('../lineBuilder')
const recursorUtils = require('../recursorUtils')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describeIterableRecursor (recursor) {
return new IterableStats(recursor.size)
}
exports.describeIterableRecursor = describeIterableRecursor
function describeListRecursor (recursor) {
return new ListStats(recursor.size)
}
exports.describeListRecursor = describeListRecursor
function describePropertyRecursor (recursor) {
return new PropertyStats(recursor.size)
}
exports.describePropertyRecursor = describePropertyRecursor
function deserializeIterableStats (size) {
return new IterableStats(size)
}
exports.deserializeIterableStats = deserializeIterableStats
function deserializeListStats (size) {
return new ListStats(size)
}
exports.deserializeListStats = deserializeListStats
function deserializePropertyStats (size) {
return new PropertyStats(size)
}
exports.deserializePropertyStats = deserializePropertyStats
const iterableTag = Symbol('IterableStats')
exports.iterableTag = iterableTag
const listTag = Symbol('ListStats')
exports.listTag = listTag
const propertyTag = Symbol('PropertyStats')
exports.propertyTag = propertyTag
class Stats {
constructor (size) {
this.size = size
}
formatDeep (theme) {
return lineBuilder.single(theme.stats.separator)
}
prepareDiff (expected, lhsRecursor, rhsRecursor, compareComplexShape) {
if (expected.isStats !== true || expected.tag === this.tag) return null
// Try to line up stats descriptors with the same tag.
const rhsFork = recursorUtils.fork(rhsRecursor)
const initialExpected = expected
const missing = []
while (expected !== null && this.tag !== expected.tag) {
missing.push(expected)
expected = rhsFork.shared()
}
if (expected !== null && missing.length > 0) {
return {
multipleAreMissing: true,
descriptors: missing,
lhsRecursor: recursorUtils.unshift(lhsRecursor, this),
// Use original `rhsRecursor`, not `rhsFork`, since the consumed
// descriptors are returned with the `missing` array.
rhsRecursor: recursorUtils.unshift(rhsRecursor, expected),
}
}
const lhsFork = recursorUtils.fork(lhsRecursor)
let actual = this
const extraneous = []
while (actual !== null && actual.tag !== initialExpected.tag) {
extraneous.push(actual)
actual = lhsFork.shared()
}
if (actual !== null && extraneous.length > 0) {
return {
multipleAreExtraneous: true,
descriptors: extraneous,
// Use original `lhsRecursor`, not `lhsFork`, since the consumed
// descriptors are returned with the `extraneous` array.
lhsRecursor: recursorUtils.unshift(lhsRecursor, actual),
rhsRecursor: recursorUtils.unshift(rhsFork.recursor, initialExpected),
}
}
return null
}
serialize () {
return this.size
}
}
Object.defineProperty(Stats.prototype, 'isStats', { value: true })
class IterableStats extends Stats {
compare (expected) {
return expected.tag === iterableTag && this.size === expected.size
? DEEP_EQUAL
: UNEQUAL
}
}
Object.defineProperty(IterableStats.prototype, 'tag', { value: iterableTag })
class ListStats extends Stats {
compare (expected) {
return expected.tag === listTag && this.size === expected.size
? DEEP_EQUAL
: UNEQUAL
}
}
Object.defineProperty(ListStats.prototype, 'tag', { value: listTag })
class PropertyStats extends Stats {
compare (expected) {
return expected.tag === propertyTag && this.size === expected.size
? DEEP_EQUAL
: UNEQUAL
}
}
Object.defineProperty(PropertyStats.prototype, 'tag', { value: propertyTag })

View file

@ -0,0 +1,222 @@
'use strict'
const semver = require('semver')
const pkg = require('../package.json')
const object = require('./complexValues/object')
const constants = require('./constants')
const formatUtils = require('./formatUtils')
const lineBuilder = require('./lineBuilder')
const itemDescriptor = require('./metaDescriptors/item')
const propertyDescriptor = require('./metaDescriptors/property')
const stringDescriptor = require('./primitiveValues/string')
const recursorUtils = require('./recursorUtils')
const themeUtils = require('./themeUtils')
const API_VERSION = 1
const CONCORDANCE_VERSION = pkg.version
const descriptorRegistry = new Map()
const registry = new Map()
class PluginError extends Error {
constructor (message, plugin) {
super(message)
this.name = 'PluginError'
this.plugin = plugin
}
}
class PluginTypeError extends TypeError {
constructor (message, plugin) {
super(message)
this.name = 'PluginTypeError'
this.plugin = plugin
}
}
class UnsupportedApiError extends PluginError {
constructor (plugin) {
super('Plugin requires an unsupported API version', plugin)
this.name = 'UnsupportedApiError'
}
}
class UnsupportedError extends PluginError {
constructor (plugin) {
super('Plugin does not support this version of Concordance', plugin)
this.name = 'UnsupportedError'
}
}
class DuplicateDescriptorTagError extends PluginError {
constructor (tag, plugin) {
super(`Could not add descriptor: tag ${String(tag)} has already been registered`, plugin)
this.name = 'DuplicateDescriptorTagError'
this.tag = tag
}
}
class DuplicateDescriptorIdError extends PluginError {
constructor (id, plugin) {
const printed = typeof id === 'number'
? `0x${id.toString(16).toUpperCase()}`
: String(id)
super(`Could not add descriptor: id ${printed} has already been registered`, plugin)
this.name = 'DuplicateDescriptorIdError'
this.id = id
}
}
function verify (plugin) {
if (typeof plugin.name !== 'string' || !plugin.name) {
throw new PluginTypeError('Plugin must have a `name`', plugin)
}
if (plugin.apiVersion !== API_VERSION) {
throw new UnsupportedApiError(plugin)
}
if ('minimalConcordanceVersion' in plugin) {
if (!semver.valid(plugin.minimalConcordanceVersion)) {
throw new PluginTypeError('If specified, `minimalConcordanceVersion` must be a valid SemVer version', plugin)
}
const range = `>=${plugin.minimalConcordanceVersion}`
if (!semver.satisfies(CONCORDANCE_VERSION, range)) {
throw new UnsupportedError(plugin)
}
}
}
// Selectively expose descriptor tags.
const publicDescriptorTags = Object.freeze({
complexItem: itemDescriptor.complexTag,
primitiveItem: itemDescriptor.primitiveTag,
primitiveProperty: propertyDescriptor.primitiveTag,
string: stringDescriptor.tag,
})
// Don't expose `setDefaultGutter()`.
const publicLineBuilder = Object.freeze({
buffer: lineBuilder.buffer,
first: lineBuilder.first,
last: lineBuilder.last,
line: lineBuilder.line,
single: lineBuilder.single,
actual: Object.freeze({
buffer: lineBuilder.actual.buffer,
first: lineBuilder.actual.first,
last: lineBuilder.actual.last,
line: lineBuilder.actual.line,
single: lineBuilder.actual.single,
}),
expected: Object.freeze({
buffer: lineBuilder.expected.buffer,
first: lineBuilder.expected.first,
last: lineBuilder.expected.last,
line: lineBuilder.expected.line,
single: lineBuilder.expected.single,
}),
})
function modifyTheme (descriptor, modifier) {
themeUtils.addModifier(descriptor, modifier)
return descriptor
}
function add (plugin) {
verify(plugin)
const name = plugin.name
if (registry.has(name)) return registry.get(name)
const id2deserialize = new Map()
const tag2id = new Map()
const addDescriptor = (id, tag, deserialize) => {
if (id2deserialize.has(id)) throw new DuplicateDescriptorIdError(id, plugin)
if (descriptorRegistry.has(tag) || tag2id.has(tag)) throw new DuplicateDescriptorTagError(tag, plugin)
id2deserialize.set(id, deserialize)
tag2id.set(tag, id)
}
const tryDescribeValue = plugin.register({
// Concordance makes assumptions about when AMBIGUOUS occurs. Do not expose
// it to plugins.
UNEQUAL: constants.UNEQUAL,
SHALLOW_EQUAL: constants.SHALLOW_EQUAL,
DEEP_EQUAL: constants.DEEP_EQUAL,
ObjectValue: object.ObjectValue,
DescribedMixin: object.DescribedMixin,
DeserializedMixin: object.DeserializedMixin,
addDescriptor,
applyThemeModifiers: themeUtils.applyModifiers,
descriptorTags: publicDescriptorTags,
lineBuilder: publicLineBuilder,
mapRecursor: recursorUtils.map,
modifyTheme,
wrapFromTheme: formatUtils.wrap,
})
const registered = {
id2deserialize,
serializerVersion: plugin.serializerVersion,
name,
tag2id,
theme: plugin.theme || {},
tryDescribeValue,
}
registry.set(name, registered)
for (const tag of tag2id.keys()) {
descriptorRegistry.set(tag, registered)
}
return registered
}
exports.add = add
function getDeserializers (plugins) {
return plugins.map(plugin => {
const registered = add(plugin)
return {
id2deserialize: registered.id2deserialize,
name: registered.name,
serializerVersion: registered.serializerVersion,
}
})
}
exports.getDeserializers = getDeserializers
function getThemes (plugins) {
return plugins.map(plugin => {
const registered = add(plugin)
return {
name: registered.name,
theme: registered.theme,
}
})
}
exports.getThemes = getThemes
function getTryDescribeValues (plugins) {
return plugins.map(plugin => add(plugin).tryDescribeValue)
}
exports.getTryDescribeValues = getTryDescribeValues
function resolveDescriptorRef (tag) {
if (!descriptorRegistry.has(tag)) return null
const registered = descriptorRegistry.get(tag)
return {
id: registered.tag2id.get(tag),
name: registered.name,
serialization: {
serializerVersion: registered.serializerVersion,
},
}
}
exports.resolveDescriptorRef = resolveDescriptorRef

View file

@ -0,0 +1,40 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (value) {
return new BigIntValue(value)
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('BigIntValue')
exports.tag = tag
class BigIntValue {
constructor (value) {
this.value = value
}
compare (expected) {
return expected.tag === tag && Object.is(this.value, expected.value)
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
return lineBuilder.single(formatUtils.wrap(theme.bigInt, `${this.value}n`))
}
serialize () {
return this.value
}
}
Object.defineProperty(BigIntValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(BigIntValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,40 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (value) {
return new BooleanValue(value)
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('BooleanValue')
exports.tag = tag
class BooleanValue {
constructor (value) {
this.value = value
}
compare (expected) {
return this.tag === expected.tag && this.value === expected.value
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
return lineBuilder.single(formatUtils.wrap(theme.boolean, this.value === true ? 'true' : 'false'))
}
serialize () {
return this.value
}
}
Object.defineProperty(BooleanValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(BooleanValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,32 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe () {
return new NullValue()
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('NullValue')
exports.tag = tag
class NullValue {
compare (expected) {
return expected.tag === tag
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
return lineBuilder.single(formatUtils.wrap(theme.null, 'null'))
}
}
Object.defineProperty(NullValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(NullValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,41 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (value) {
return new NumberValue(value)
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('NumberValue')
exports.tag = tag
class NumberValue {
constructor (value) {
this.value = value
}
compare (expected) {
return expected.tag === tag && Object.is(this.value, expected.value)
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
const string = Object.is(this.value, -0) ? '-0' : String(this.value)
return lineBuilder.single(formatUtils.wrap(theme.number, string))
}
serialize () {
return this.value
}
}
Object.defineProperty(NumberValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(NumberValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,318 @@
'use strict'
const keyword = require('esutils').keyword
const fastDiff = require('fast-diff')
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (value) {
return new StringValue(value)
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('StringValue')
exports.tag = tag
// TODO: Escape invisible characters (e.g. zero-width joiner, non-breaking space),
// ambiguous characters (other kinds of spaces, combining characters). Use
// http://graphemica.com/blocks/control-pictures where applicable.
function basicEscape (string) {
return string.replace(/\\/g, '\\\\')
}
const CRLF_CONTROL_PICTURE = '\u240D\u240A'
const LF_CONTROL_PICTURE = '\u240A'
const CR_CONTROL_PICTURE = '\u240D'
const MATCH_CONTROL_PICTURES = new RegExp(`${CR_CONTROL_PICTURE}|${LF_CONTROL_PICTURE}|${CR_CONTROL_PICTURE}`, 'g')
function escapeLinebreak (string) {
if (string === '\r\n') return CRLF_CONTROL_PICTURE
if (string === '\n') return LF_CONTROL_PICTURE
if (string === '\r') return CR_CONTROL_PICTURE
return string
}
function themeControlPictures (theme, resetWrap, str) {
return str.replace(MATCH_CONTROL_PICTURES, picture => {
return resetWrap.close + formatUtils.wrap(theme.string.controlPicture, picture) + resetWrap.open
})
}
const MATCH_SINGLE_QUOTE = /'/g
const MATCH_DOUBLE_QUOTE = /"/g
const MATCH_BACKTICKS = /`/g
function escapeQuotes (line, string) {
const quote = line.escapeQuote
if (quote === '\'') return string.replace(MATCH_SINGLE_QUOTE, "\\'")
if (quote === '"') return string.replace(MATCH_DOUBLE_QUOTE, '\\"')
if (quote === '`') return string.replace(MATCH_BACKTICKS, '\\`')
return string
}
function includesLinebreaks (string) {
return string.includes('\r') || string.includes('\n')
}
function diffLine (theme, actual, expected, invert) {
const outcome = fastDiff(actual, expected)
// TODO: Compute when line is mostly unequal (80%? 90%?) and treat it as being
// completely unequal.
const isPartiallyEqual = !(
(outcome.length === 2 && outcome[0][1] === actual && outcome[1][1] === expected) ||
// Discount line ending control pictures, which will be equal even when the
// rest of the line isn't.
(
outcome.length === 3 &&
outcome[2][0] === fastDiff.EQUAL &&
MATCH_CONTROL_PICTURES.test(outcome[2][1]) &&
outcome[0][1] + outcome[2][1] === actual &&
outcome[1][1] + outcome[2][1] === expected
)
)
let stringActual = ''
let stringExpected = ''
const noopWrap = { open: '', close: '' }
let deleteWrap = isPartiallyEqual ? theme.string.diff.delete : noopWrap
let insertWrap = isPartiallyEqual ? theme.string.diff.insert : noopWrap
const equalWrap = isPartiallyEqual ? theme.string.diff.equal : noopWrap
if (invert) {
[deleteWrap, insertWrap] = [insertWrap, deleteWrap]
}
for (const diff of outcome) {
if (diff[0] === fastDiff.DELETE) {
stringActual += formatUtils.wrap(deleteWrap, diff[1])
} else if (diff[0] === fastDiff.INSERT) {
stringExpected += formatUtils.wrap(insertWrap, diff[1])
} else {
const string = formatUtils.wrap(equalWrap, themeControlPictures(theme, equalWrap, diff[1]))
stringActual += string
stringExpected += string
}
}
if (!isPartiallyEqual) {
const deleteLineWrap = invert ? theme.string.diff.insertLine : theme.string.diff.deleteLine
const insertLineWrap = invert ? theme.string.diff.deleteLine : theme.string.diff.insertLine
stringActual = formatUtils.wrap(deleteLineWrap, stringActual)
stringExpected = formatUtils.wrap(insertLineWrap, stringExpected)
}
return [stringActual, stringExpected]
}
const LINEBREAKS = /\r\n|\r|\n/g
function gatherLines (string) {
const lines = []
let prevIndex = 0
for (let match; (match = LINEBREAKS.exec(string)); prevIndex = match.index + match[0].length) {
lines.push(string.slice(prevIndex, match.index) + escapeLinebreak(match[0]))
}
lines.push(string.slice(prevIndex))
return lines
}
class StringValue {
constructor (value) {
this.value = value
}
compare (expected) {
return expected.tag === tag && this.value === expected.value
? DEEP_EQUAL
: UNEQUAL
}
get includesLinebreaks () {
return includesLinebreaks(this.value)
}
formatDeep (theme, indent) {
// Escape backslashes
let escaped = basicEscape(this.value)
if (!this.includesLinebreaks) {
escaped = escapeQuotes(theme.string.line, escaped)
return lineBuilder.single(formatUtils.wrap(theme.string.line, formatUtils.wrap(theme.string, escaped)))
}
escaped = escapeQuotes(theme.string.multiline, escaped)
const lineStrings = gatherLines(escaped).map(string => {
return formatUtils.wrap(theme.string, themeControlPictures(theme, theme.string, string))
})
const lastIndex = lineStrings.length - 1
const indentation = indent
return lineBuilder.buffer()
.append(
lineStrings.map((string, index) => {
if (index === 0) return lineBuilder.first(theme.string.multiline.start + string)
if (index === lastIndex) return lineBuilder.last(indentation + string + theme.string.multiline.end)
return lineBuilder.line(indentation + string)
}))
}
formatAsKey (theme) {
const key = this.value
if (keyword.isIdentifierNameES6(key, true) || String(parseInt(key, 10)) === key) {
return key
}
const escaped = basicEscape(key)
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/'/g, "\\'")
return formatUtils.wrap(theme.string.line, formatUtils.wrap(theme.string, escaped))
}
diffDeep (expected, theme, indent, invert) {
if (expected.tag !== tag) return null
const escapedActual = basicEscape(this.value)
const escapedExpected = basicEscape(expected.value)
if (!includesLinebreaks(escapedActual) && !includesLinebreaks(escapedExpected)) {
const result = diffLine(theme,
escapeQuotes(theme.string.line, escapedActual),
escapeQuotes(theme.string.line, escapedExpected),
invert,
)
return lineBuilder.actual.single(formatUtils.wrap(theme.string.line, result[0]))
.concat(lineBuilder.expected.single(formatUtils.wrap(theme.string.line, result[1])))
}
const actualLines = gatherLines(escapeQuotes(theme.string.multiline, escapedActual))
const expectedLines = gatherLines(escapeQuotes(theme.string.multiline, escapedExpected))
const indentation = indent
const lines = lineBuilder.buffer()
const lastActualIndex = actualLines.length - 1
const lastExpectedIndex = expectedLines.length - 1
let actualBuffer = []
let expectedBuffer = []
let mustOpenNextExpected = false
for (let actualIndex = 0, expectedIndex = 0, extraneousOffset = 0; actualIndex < actualLines.length;) {
if (actualLines[actualIndex] === expectedLines[expectedIndex]) {
lines.append(actualBuffer)
lines.append(expectedBuffer)
actualBuffer = []
expectedBuffer = []
let string = actualLines[actualIndex]
string = themeControlPictures(theme, theme.string.diff.equal, string)
string = formatUtils.wrap(theme.string.diff.equal, string)
if (actualIndex === 0) {
lines.append(lineBuilder.first(theme.string.multiline.start + string))
} else if (actualIndex === lastActualIndex && expectedIndex === lastExpectedIndex) {
lines.append(lineBuilder.last(indentation + string + theme.string.multiline.end))
} else {
lines.append(lineBuilder.line(indentation + string))
}
actualIndex++
expectedIndex++
continue
}
let expectedIsMissing = false
{
const compare = actualLines[actualIndex]
for (let index = expectedIndex; !expectedIsMissing && index < expectedLines.length; index++) {
expectedIsMissing = compare === expectedLines[index]
}
}
let actualIsExtraneous = (actualIndex - extraneousOffset) > lastExpectedIndex || expectedIndex > lastExpectedIndex
if (!actualIsExtraneous) {
const compare = expectedLines[expectedIndex]
for (let index = actualIndex; !actualIsExtraneous && index < actualLines.length; index++) {
actualIsExtraneous = compare === actualLines[index]
}
if (!actualIsExtraneous && (actualIndex - extraneousOffset) === lastExpectedIndex && actualIndex < lastActualIndex) {
actualIsExtraneous = true
}
}
if (actualIsExtraneous && !expectedIsMissing) {
const wrap = invert ? theme.string.diff.insertLine : theme.string.diff.deleteLine
const string = formatUtils.wrap(wrap, actualLines[actualIndex])
if (actualIndex === 0) {
actualBuffer.push(lineBuilder.actual.first(theme.string.multiline.start + string))
mustOpenNextExpected = true
} else if (actualIndex === lastActualIndex) {
actualBuffer.push(lineBuilder.actual.last(indentation + string + theme.string.multiline.end))
} else {
actualBuffer.push(lineBuilder.actual.line(indentation + string))
}
actualIndex++
extraneousOffset++
} else if (expectedIsMissing && !actualIsExtraneous) {
const wrap = invert ? theme.string.diff.deleteLine : theme.string.diff.insertLine
const string = formatUtils.wrap(wrap, expectedLines[expectedIndex])
if (mustOpenNextExpected) {
expectedBuffer.push(lineBuilder.expected.first(theme.string.multiline.start + string))
mustOpenNextExpected = false
} else if (expectedIndex === lastExpectedIndex) {
expectedBuffer.push(lineBuilder.expected.last(indentation + string + theme.string.multiline.end))
} else {
expectedBuffer.push(lineBuilder.expected.line(indentation + string))
}
expectedIndex++
} else {
const result = diffLine(theme, actualLines[actualIndex], expectedLines[expectedIndex], invert)
if (actualIndex === 0) {
actualBuffer.push(lineBuilder.actual.first(theme.string.multiline.start + result[0]))
mustOpenNextExpected = true
} else if (actualIndex === lastActualIndex) {
actualBuffer.push(lineBuilder.actual.last(indentation + result[0] + theme.string.multiline.end))
} else {
actualBuffer.push(lineBuilder.actual.line(indentation + result[0]))
}
if (mustOpenNextExpected) {
expectedBuffer.push(lineBuilder.expected.first(theme.string.multiline.start + result[1]))
mustOpenNextExpected = false
} else if (expectedIndex === lastExpectedIndex) {
expectedBuffer.push(lineBuilder.expected.last(indentation + result[1] + theme.string.multiline.end))
} else {
expectedBuffer.push(lineBuilder.expected.line(indentation + result[1]))
}
actualIndex++
expectedIndex++
}
}
lines.append(actualBuffer)
lines.append(expectedBuffer)
return lines
}
serialize () {
return this.value
}
}
Object.defineProperty(StringValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(StringValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,114 @@
'use strict'
const stringEscape = require('js-string-escape')
const wellKnownSymbols = require('well-known-symbols')
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe (value) {
let stringCompare = null
const key = Symbol.keyFor(value)
if (key !== undefined) {
stringCompare = `Symbol.for(${stringEscape(key)})`
} else if (wellKnownSymbols.isWellKnown(value)) {
stringCompare = wellKnownSymbols.getLabel(value)
}
return new SymbolValue({
stringCompare,
value,
})
}
exports.describe = describe
function deserialize (state) {
const stringCompare = state[0]
const string = state[1] || state[0]
return new DeserializedSymbolValue({
string,
stringCompare,
value: null,
})
}
exports.deserialize = deserialize
const tag = Symbol('SymbolValue')
exports.tag = tag
class SymbolValue {
constructor (props) {
this.stringCompare = props.stringCompare
this.value = props.value
}
compare (expected) {
if (expected.tag !== tag) return UNEQUAL
if (this.stringCompare !== null) {
return this.stringCompare === expected.stringCompare
? DEEP_EQUAL
: UNEQUAL
}
return this.value === expected.value
? DEEP_EQUAL
: UNEQUAL
}
formatString () {
if (this.stringCompare !== null) return this.stringCompare
return stringEscape(this.value.toString())
}
formatDeep (theme) {
return lineBuilder.single(formatUtils.wrap(theme.symbol, this.formatString()))
}
formatAsKey (theme) {
return formatUtils.wrap(theme.property.keyBracket, formatUtils.wrap(theme.symbol, this.formatString()))
}
serialize () {
const string = this.formatString()
return this.stringCompare === string
? [this.stringCompare]
: [this.stringCompare, string]
}
}
Object.defineProperty(SymbolValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(SymbolValue.prototype, 'tag', { value: tag })
class DeserializedSymbolValue extends SymbolValue {
constructor (props) {
super(props)
this.string = props.string
}
compare (expected) {
if (expected.tag !== tag) return UNEQUAL
if (this.stringCompare !== null) {
return this.stringCompare === expected.stringCompare
? DEEP_EQUAL
: UNEQUAL
}
// Symbols that are not in the global symbol registry, and are not
// well-known, cannot be compared when deserialized. Treat symbols
// as equal if they are formatted the same.
return this.string === expected.formatString()
? DEEP_EQUAL
: UNEQUAL
}
formatString () {
return this.string
}
}

View file

@ -0,0 +1,32 @@
'use strict'
const constants = require('../constants')
const formatUtils = require('../formatUtils')
const lineBuilder = require('../lineBuilder')
const DEEP_EQUAL = constants.DEEP_EQUAL
const UNEQUAL = constants.UNEQUAL
function describe () {
return new UndefinedValue()
}
exports.describe = describe
exports.deserialize = describe
const tag = Symbol('UndefinedValue')
exports.tag = tag
class UndefinedValue {
compare (expected) {
return expected.tag === tag
? DEEP_EQUAL
: UNEQUAL
}
formatDeep (theme) {
return lineBuilder.single(formatUtils.wrap(theme.undefined, 'undefined'))
}
}
Object.defineProperty(UndefinedValue.prototype, 'isPrimitive', { value: true })
Object.defineProperty(UndefinedValue.prototype, 'tag', { value: tag })

View file

@ -0,0 +1,126 @@
'use strict'
const NOOP_RECURSOR = {
size: 0,
next () { return null },
}
exports.NOOP_RECURSOR = NOOP_RECURSOR
function fork (recursor) {
const buffer = []
return {
shared () {
const next = recursor()
if (next !== null) buffer.push(next)
return next
},
recursor () {
if (buffer.length > 0) return buffer.shift()
return recursor()
},
}
}
exports.fork = fork
function consumeDeep (recursor) {
const stack = [recursor]
while (stack.length > 0) {
const subject = stack[stack.length - 1]()
if (subject === null) {
stack.pop()
continue
}
if (typeof subject.createRecursor === 'function') {
stack.push(subject.createRecursor())
}
}
}
exports.consumeDeep = consumeDeep
function map (recursor, mapFn) {
return () => {
const next = recursor()
if (next === null) return null
return mapFn(next)
}
}
exports.map = map
function replay (state, create) {
if (!state) {
const recursor = create()
if (recursor === NOOP_RECURSOR) {
state = recursor
} else {
state = Object.assign({
buffer: [],
done: false,
}, recursor)
}
}
if (state === NOOP_RECURSOR) return { state, recursor: state }
let done = false
let index = 0
const next = () => {
if (done) return null
let retval = state.buffer[index]
if (retval === undefined) {
retval = state.buffer[index] = state.next()
}
index++
if (retval === null) {
done = true
}
return retval
}
return { state, recursor: { next, size: state.size } }
}
exports.replay = replay
function sequence (first, second) {
let fromFirst = true
return () => {
if (fromFirst) {
const next = first()
if (next !== null) return next
fromFirst = false
}
return second()
}
}
exports.sequence = sequence
function singleValue (value) {
let done = false
return () => {
if (done) return null
done = true
return value
}
}
exports.singleValue = singleValue
function unshift (recursor, value) {
return () => {
if (value !== null) {
const next = value
value = null
return next
}
return recursor()
}
}
exports.unshift = unshift

View file

@ -0,0 +1,361 @@
'use strict'
const md5hex = require('md5-hex')
const argumentsValue = require('./complexValues/arguments')
const arrayBufferValue = require('./complexValues/arrayBuffer')
const boxedValue = require('./complexValues/boxed')
const dataViewValue = require('./complexValues/dataView')
const dateValue = require('./complexValues/date')
const errorValue = require('./complexValues/error')
const functionValue = require('./complexValues/function')
const globalValue = require('./complexValues/global')
const mapValue = require('./complexValues/map')
const objectValue = require('./complexValues/object')
const promiseValue = require('./complexValues/promise')
const regexpValue = require('./complexValues/regexp')
const setValue = require('./complexValues/set')
const typedArrayValue = require('./complexValues/typedArray')
const encoder = require('./encoder')
const itemDescriptor = require('./metaDescriptors/item')
const mapEntryDescriptor = require('./metaDescriptors/mapEntry')
const pointerDescriptor = require('./metaDescriptors/pointer')
const propertyDescriptor = require('./metaDescriptors/property')
const statsDescriptors = require('./metaDescriptors/stats')
const pluginRegistry = require('./pluginRegistry')
const bigIntValue = require('./primitiveValues/bigInt')
const booleanValue = require('./primitiveValues/boolean')
const nullValue = require('./primitiveValues/null')
const numberValue = require('./primitiveValues/number')
const stringValue = require('./primitiveValues/string')
const symbolValue = require('./primitiveValues/symbol')
const undefinedValue = require('./primitiveValues/undefined')
const recursorUtils = require('./recursorUtils')
// Increment if encoding layout, descriptor IDs, or value types change. Previous
// Concordance versions will not be able to decode buffers generated by a newer
// version, so changing this value will require a major version bump of
// Concordance itself. The version is encoded as an unsigned 16 bit integer.
const VERSION = 3
// Adding or removing mappings or changing an index requires the version in
// encoder.js to be bumped, which necessitates a major version bump of
// Concordance itself. Indexes are hexadecimal to make reading the binary
// output easier.
const mappings = [
[0x01, bigIntValue.tag, bigIntValue.deserialize],
[0x02, booleanValue.tag, booleanValue.deserialize],
[0x03, nullValue.tag, nullValue.deserialize],
[0x04, numberValue.tag, numberValue.deserialize],
[0x05, stringValue.tag, stringValue.deserialize],
[0x06, symbolValue.tag, symbolValue.deserialize],
[0x07, undefinedValue.tag, undefinedValue.deserialize],
[0x08, objectValue.tag, objectValue.deserialize],
[0x09, statsDescriptors.iterableTag, statsDescriptors.deserializeIterableStats],
[0x0A, statsDescriptors.listTag, statsDescriptors.deserializeListStats],
[0x0B, itemDescriptor.complexTag, itemDescriptor.deserializeComplex],
[0x0C, itemDescriptor.primitiveTag, itemDescriptor.deserializePrimitive],
[0x0D, statsDescriptors.propertyTag, statsDescriptors.deserializePropertyStats],
[0x0E, propertyDescriptor.complexTag, propertyDescriptor.deserializeComplex],
[0x0F, propertyDescriptor.primitiveTag, propertyDescriptor.deserializePrimitive],
[0x10, pointerDescriptor.tag, pointerDescriptor.deserialize],
[0x11, mapValue.tag, mapValue.deserialize],
[0x12, mapEntryDescriptor.tag, mapEntryDescriptor.deserialize],
[0x13, argumentsValue.tag, argumentsValue.deserialize],
[0x14, arrayBufferValue.tag, arrayBufferValue.deserialize],
[0x15, boxedValue.tag, boxedValue.deserialize],
[0x16, dataViewValue.tag, dataViewValue.deserialize],
[0x17, dateValue.tag, dateValue.deserialize],
[0x18, errorValue.tag, errorValue.deserialize],
[0x19, functionValue.tag, functionValue.deserialize],
[0x1A, globalValue.tag, globalValue.deserialize],
[0x1B, promiseValue.tag, promiseValue.deserialize],
[0x1C, regexpValue.tag, regexpValue.deserialize],
[0x1D, setValue.tag, setValue.deserialize],
[0x1E, typedArrayValue.tag, typedArrayValue.deserialize],
[0x1F, typedArrayValue.bytesTag, typedArrayValue.deserializeBytes],
]
const tag2id = new Map(mappings.map(mapping => [mapping[1], mapping[0]]))
const id2deserialize = new Map(mappings.map(mapping => [mapping[0], mapping[2]]))
class DescriptorSerializationError extends Error {
constructor (descriptor) {
super('Could not serialize descriptor')
this.name = 'DescriptorSerializationError'
this.descriptor = descriptor
}
}
class MissingPluginError extends Error {
constructor (pluginName) {
super(`Could not deserialize buffer: missing plugin ${JSON.stringify(pluginName)}`)
this.name = 'MissingPluginError'
this.pluginName = pluginName
}
}
class PointerLookupError extends Error {
constructor (index) {
super(`Could not deserialize buffer: pointer ${index} could not be resolved`)
this.name = 'PointerLookupError'
this.index = index
}
}
class UnsupportedPluginError extends Error {
constructor (pluginName, serializerVersion) {
super(`Could not deserialize buffer: plugin ${JSON.stringify(pluginName)} expects a different serialization`)
this.name = 'UnsupportedPluginError'
this.pluginName = pluginName
this.serializerVersion = serializerVersion
}
}
class UnsupportedVersion extends Error { // eslint-disable-line unicorn/custom-error-definition
constructor (serializerVersion) {
super('Could not deserialize buffer: a different serialization was expected')
this.name = 'UnsupportedVersion'
this.serializerVersion = serializerVersion
}
}
function shallowSerializeDescriptor (descriptor, resolvePluginRef) {
if (!descriptor.serialize) return undefined
return serializeState(descriptor.serialize(), resolvePluginRef)
}
function serializeState (state, resolvePluginRef) {
if (Array.isArray(state)) return state.map(x => serializeState(x))
if (state && state.tag) {
let id, pluginIndex
if (tag2id.has(state.tag)) {
id = tag2id.get(state.tag)
pluginIndex = 0
} else {
const ref = resolvePluginRef(state.tag)
if (ref) {
id = ref.id
pluginIndex = ref.pluginIndex
}
}
if (id !== undefined) {
const serialized = [pluginIndex, id, shallowSerializeDescriptor(state, resolvePluginRef)]
serialized[encoder.descriptorSymbol] = true
return serialized
}
}
return state
}
function serialize (descriptor) {
const usedPlugins = new Map()
const resolvePluginRef = tag => {
const ref = pluginRegistry.resolveDescriptorRef(tag)
if (!ref) return null
if (!usedPlugins.has(ref.name)) {
// Start at 1, since 0 is reserved for Concordance's descriptors.
const index = usedPlugins.size + 1
usedPlugins.set(ref.name, Object.assign({ index }, ref.serialization))
}
return {
id: ref.id,
pluginIndex: usedPlugins.get(ref.name).index,
}
}
const seen = new Set()
const stack = []
let topIndex = -1
let rootRecord
do {
if (descriptor.isComplex === true) {
if (seen.has(descriptor.pointer)) {
descriptor = pointerDescriptor.describe(descriptor.pointer)
} else {
seen.add(descriptor.pointer)
}
}
let id
let pluginIndex = 0
if (tag2id.has(descriptor.tag)) {
id = tag2id.get(descriptor.tag)
} else {
const ref = resolvePluginRef(descriptor.tag)
if (!ref) throw new DescriptorSerializationError(descriptor)
id = ref.id
pluginIndex = ref.pluginIndex
}
const record = {
id,
pluginIndex,
children: [],
state: shallowSerializeDescriptor(descriptor, resolvePluginRef),
}
if (!rootRecord) {
rootRecord = record
} else {
stack[topIndex].children.push(record)
}
if (descriptor.createRecursor) {
stack.push({ recursor: descriptor.createRecursor(), children: record.children })
topIndex++
}
while (topIndex >= 0) {
descriptor = stack[topIndex].recursor()
if (descriptor === null) {
stack.pop()
topIndex--
} else {
break
}
}
} while (topIndex >= 0)
return encoder.encode(VERSION, rootRecord, usedPlugins)
}
exports.serialize = serialize
function deserializeState (state, getDescriptorDeserializer) {
if (state && state[encoder.descriptorSymbol] === true) {
return shallowDeserializeDescriptor(state, getDescriptorDeserializer)
}
return Array.isArray(state)
? state.map(item => deserializeState(item, getDescriptorDeserializer))
: state
}
function shallowDeserializeDescriptor (entry, getDescriptorDeserializer) {
const deserializeDescriptor = getDescriptorDeserializer(entry[0], entry[1])
return deserializeDescriptor(entry[2])
}
function deserializeRecord (record, getDescriptorDeserializer, buffer) {
const deserializeDescriptor = getDescriptorDeserializer(record.pluginIndex, record.id)
const state = deserializeState(record.state, getDescriptorDeserializer)
if (record.pointerAddresses.length === 0) {
return deserializeDescriptor(state)
}
const endIndex = record.pointerAddresses.length
let index = 0
const recursor = () => {
if (index === endIndex) return null
const recursorRecord = encoder.decodeRecord(buffer, record.pointerAddresses[index++])
return deserializeRecord(recursorRecord, getDescriptorDeserializer, buffer)
}
return deserializeDescriptor(state, recursor)
}
function buildPluginMap (buffer, options) {
const cache = options && options.deserializedPluginsCache
const cacheKey = md5hex(buffer)
if (cache && cache.has(cacheKey)) return cache.get(cacheKey)
const decodedPlugins = encoder.decodePlugins(buffer)
if (decodedPlugins.size === 0) {
const pluginMap = new Map()
if (cache) cache.set(cacheKey, pluginMap)
return pluginMap
}
const deserializerLookup = new Map()
if (Array.isArray(options && options.plugins)) {
for (const deserializer of pluginRegistry.getDeserializers(options.plugins)) {
deserializerLookup.set(deserializer.name, deserializer)
}
}
const pluginMap = new Map()
for (const index of decodedPlugins.keys()) {
const used = decodedPlugins.get(index)
const pluginName = used.name
const serializerVersion = used.serializerVersion
// TODO: Allow plugin author to encode a helpful message in its serialization
if (!deserializerLookup.has(pluginName)) {
throw new MissingPluginError(pluginName)
}
if (serializerVersion !== deserializerLookup.get(pluginName).serializerVersion) {
throw new UnsupportedPluginError(pluginName, serializerVersion)
}
pluginMap.set(index, deserializerLookup.get(pluginName).id2deserialize)
}
if (cache) cache.set(cacheKey, pluginMap)
return pluginMap
}
function deserialize (buffer, options) {
const version = encoder.extractVersion(buffer)
if (version !== VERSION) throw new UnsupportedVersion(version)
const decoded = encoder.decode(buffer)
const pluginMap = buildPluginMap(decoded.pluginBuffer, options)
const descriptorsByPointerIndex = new Map()
const mapPointerDescriptor = descriptor => {
if (descriptor.isPointer === true) {
if (descriptorsByPointerIndex.has(descriptor.index)) {
return descriptorsByPointerIndex.get(descriptor.index)
}
if (typeof rootDescriptor.createRecursor === 'function') {
// The descriptor we're pointing to may be elsewhere in the serialized
// structure. Consume the entire structure and check again.
recursorUtils.consumeDeep(rootDescriptor.createRecursor())
if (descriptorsByPointerIndex.has(descriptor.index)) {
return descriptorsByPointerIndex.get(descriptor.index)
}
}
throw new PointerLookupError(descriptor.index)
}
if (descriptor.isComplex === true) {
descriptorsByPointerIndex.set(descriptor.pointer, descriptor)
}
return descriptor
}
const getDescriptorDeserializer = (pluginIndex, id) => {
return (state, recursor) => {
const deserializeDescriptor = pluginIndex === 0
? id2deserialize.get(id)
: pluginMap.get(pluginIndex).get(id)
return mapPointerDescriptor(deserializeDescriptor(state, recursor))
}
}
const rootDescriptor = deserializeRecord(decoded.rootRecord, getDescriptorDeserializer, buffer)
return rootDescriptor
}
exports.deserialize = deserialize

View file

@ -0,0 +1,17 @@
'use strict'
const argumentsObject = require('./complexValues/arguments').tag
const constants = require('./constants')
const AMBIGUOUS = constants.AMBIGUOUS
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
function shouldCompareDeep (result, lhs, rhs) {
if (result === SHALLOW_EQUAL) return true
if (result !== AMBIGUOUS) return false
// Properties are only ambiguous if they have symbol keys. These properties
// must be compared in an order-insensitive manner.
return lhs.tag === argumentsObject || lhs.isProperty === true
}
module.exports = shouldCompareDeep

View file

@ -0,0 +1,106 @@
'use strict'
const constants = require('./constants')
const recursorUtils = require('./recursorUtils')
const DEEP_EQUAL = constants.DEEP_EQUAL
const SHALLOW_EQUAL = constants.SHALLOW_EQUAL
const UNEQUAL = constants.UNEQUAL
class Comparable {
constructor (properties) {
this.properties = properties
this.ordered = properties.slice()
}
createRecursor () {
const length = this.ordered.length
let index = 0
return () => {
if (index === length) return null
return this.ordered[index++]
}
}
compare (expected) {
if (this.properties.length !== expected.properties.length) return UNEQUAL
// Compare property keys, reordering the expected properties in the process
// so values can be compared if all keys are equal.
const ordered = []
const processed = new Set()
for (const property of this.properties) {
let extraneous = true
for (const other of expected.properties) {
if (processed.has(other.key)) continue
if (property.key.compare(other.key) === DEEP_EQUAL) {
extraneous = false
processed.add(other.key)
ordered.push(other)
break
}
}
if (extraneous) return UNEQUAL
}
expected.ordered = ordered
return SHALLOW_EQUAL
}
prepareDiff (expected) {
// Reorder the expected properties before recursion starts.
const missingProperties = []
const ordered = []
const processed = new Set()
for (const other of expected.properties) {
let missing = true
for (const property of this.properties) {
if (processed.has(property.key)) continue
if (property.key.compare(other.key) === DEEP_EQUAL) {
missing = false
processed.add(property.key)
ordered.push(other)
break
}
}
if (missing) {
missingProperties.push(other)
}
}
expected.ordered = ordered.concat(missingProperties)
return { mustRecurse: true }
}
}
Object.defineProperty(Comparable.prototype, 'isSymbolPropertiesComparable', { value: true })
exports.Comparable = Comparable
class Collector {
constructor (firstProperty, recursor) {
this.properties = [firstProperty]
this.recursor = recursor
this.remainder = null
}
collectAll () {
do {
const next = this.recursor()
if (next && next.isProperty === true) { // All properties will have symbol keys
this.properties.push(next)
} else {
return next
}
} while (true)
}
createRecursor () {
return recursorUtils.singleValue(new Comparable(this.properties))
}
}
Object.defineProperty(Collector.prototype, 'isSymbolPropertiesCollector', { value: true })
exports.Collector = Collector

View file

@ -0,0 +1,196 @@
'use strict'
const cloneDeep = require('lodash/cloneDeep')
const merge = require('lodash/merge')
const pluginRegistry = require('./pluginRegistry')
function freezeTheme (theme) {
const queue = [theme]
while (queue.length > 0) {
const object = queue.shift()
Object.freeze(object)
for (const key of Object.keys(object)) {
const value = object[key]
if (value !== null && typeof value === 'object') {
queue.push(value)
}
}
}
return theme
}
const defaultTheme = freezeTheme({
bigInt: { open: '', close: '' },
boolean: { open: '', close: '' },
circular: '[Circular]',
date: {
invalid: 'invalid',
value: { open: '', close: '' },
},
diffGutters: {
actual: '- ',
expected: '+ ',
padding: ' ',
},
error: {
ctor: { open: '(', close: ')' },
name: { open: '', close: '' },
},
function: {
name: { open: '', close: '' },
stringTag: { open: '', close: '' },
},
global: { open: '', close: '' },
item: {
after: ',',
customFormat: null,
increaseValueIndent: false,
},
list: { openBracket: '[', closeBracket: ']' },
mapEntry: {
after: ',',
separator: ' => ',
},
maxDepth: '…',
null: { open: '', close: '' },
number: { open: '', close: '' },
object: {
openBracket: '{',
closeBracket: '}',
ctor: { open: '', close: '' },
stringTag: { open: '@', close: '' },
secondaryStringTag: { open: '@', close: '' },
},
property: {
after: ',',
customFormat: null,
keyBracket: { open: '[', close: ']' },
separator: ': ',
increaseValueIndent: false,
},
regexp: {
source: { open: '/', close: '/' },
flags: { open: '', close: '' },
separator: '---',
},
stats: { separator: '---' },
string: {
open: '',
close: '',
line: { open: "'", close: "'", escapeQuote: "'" },
multiline: { start: '`', end: '`', escapeQuote: '`' },
controlPicture: { open: '', close: '' },
diff: {
insert: { open: '', close: '' },
delete: { open: '', close: '' },
equal: { open: '', close: '' },
insertLine: { open: '', close: '' },
deleteLine: { open: '', close: '' },
},
},
symbol: { open: '', close: '' },
typedArray: {
bytes: { open: '', close: '' },
},
undefined: { open: '', close: '' },
})
const pluginRefs = new Map()
pluginRefs.count = 0
const normalizedPluginThemes = new Map()
function normalizePlugins (plugins) {
if (!Array.isArray(plugins) || plugins.length === 0) return null
const refs = []
const themes = []
for (const fromPlugin of pluginRegistry.getThemes(plugins)) {
if (!pluginRefs.has(fromPlugin.name)) {
pluginRefs.set(fromPlugin.name, pluginRefs.count++)
}
refs.push(pluginRefs.get(fromPlugin.name))
themes.push(fromPlugin.theme)
}
const ref = refs.join('.')
if (normalizedPluginThemes.has(ref)) {
return {
ref,
theme: normalizedPluginThemes.get(ref),
}
}
const theme = freezeTheme(themes.reduce((acc, pluginTheme) => {
return merge(acc, pluginTheme)
}, cloneDeep(defaultTheme)))
normalizedPluginThemes.set(ref, theme)
return { ref, theme }
}
const normalizedCache = new WeakMap()
function normalize (options) {
options = Object.assign({ plugins: [], theme: null }, options)
const normalizedPlugins = normalizePlugins(options.plugins)
if (!options.theme) {
return normalizedPlugins ? normalizedPlugins.theme : defaultTheme
}
const entry = normalizedCache.get(options.theme) || { theme: null, withPlugins: new Map() }
if (!normalizedCache.has(options.theme)) normalizedCache.set(options.theme, entry)
if (normalizedPlugins) {
if (entry.withPlugins.has(normalizedPlugins.ref)) {
return entry.withPlugins.get(normalizedPlugins.ref)
}
const theme = freezeTheme(merge(cloneDeep(normalizedPlugins.theme), options.theme))
entry.withPlugins.set(normalizedPlugins.ref, theme)
return theme
}
if (!entry.theme) {
entry.theme = freezeTheme(merge(cloneDeep(defaultTheme), options.theme))
}
return entry.theme
}
exports.normalize = normalize
const modifiers = new WeakMap()
function addModifier (descriptor, modifier) {
if (modifiers.has(descriptor)) {
modifiers.get(descriptor).add(modifier)
} else {
modifiers.set(descriptor, new Set([modifier]))
}
}
exports.addModifier = addModifier
const modifierCache = new WeakMap()
const originalCache = new WeakMap()
function applyModifiers (descriptor, theme) {
if (!modifiers.has(descriptor)) return theme
return Array.from(modifiers.get(descriptor)).reduce((prev, modifier) => {
const cache = modifierCache.get(modifier) || new WeakMap()
if (!modifierCache.has(modifier)) modifierCache.set(modifier, cache)
if (cache.has(prev)) return cache.get(prev)
const modifiedTheme = cloneDeep(prev)
modifier(modifiedTheme)
freezeTheme(modifiedTheme)
cache.set(prev, modifiedTheme)
originalCache.set(modifiedTheme, theme)
return modifiedTheme
}, theme)
}
exports.applyModifiers = applyModifiers
function applyModifiersToOriginal (descriptor, theme) {
return applyModifiers(descriptor, originalCache.get(theme) || theme)
}
exports.applyModifiersToOriginal = applyModifiersToOriginal

View file

@ -0,0 +1,50 @@
{
"name": "concordance",
"version": "5.0.4",
"description": "Compare, format, diff and serialize any JavaScript value",
"main": "index.js",
"files": [
"lib",
"index.js"
],
"engines": {
"node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
},
"scripts": {
"test": "as-i-preach && c8 ava"
},
"repository": {
"type": "git",
"url": "git+https://github.com/concordancejs/concordance.git"
},
"author": "Mark Wubben (https://novemberborn.net/)",
"license": "ISC",
"bugs": {
"url": "https://github.com/concordancejs/concordance/issues"
},
"homepage": "https://github.com/concordancejs/concordance#readme",
"dependencies": {
"date-time": "^3.1.0",
"esutils": "^2.0.3",
"fast-diff": "^1.2.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.15",
"md5-hex": "^3.0.1",
"semver": "^7.3.2",
"well-known-symbols": "^2.0.0"
},
"devDependencies": {
"@novemberborn/eslint-plugin-as-i-preach": "^12.0.0",
"ava": "^3.15.0",
"c8": "^7.1.2",
"eslint": "^6.8.0",
"eslint-plugin-ava": "^10.3.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-standard": "^4.0.1",
"eslint-plugin-unicorn": "^17.2.0",
"proxyquire": "^2.1.3"
}
}