type PhpNullish = null | undefined
type PhpInput = {} | PhpNullish
type PhpCallableArgs = PhpInput[]
type PhpCallable<TArgs extends PhpCallableArgs = PhpCallableArgs, TResult = PhpInput> = ( ...args: TArgs ) => TResult
type PhpObjectEntryContainer = object | PhpCallable
function getPhpObjectEntry(value: PhpInput, key: string): PhpInput | undefined { if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { return undefined }
let current: object | null = value while (current) { const descriptor = Object.getOwnPropertyDescriptor(current, key) if (descriptor) { if (typeof descriptor.get === 'function') { const getterValue = descriptor.get.call(value) return typeof getterValue === 'undefined' ? undefined : getterValue } const directValue = descriptor.value return typeof directValue === 'undefined' ? undefined : directValue } current = Object.getPrototypeOf(current) }
return undefined }
function setPhpObjectEntry(value: PhpInput, key: string, entry: PhpInput): boolean { if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { return false }
const container: PhpObjectEntryContainer = value let current: object | null = container while (current) { const descriptor = Object.getOwnPropertyDescriptor(current, key) if (descriptor) { if (typeof descriptor.set === 'function') { descriptor.set.call(container, entry) return true } if (descriptor.writable) { Object.defineProperty(container, key, { configurable: descriptor.configurable ?? true, enumerable: descriptor.enumerable ?? true, value: entry, writable: true, }) return true } return false } current = Object.getPrototypeOf(current) }
Object.defineProperty(container, key, { configurable: true, enumerable: true, value: entry, writable: true, }) return true }
type XRegExpMeta = { source: string captureNames?: string[] | null }
type PatchErrorObject = { value?: string }
function xdiff_string_patch( originalStr: string, patch: string, flags?: number | string | string[], errorObj?: PatchErrorObject, ): string | false {
const isRecord = (value: PhpInput): value is { [key: string]: PhpInput } => typeof value === 'object' && value !== null && !Array.isArray(value)
const _getNativeFlags = function (regex: RegExp): string { const extended = getPhpObjectEntry(regex, 'extended') === true const sticky = getPhpObjectEntry(regex, 'sticky') === true return [ regex.global ? 'g' : '', regex.ignoreCase ? 'i' : '', regex.multiline ? 'm' : '', extended ? 'x' : '', sticky ? 'y' : '', ].join('') }
const getXRegExpMeta = (regex: RegExp): XRegExpMeta | undefined => { const xregexpValue = getPhpObjectEntry(regex, '_xregexp') if (!isRecord(xregexpValue)) { return undefined }
const sourceValue = getPhpObjectEntry(xregexpValue, 'source') if (typeof sourceValue !== 'string') { return undefined }
const captureNamesValue = getPhpObjectEntry(xregexpValue, 'captureNames') let captureNames: string[] | null | undefined if (captureNamesValue === null) { captureNames = null } else if ( Array.isArray(captureNamesValue) && captureNamesValue.every((captureName) => typeof captureName === 'string') ) { captureNames = captureNamesValue }
if (captureNames === undefined) { return { source: sourceValue } }
return { source: sourceValue, captureNames, } }
const _cbSplit = function (input: string, sep: RegExp | string): string[] { if (!(sep instanceof RegExp)) { return input.split(sep) } const str = String(input) const output: string[] = [] let lastLastIndex = 0 let match: RegExpExecArray | null = null let lastLength = 0 const limit = Infinity const x = getXRegExpMeta(sep) const s = new RegExp(sep.source, _getNativeFlags(sep) + 'g') if (x) { setPhpObjectEntry(s, '_xregexp', { source: x.source, captureNames: x.captureNames ? x.captureNames.slice(0) : null, }) }
while ((match = s.exec(str))) { if (s.lastIndex > lastLastIndex) { output.push(str.slice(lastLastIndex, match.index))
if (match.length > 1 && match.index < str.length) { const captures = match.slice(1).filter((capture): capture is string => capture !== undefined) Array.prototype.push.apply(output, captures) }
lastLength = match[0].length lastLastIndex = s.lastIndex
if (output.length >= limit) { break } }
if (s.lastIndex === match.index) { s.lastIndex++ } }
if (lastLastIndex === str.length) { if (!s.test('') || lastLength) { output.push('') } } else { output.push(str.slice(lastLastIndex)) }
return output.length > limit ? output.slice(0, limit) : output }
if (!patch) { return false }
let i = 0 let ll = 0 let ranges: RegExpMatchArray | null = null let lastLinePos = 0 let firstChar = '' const rangeExp = /^@@\s+-(\d+),(\d+)\s+\+(\d+),(\d+)\s+@@$/ const lineBreaks = /\r?\n/ const lines = _cbSplit(patch.replace(/(\r?\n)+$/, ''), lineBreaks) const origLines = _cbSplit(originalStr, lineBreaks) const newStrArr: string[] = [] let linePos = 0 const errors = '' let optTemp = 0 const OPTS = { XDIFF_PATCH_NORMAL: 1, XDIFF_PATCH_REVERSE: 2, XDIFF_PATCH_IGNORESPACE: 4, }
if (!flags) { flags = 'XDIFF_PATCH_NORMAL' }
if (typeof flags !== 'number') { const flagList = Array.isArray(flags) ? flags : [flags] const isPatchOption = (value: string): value is keyof typeof OPTS => value in OPTS for (i = 0; i < flagList.length; i++) { const currentFlag = flagList[i] if (!currentFlag) { continue } if (isPatchOption(currentFlag)) { optTemp = optTemp | OPTS[currentFlag] } } flags = optTemp }
if (flags & OPTS.XDIFF_PATCH_NORMAL) { for (i = 0, ll = lines.length; i < ll; i++) { const line = lines[i] ?? '' ranges = line.match(rangeExp) if (ranges?.[1]) { lastLinePos = linePos linePos = Number(ranges[1]) - 1 while (lastLinePos < linePos) { newStrArr.push(origLines[lastLinePos++] ?? '') } while (lines[++i] !== undefined && rangeExp.exec(lines[i] ?? '') === null) { const patchedLine = lines[i] ?? '' firstChar = patchedLine.charAt(0) switch (firstChar) { case '-': ++linePos break case '+': newStrArr.push(patchedLine.slice(1)) break case ' ': newStrArr.push(origLines[linePos++] ?? '') break default: throw new Error('Unrecognized initial character in unidiff line') } } if (lines[i]) { i-- } } } while (linePos > 0 && linePos < origLines.length) { newStrArr.push(origLines[linePos++] ?? '') } } else if (flags & OPTS.XDIFF_PATCH_REVERSE) { for (i = 0, ll = lines.length; i < ll; i++) { const line = lines[i] ?? '' ranges = line.match(rangeExp) if (ranges?.[3]) { lastLinePos = linePos linePos = Number(ranges[3]) - 1 while (lastLinePos < linePos) { newStrArr.push(origLines[lastLinePos++] ?? '') } while (lines[++i] !== undefined && rangeExp.exec(lines[i] ?? '') === null) { const patchedLine = lines[i] ?? '' firstChar = patchedLine.charAt(0) switch (firstChar) { case '-': newStrArr.push(patchedLine.slice(1)) break case '+': ++linePos break case ' ': newStrArr.push(origLines[linePos++] ?? '') break default: throw new Error('Unrecognized initial character in unidiff line') } } if (lines[i]) { i-- } } } while (linePos > 0 && linePos < origLines.length) { newStrArr.push(origLines[linePos++] ?? '') } }
if (errorObj) { errorObj.value = errors }
return newStrArr.join('\n') }
|