Skip to content
Permalink
9bfb9ba527
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
318 lines (263 sloc) 11.2 KB
'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 })