<commit_before>import leven from 'leven';
import {diffChars, diffWordsWithSpace} from 'diff';
import findLastIndex from 'lodash.findlastindex';

const first = array => array[0];

const last = array => array[array.length - 1];

export const computeOldLineNumber = ({isNormal, isInsert, lineNumber, oldLineNumber}) => {
    if (isInsert) {
        return -1;
    }

    return isNormal ? oldLineNumber : lineNumber;
};

export const computeNewLineNumber = ({isNormal, isDelete, lineNumber, newLineNumber}) => {
    if (isDelete) {
        return -1;
    }

    return isNormal ? newLineNumber : lineNumber;
};

const getOldRangeFromHunk = ({oldStart, oldLines}) => [oldStart, oldStart + oldLines - 1];

const createHunkFromChanges = changes => {
    if (!changes.length) {
        return null;
    }

    const initial = {
        isTextHunk: true,
        content: '',
        oldStart: -1,
        oldLines: 0,
        newStart: -1,
        newLines: 0
    };
    const hunk = changes.reduce(
        (hunk, change) => {
            if (!change.isNormal) {
                hunk.isTextHunk = false;
            }

            if (!change.isInsert) {
                hunk.oldLines = hunk.oldLines + 1;

                if (hunk.oldStart === -1) {
                    hunk.oldStart = computeOldLineNumber(change);
                }
            }

            if (!change.isDelete) {
                hunk.newLines = hunk.newLines + 1;

                if (hunk.newStart === -1) {
                    hunk.newStart = computeNewLineNumber(change);
                }
            }

            return hunk;
        },
        initial
    );
    const {oldStart, oldLines, newStart, newLines} = hunk;

    return {
        ...hunk,
        content: `@@ -${oldStart},${oldLines} +${newStart},${newLines}`,
        changes: changes
    };
};

export const textLinesToHunk = (lines, oldStartLineNumber, newStartLineNumber) => {
    const lineToChange = (line, i) => {
        return {
            type: 'normal',
            isNormal: true,
            oldLineNumber: oldStartLineNumber + i,
            newLineNumber: newStartLineNumber + i,
            content: '' + line
        };
    };
    const changes = lines.map(lineToChange);

    return createHunkFromChanges(changes);
};

const createIsInHunkFunction = (startProperty, linesProperty) => (hunk, lineNumber) => {
    const start = hunk[startProperty];
    const end = start + hunk[linesProperty];

    return lineNumber >= start && lineNumber < end;
};

const createIsBetweenHunksFunction = (startProperty, linesProperty) => (previousHunk, nextHunk, lineNumber) => {
    const start = previousHunk[startProperty] + previousHunk[linesProperty];
    const end = nextHunk[startProperty];

    return lineNumber >= start && lineNumber < end;
};

const createFindContainerHunkFunction = side => {
    const startProperty = side + 'Start';
    const linesProperty = side + 'Lines';
    const isInHunk = createIsInHunkFunction(startProperty, linesProperty);

    return (hunks, lineNumber) => hunks.find(hunk => isInHunk(hunk, lineNumber));
};

const findContainerHunkByOldLineNumber = createFindContainerHunkFunction('old');

const createFindChangeByLineNumberFunction = side => {
    const computeLineNumber = side === 'old' ? computeOldLineNumber : computeNewLineNumber;
    const findContainerHunk = createFindContainerHunkFunction(side);

    return (hunks, lineNumber) => {
        const containerHunk = findContainerHunk(hunks, lineNumber);

        if (!containerHunk) {
            return undefined;
        }

        return containerHunk.changes.find(change => computeLineNumber(change) === lineNumber);
    };
};

export const findChangeByOldLineNumber = createFindChangeByLineNumberFunction('old');

export const findChangeByNewLineNumber = createFindChangeByLineNumberFunction('new');

const createCorrespondingLineNumberComputeFunction = baseSide => {
    const anotherSide = baseSide === 'old' ? 'new' : 'old';
    const baseStart = baseSide + 'Start';
    const baseLines = baseSide + 'Lines';
    const correspondingStart = anotherSide + 'Start';
    const correspondingLines = anotherSide + 'Lines';
    const baseLineNumber = baseSide === 'old' ? computeOldLineNumber : computeNewLineNumber;
    const correspondingLineNumber = baseSide === 'old' ? computeNewLineNumber : computeOldLineNumber;
    const isInHunk = createIsInHunkFunction(baseStart, baseLines);
    const isBetweenHunks = createIsBetweenHunksFunction(baseStart, baseLines);

    return (hunks, lineNumber) => {
        const firstHunk = first(hunks);

        // Before first hunk
        if (lineNumber < firstHunk[baseStart]) {
            const spanFromStart = firstHunk[baseStart] - lineNumber;
            return firstHunk[correspondingStart] - spanFromStart;
        }

        // After last hunk, this can be done in `for` loop, just a quick return path
        const lastHunk = last(hunks);
        if (lastHunk[baseStart] + lastHunk[baseLines] <= lineNumber) {
            const spanFromEnd = lineNumber - lastHunk[baseStart] - lastHunk[baseLines];
            return lastHunk[correspondingStart] + lastHunk[correspondingLines] + spanFromEnd;
        }

        for (let i = 0; i < hunks.length; i++) {
            const currentHunk = hunks[i];
            const nextHunk = hunks[i + 1];

            // Within current hunk
            if (isInHunk(currentHunk, lineNumber)) {
                const changeIndex = currentHunk.changes.findIndex(change => baseLineNumber(change) === lineNumber);
                const change = currentHunk.changes[changeIndex];

                if (change.isNormal) {
                    return correspondingLineNumber(change);
                }

                // For changes of type "insert" and "delete", the sibling change can be the corresponding one,
                // or they can have no corresponding change
                //
                // Git diff always put delete change before insert change
                //
                // Note that `nearbySequences: "zip"` option can affect this function
                const possibleCorrespondingChangeIndex = change.isDelete ? changeIndex + 1 : changeIndex - 1;
                const possibleCorrespondingChange = currentHunk.changes[possibleCorrespondingChangeIndex];

                if (!possibleCorrespondingChange) {
                    return -1;
                }

                const negativeChangeType = change.isInsert ? 'delete' : 'insert';

                return possibleCorrespondingChange.type === negativeChangeType
                    ? correspondingLineNumber(possibleCorrespondingChange)
                    : -1;
            }

            // Between 2 hunks
            if (isBetweenHunks(currentHunk, nextHunk, lineNumber)) {
                const spanFromEnd = lineNumber - currentHunk[baseStart] - currentHunk[baseLines];
                return currentHunk[correspondingStart] + currentHunk[correspondingLines] + spanFromEnd;
            }
        }

        throw new Error(`Unexpected line position ${lineNumber}`);
    };
};

export const getCorrespondingOldLineNumber = createCorrespondingLineNumberComputeFunction('new');

export const getCorrespondingNewLineNumber = createCorrespondingLineNumberComputeFunction('old');

const sliceHunk = (hunk, startOldLineNumber, endOldLineNumber) => {
    const startIndex = hunk.changes.findIndex(change => computeOldLineNumber(change) >= startOldLineNumber);

    if (startIndex === -1) {
        return null;
    }

    if (endOldLineNumber === undefined) {
        const slicedChanges = hunk.changes.slice(startIndex);

        return createHunkFromChanges(slicedChanges);
    }

    const endIndex = findLastIndex(hunk.changes, change => computeOldLineNumber(change) <= endOldLineNumber);
    const slicedChanges = hunk.changes.slice(startIndex, endIndex === -1 ? undefined : endIndex);

    return createHunkFromChanges(slicedChanges);
};

const mergeHunk = (previousHunk, nextHunk) => {
    if (!previousHunk) {
        return nextHunk;
    }

    if (!nextHunk) {
        return previousHunk;
    }

    const [previousStart, previousEnd] = getOldRangeFromHunk(previousHunk);
    const [nextStart, nextEnd] = getOldRangeFromHunk(nextHunk);

    // They are just neighboring, simply concat changes and adjust lines count
    if (previousEnd + 1 === nextStart) {
        return {
            ...previousHunk,
            oldLines: previousHunk.oldLines + nextHunk.oldLines,
            newLines: previousHunk.newLines + nextHunk.newLines,
            changes: [...previousHunk.changes, ...nextHunk.changes]
        };
    }

    // It is possible that `previousHunk` entirely **contains** `nextHunk`,
    // and if `nextHunk` is not a fake one, we need to replace `nextHunk`'s corresponding range
    if (previousStart <= nextStart && previousEnd >= nextEnd && !nextHunk.isTextHunk) {
        const head = sliceHunk(previousHunk, previousStart, nextStart);
        const tail = sliceHunk(previousHunk, nextEnd + 1);
        return mergeHunk(mergeHunk(head, nextHunk), tail);
    }

    // The 2 hunks have some overlapping, we need to slice the fake one in order to preserve non-normal changes
    if (previousHunk.isTextHunk) {
        const head = sliceHunk(previousHunk, previousStart, nextStart);
        return mergeHunk(head, nextHunk);
    }

    const tail = sliceHunk(nextHunk, previousEnd + 1);
    return mergeHunk(previousHunk, tail);
};

const appendOrMergeHunk = (hunks, nextHunk) => {
    const lastHunk = last(hunks);

    if (!lastHunk) {
        return [nextHunk];
    }

    const expectedNextStart = lastHunk.oldStart + lastHunk.oldLines;
    const actualNextStart = nextHunk.oldStart;

    if (expectedNextStart < actualNextStart) {
        return hunks.concat(nextHunk);
    }

    const mergedHunk = mergeHunk(lastHunk, nextHunk);

    return [...hunks.slice(0, -1), mergedHunk];
};

export const insertHunk = (hunks, insertion) => {
    const insertionOldLineNumber = computeOldLineNumber(insertion.changes[0]);
    const insertPosition = hunks.findIndex(hunk => computeOldLineNumber(hunk.changes[0]) >= insertionOldLineNumber);
    const hunksWithInsertion = insertPosition === -1
        ? hunks.concat(insertion)
        : [
            ...hunks.slice(0, insertPosition),
            insertion,
            ...hunks.slice(insertPosition)
        ];

    return hunksWithInsertion.reduce(appendOrMergeHunk, []);
};

const adjustStartToNormalChange = (hunks, start) => {
    const hunk = findContainerHunkByOldLineNumber(hunks, start);

    if (!hunk) {
        return start;
    }

    const {changes} = hunk;
    const index = changes.findIndex(change => computeOldLineNumber(change) === start);

    for (let i = index; i < changes.length; i++) {
        const change = changes[i];

        if (change.isNormal) {
            return computeOldLineNumber(change);
        }
    }

    throw new Error(`There is no nearby normal change after line ${start} in hunks`);
};

export const expandFromRawCode = (hunks, rawCodeOrLines, start, end) => {
    // It is not possible to  slice from a normal change since there is no corresponding new line number.
    //
    // In this case, we need to find the nearest subsequent normal change and start from there.
    //
    // The issue does not apply to `end` because we need only a `newStart` property in hunk object.
    const adjustedStart = adjustStartToNormalChange(hunks, start);

    // Note `end` is not inclusive, this is the same as `Array.prototype.slice` method
    const linesOfCode = typeof rawCodeOrLines === 'string' ? rawCodeOrLines.split('\n') : rawCodeOrLines;
    const slicedLines = linesOfCode.slice(Math.max(adjustedStart, 1) - 1, end - 1);

    if (!slicedLines.length) {
        return hunks;
    }

    const slicedHunk = textLinesToHunk(slicedLines, adjustedStart, getCorrespondingNewLineNumber(hunks, adjustedStart));
    return insertHunk(hunks, slicedHunk);
};

export const getCollapsedLinesCountBetween = (previousHunk, nextHunk) => {
    if (!previousHunk) {
        return nextHunk.oldStart - 1;
    }

    if (!nextHunk) {
        throw new Error('Unable to compute lines count after the last hunk');
    }

    const previousEnd = previousHunk.oldStart + previousHunk.oldLines;
    const nextStart = nextHunk.oldStart;

    return nextStart - previousEnd;
};

export const expandCollapsedBlockBy = (hunks, rawCodeOrLines, predicate) => {
    const linesOfCode = typeof rawCodeOrLines === 'string' ? rawCodeOrLines.split('\n') : rawCodeOrLines;
    const firstHunk = first(hunks);
    const initialExpandingBlocks = predicate(firstHunk.oldStart - 1, 1, 1) ? [[1, firstHunk.oldStart]] : [];

    const expandingBlocks = hunks.reduce(
        (expandingBlocks, currentHunk, index, hunks) => {
            const nextHunk = hunks[index + 1];
            const oldStart = currentHunk.oldStart + currentHunk.oldLines;
            const newStart = currentHunk.newStart + currentHunk.newLines;
            const lines = nextHunk
                ? getCollapsedLinesCountBetween(currentHunk, nextHunk)
                : linesOfCode.length - oldStart + 1;
            const shouldExpand = predicate(lines, oldStart, newStart);

            return shouldExpand ? [...expandingBlocks, [oldStart, oldStart + lines]] : expandingBlocks;
        },
        initialExpandingBlocks
    );

    return expandingBlocks.reduce((hunks, [start, end]) => expandFromRawCode(hunks, linesOfCode, start, end), hunks);
};

export const getChangeKey = hunk => {
    if (!hunk) {
        throw new Error('hunk is not provided');
    }

    const {isNormal, isInsert, lineNumber, oldLineNumber} = hunk;

    if (isNormal) {
        return 'N' + oldLineNumber;
    }

    const prefix = isInsert ? 'I' : 'D';
    return prefix + lineNumber;
};

const NO_EDITS = [[], []];

const markEditsBy = computeDiff => (options = {}) => {
    const {threshold = Infinity, markLongDistanceDiff = false} = options;

    return (oldChange, newChange) => {
        if (!oldChange || !newChange) {
            return NO_EDITS;
        }

        const oldContent = oldChange.content;
        const newContent = newChange.content;

        // Precheck `threshold !== Infinity` to reduce calls to `leven`
        if (threshold !== Infinity && leven(oldContent, newContent) > threshold) {
            // Mark the whole line as an edit to highlight "this line is completely changed"
            if (markLongDistanceDiff) {
                return [
                    [[0, oldContent.length]],
                    [[0, newContent.length]]
                ];
            }

            return NO_EDITS;
        }

        const diff = computeDiff(oldContent, newContent);
        const {aEdits, bEdits} = diff.reduce(
            (result, {added, removed, value}) => {
                if (!added && !removed) {
                    result.aIndex += value.length;
                    result.bIndex += value.length;

                    return result;
                }

                if (added) {
                    result.bEdits.push([result.bIndex, value.length]);
                    result.bIndex += value.length;

                    return result;
                }

                result.aEdits.push([result.aIndex, value.length]);
                result.aIndex += value.length;

                return result;
            },
            {aEdits: [], bEdits: [], aIndex: 0, bIndex: 0}
        );

        return [aEdits, bEdits];
    };
};

export const markWordEdits = markEditsBy(diffWordsWithSpace);

export const markCharacterEdits = markEditsBy(diffChars);
<commit_msg>Fix a bug where isTextHunk property get the wrong value when merged<commit_after>import leven from 'leven';
import {diffChars, diffWordsWithSpace} from 'diff';
import findLastIndex from 'lodash.findlastindex';

const first = array => array[0];

const last = array => array[array.length - 1];

export const computeOldLineNumber = ({isNormal, isInsert, lineNumber, oldLineNumber}) => {
    if (isInsert) {
        return -1;
    }

    return isNormal ? oldLineNumber : lineNumber;
};

export const computeNewLineNumber = ({isNormal, isDelete, lineNumber, newLineNumber}) => {
    if (isDelete) {
        return -1;
    }

    return isNormal ? newLineNumber : lineNumber;
};

const getOldRangeFromHunk = ({oldStart, oldLines}) => [oldStart, oldStart + oldLines - 1];

const createHunkFromChanges = changes => {
    if (!changes.length) {
        return null;
    }

    const initial = {
        isTextHunk: true,
        content: '',
        oldStart: -1,
        oldLines: 0,
        newStart: -1,
        newLines: 0
    };
    const hunk = changes.reduce(
        (hunk, change) => {
            if (!change.isNormal) {
                hunk.isTextHunk = false;
            }

            if (!change.isInsert) {
                hunk.oldLines = hunk.oldLines + 1;

                if (hunk.oldStart === -1) {
                    hunk.oldStart = computeOldLineNumber(change);
                }
            }

            if (!change.isDelete) {
                hunk.newLines = hunk.newLines + 1;

                if (hunk.newStart === -1) {
                    hunk.newStart = computeNewLineNumber(change);
                }
            }

            return hunk;
        },
        initial
    );
    const {oldStart, oldLines, newStart, newLines} = hunk;

    return {
        ...hunk,
        content: `@@ -${oldStart},${oldLines} +${newStart},${newLines}`,
        changes: changes
    };
};

export const textLinesToHunk = (lines, oldStartLineNumber, newStartLineNumber) => {
    const lineToChange = (line, i) => {
        return {
            type: 'normal',
            isNormal: true,
            oldLineNumber: oldStartLineNumber + i,
            newLineNumber: newStartLineNumber + i,
            content: '' + line
        };
    };
    const changes = lines.map(lineToChange);

    return createHunkFromChanges(changes);
};

const createIsInHunkFunction = (startProperty, linesProperty) => (hunk, lineNumber) => {
    const start = hunk[startProperty];
    const end = start + hunk[linesProperty];

    return lineNumber >= start && lineNumber < end;
};

const createIsBetweenHunksFunction = (startProperty, linesProperty) => (previousHunk, nextHunk, lineNumber) => {
    const start = previousHunk[startProperty] + previousHunk[linesProperty];
    const end = nextHunk[startProperty];

    return lineNumber >= start && lineNumber < end;
};

const createFindContainerHunkFunction = side => {
    const startProperty = side + 'Start';
    const linesProperty = side + 'Lines';
    const isInHunk = createIsInHunkFunction(startProperty, linesProperty);

    return (hunks, lineNumber) => hunks.find(hunk => isInHunk(hunk, lineNumber));
};

const findContainerHunkByOldLineNumber = createFindContainerHunkFunction('old');

const createFindChangeByLineNumberFunction = side => {
    const computeLineNumber = side === 'old' ? computeOldLineNumber : computeNewLineNumber;
    const findContainerHunk = createFindContainerHunkFunction(side);

    return (hunks, lineNumber) => {
        const containerHunk = findContainerHunk(hunks, lineNumber);

        if (!containerHunk) {
            return undefined;
        }

        return containerHunk.changes.find(change => computeLineNumber(change) === lineNumber);
    };
};

export const findChangeByOldLineNumber = createFindChangeByLineNumberFunction('old');

export const findChangeByNewLineNumber = createFindChangeByLineNumberFunction('new');

const createCorrespondingLineNumberComputeFunction = baseSide => {
    const anotherSide = baseSide === 'old' ? 'new' : 'old';
    const baseStart = baseSide + 'Start';
    const baseLines = baseSide + 'Lines';
    const correspondingStart = anotherSide + 'Start';
    const correspondingLines = anotherSide + 'Lines';
    const baseLineNumber = baseSide === 'old' ? computeOldLineNumber : computeNewLineNumber;
    const correspondingLineNumber = baseSide === 'old' ? computeNewLineNumber : computeOldLineNumber;
    const isInHunk = createIsInHunkFunction(baseStart, baseLines);
    const isBetweenHunks = createIsBetweenHunksFunction(baseStart, baseLines);

    return (hunks, lineNumber) => {
        const firstHunk = first(hunks);

        // Before first hunk
        if (lineNumber < firstHunk[baseStart]) {
            const spanFromStart = firstHunk[baseStart] - lineNumber;
            return firstHunk[correspondingStart] - spanFromStart;
        }

        // After last hunk, this can be done in `for` loop, just a quick return path
        const lastHunk = last(hunks);
        if (lastHunk[baseStart] + lastHunk[baseLines] <= lineNumber) {
            const spanFromEnd = lineNumber - lastHunk[baseStart] - lastHunk[baseLines];
            return lastHunk[correspondingStart] + lastHunk[correspondingLines] + spanFromEnd;
        }

        for (let i = 0; i < hunks.length; i++) {
            const currentHunk = hunks[i];
            const nextHunk = hunks[i + 1];

            // Within current hunk
            if (isInHunk(currentHunk, lineNumber)) {
                const changeIndex = currentHunk.changes.findIndex(change => baseLineNumber(change) === lineNumber);
                const change = currentHunk.changes[changeIndex];

                if (change.isNormal) {
                    return correspondingLineNumber(change);
                }

                // For changes of type "insert" and "delete", the sibling change can be the corresponding one,
                // or they can have no corresponding change
                //
                // Git diff always put delete change before insert change
                //
                // Note that `nearbySequences: "zip"` option can affect this function
                const possibleCorrespondingChangeIndex = change.isDelete ? changeIndex + 1 : changeIndex - 1;
                const possibleCorrespondingChange = currentHunk.changes[possibleCorrespondingChangeIndex];

                if (!possibleCorrespondingChange) {
                    return -1;
                }

                const negativeChangeType = change.isInsert ? 'delete' : 'insert';

                return possibleCorrespondingChange.type === negativeChangeType
                    ? correspondingLineNumber(possibleCorrespondingChange)
                    : -1;
            }

            // Between 2 hunks
            if (isBetweenHunks(currentHunk, nextHunk, lineNumber)) {
                const spanFromEnd = lineNumber - currentHunk[baseStart] - currentHunk[baseLines];
                return currentHunk[correspondingStart] + currentHunk[correspondingLines] + spanFromEnd;
            }
        }

        throw new Error(`Unexpected line position ${lineNumber}`);
    };
};

export const getCorrespondingOldLineNumber = createCorrespondingLineNumberComputeFunction('new');

export const getCorrespondingNewLineNumber = createCorrespondingLineNumberComputeFunction('old');

const sliceHunk = (hunk, startOldLineNumber, endOldLineNumber) => {
    const startIndex = hunk.changes.findIndex(change => computeOldLineNumber(change) >= startOldLineNumber);

    if (startIndex === -1) {
        return null;
    }

    if (endOldLineNumber === undefined) {
        const slicedChanges = hunk.changes.slice(startIndex);

        return createHunkFromChanges(slicedChanges);
    }

    const endIndex = findLastIndex(hunk.changes, change => computeOldLineNumber(change) <= endOldLineNumber);
    const slicedChanges = hunk.changes.slice(startIndex, endIndex === -1 ? undefined : endIndex);

    return createHunkFromChanges(slicedChanges);
};

const mergeHunk = (previousHunk, nextHunk) => {
    if (!previousHunk) {
        return nextHunk;
    }

    if (!nextHunk) {
        return previousHunk;
    }

    const [previousStart, previousEnd] = getOldRangeFromHunk(previousHunk);
    const [nextStart, nextEnd] = getOldRangeFromHunk(nextHunk);

    // They are just neighboring, simply concat changes and adjust lines count
    if (previousEnd + 1 === nextStart) {
        return {
            ...previousHunk,
            isTextHunk: previousHunk.isTextHunk && nextHunk.isTextHunk,
            oldLines: previousHunk.oldLines + nextHunk.oldLines,
            newLines: previousHunk.newLines + nextHunk.newLines,
            changes: [...previousHunk.changes, ...nextHunk.changes]
        };
    }

    // It is possible that `previousHunk` entirely **contains** `nextHunk`,
    // and if `nextHunk` is not a fake one, we need to replace `nextHunk`'s corresponding range
    if (previousStart <= nextStart && previousEnd >= nextEnd) {
        if (nextHunk.isTextHunk) {
            return previousHunk;
        }

        const head = sliceHunk(previousHunk, previousStart, nextStart);
        const tail = sliceHunk(previousHunk, nextEnd + 1);
        return mergeHunk(mergeHunk(head, nextHunk), tail);
    }

    // The 2 hunks have some overlapping, we need to slice the fake one in order to preserve non-normal changes
    if (previousHunk.isTextHunk) {
        const head = sliceHunk(previousHunk, previousStart, nextStart);
        return mergeHunk(head, nextHunk);
    }

    const tail = sliceHunk(nextHunk, previousEnd + 1);
    return mergeHunk(previousHunk, tail);
};

const appendOrMergeHunk = (hunks, nextHunk) => {
    const lastHunk = last(hunks);

    if (!lastHunk) {
        return [nextHunk];
    }

    const expectedNextStart = lastHunk.oldStart + lastHunk.oldLines;
    const actualNextStart = nextHunk.oldStart;

    if (expectedNextStart < actualNextStart) {
        return hunks.concat(nextHunk);
    }

    const mergedHunk = mergeHunk(lastHunk, nextHunk);

    return [...hunks.slice(0, -1), mergedHunk];
};

export const insertHunk = (hunks, insertion) => {
    const insertionOldLineNumber = computeOldLineNumber(insertion.changes[0]);
    const insertPosition = hunks.findIndex(hunk => computeOldLineNumber(hunk.changes[0]) >= insertionOldLineNumber);
    const hunksWithInsertion = insertPosition === -1
        ? hunks.concat(insertion)
        : [
            ...hunks.slice(0, insertPosition),
            insertion,
            ...hunks.slice(insertPosition)
        ];

    return hunksWithInsertion.reduce(appendOrMergeHunk, []);
};

const adjustStartToNormalChange = (hunks, start) => {
    const hunk = findContainerHunkByOldLineNumber(hunks, start);

    if (!hunk) {
        return start;
    }

    const {changes} = hunk;
    const index = changes.findIndex(change => computeOldLineNumber(change) === start);

    for (let i = index; i < changes.length; i++) {
        const change = changes[i];

        if (change.isNormal) {
            return computeOldLineNumber(change);
        }
    }

    throw new Error(`There is no nearby normal change after line ${start} in hunks`);
};

export const expandFromRawCode = (hunks, rawCodeOrLines, start, end) => {
    // It is not possible to  slice from a normal change since there is no corresponding new line number.
    //
    // In this case, we need to find the nearest subsequent normal change and start from there.
    //
    // The issue does not apply to `end` because we need only a `newStart` property in hunk object.
    const adjustedStart = adjustStartToNormalChange(hunks, start);

    // Note `end` is not inclusive, this is the same as `Array.prototype.slice` method
    const linesOfCode = typeof rawCodeOrLines === 'string' ? rawCodeOrLines.split('\n') : rawCodeOrLines;
    const slicedLines = linesOfCode.slice(Math.max(adjustedStart, 1) - 1, end - 1);

    if (!slicedLines.length) {
        return hunks;
    }

    const slicedHunk = textLinesToHunk(slicedLines, adjustedStart, getCorrespondingNewLineNumber(hunks, adjustedStart));
    return insertHunk(hunks, slicedHunk);
};

export const getCollapsedLinesCountBetween = (previousHunk, nextHunk) => {
    if (!previousHunk) {
        return nextHunk.oldStart - 1;
    }

    if (!nextHunk) {
        throw new Error('Unable to compute lines count after the last hunk');
    }

    const previousEnd = previousHunk.oldStart + previousHunk.oldLines;
    const nextStart = nextHunk.oldStart;

    return nextStart - previousEnd;
};

export const expandCollapsedBlockBy = (hunks, rawCodeOrLines, predicate) => {
    const linesOfCode = typeof rawCodeOrLines === 'string' ? rawCodeOrLines.split('\n') : rawCodeOrLines;
    const firstHunk = first(hunks);
    const initialExpandingBlocks = predicate(firstHunk.oldStart - 1, 1, 1) ? [[1, firstHunk.oldStart]] : [];

    const expandingBlocks = hunks.reduce(
        (expandingBlocks, currentHunk, index, hunks) => {
            const nextHunk = hunks[index + 1];
            const oldStart = currentHunk.oldStart + currentHunk.oldLines;
            const newStart = currentHunk.newStart + currentHunk.newLines;
            const lines = nextHunk
                ? getCollapsedLinesCountBetween(currentHunk, nextHunk)
                : linesOfCode.length - oldStart + 1;
            const shouldExpand = predicate(lines, oldStart, newStart);

            return shouldExpand ? [...expandingBlocks, [oldStart, oldStart + lines]] : expandingBlocks;
        },
        initialExpandingBlocks
    );

    return expandingBlocks.reduce((hunks, [start, end]) => expandFromRawCode(hunks, linesOfCode, start, end), hunks);
};

export const getChangeKey = hunk => {
    if (!hunk) {
        throw new Error('hunk is not provided');
    }

    const {isNormal, isInsert, lineNumber, oldLineNumber} = hunk;

    if (isNormal) {
        return 'N' + oldLineNumber;
    }

    const prefix = isInsert ? 'I' : 'D';
    return prefix + lineNumber;
};

const NO_EDITS = [[], []];

const markEditsBy = computeDiff => (options = {}) => {
    const {threshold = Infinity, markLongDistanceDiff = false} = options;

    return (oldChange, newChange) => {
        if (!oldChange || !newChange) {
            return NO_EDITS;
        }

        const oldContent = oldChange.content;
        const newContent = newChange.content;

        // Precheck `threshold !== Infinity` to reduce calls to `leven`
        if (threshold !== Infinity && leven(oldContent, newContent) > threshold) {
            // Mark the whole line as an edit to highlight "this line is completely changed"
            if (markLongDistanceDiff) {
                return [
                    [[0, oldContent.length]],
                    [[0, newContent.length]]
                ];
            }

            return NO_EDITS;
        }

        const diff = computeDiff(oldContent, newContent);
        const {aEdits, bEdits} = diff.reduce(
            (result, {added, removed, value}) => {
                if (!added && !removed) {
                    result.aIndex += value.length;
                    result.bIndex += value.length;

                    return result;
                }

                if (added) {
                    result.bEdits.push([result.bIndex, value.length]);
                    result.bIndex += value.length;

                    return result;
                }

                result.aEdits.push([result.aIndex, value.length]);
                result.aIndex += value.length;

                return result;
            },
            {aEdits: [], bEdits: [], aIndex: 0, bIndex: 0}
        );

        return [aEdits, bEdits];
    };
};

export const markWordEdits = markEditsBy(diffWordsWithSpace);

export const markCharacterEdits = markEditsBy(diffChars);
<|endoftext|>


<commit_before>export function trace(clientId: ?string, title: string, value: Object | string) {
  let prefix = '';
  if (window.performance) {
    prefix = '[' + (window.performance.now() / 1000).toFixed(3) + ']';
  }
  if (clientId) {
    prefix = prefix + '[' + clientId + ']';
  }

  if (isEdge()) {
    console.log(prefix + ' ' + title + '\n', value); // eslint-disable-line
  }
  else {
    console.info(prefix + ' ' + title + '\n', value); // eslint-disable-line
  }
}

function browser() {
  const ua = window.navigator.userAgent.toLocaleLowerCase();
  if (ua.indexOf('edge') !== -1) {
    return 'edge';
  }
  else if (ua.indexOf('chrome')  !== -1 && ua.indexOf('edge') === -1) {
    return 'chrome';
  }
  else if (ua.indexOf('safari')  !== -1 && ua.indexOf('chrome') === -1) {
    return 'safari';
  }
  else if (ua.indexOf('opera')   !== -1) {
    return 'opera';
  }
  else if (ua.indexOf('firefox') !== -1) {
    return 'firefox';
  }
  return ;
}

function isPlanB() {
  return browser() === 'chrome' || browser() === 'safari';
}

export function isUnifiedChrome() {
  if (browser() !== 'chrome') {
    return false;
  }
  const ua = window.navigator.userAgent.toLocaleLowerCase();
  const splitedUserAgent = /chrome\/([\d.]+)/.exec(ua);
  if (!splitedUserAgent || splitedUserAgent.length < 2) {
    return false;
  }
  return 71 <= parseInt(splitedUserAgent[1]);
}

export function isEdge() {
  return browser() === 'edge';
}

export function isSafari() {
  return browser() === 'safari';
}

export function createSignalingMessage(offerSDP, role, channelId, metadata, options) {
  const message = {
    type: 'connect',
    role: role,
    channel_id: channelId,
    metadata: metadata,
    sdp: offerSDP,
    userAgent: window.navigator.userAgent,
    audio: true,
    video: true
  };
  Object.keys(message).forEach(key => {
    if (message[key] === undefined) {
      message[key] = null;
    }
  });
  // multistream
  if ('multistream' in options && options.multistream === true) {
    message.multistream = true;
    if (!isUnifiedChrome() && isPlanB()) {
      message.plan_b = true;
    }
  }
  // spotlight
  if ('spotlight' in options) {
    message.spotlight = options.spotlight;
  }
  // parse options
  const audioPropertyKeys = ['audioCodecType', 'audioBitRate'];
  const videoPropertyKeys = ['videoCodecType', 'videoBitRate'];
  const copyOptions = Object.assign({}, options);
  Object.keys(copyOptions).forEach(key => {
    if (key === 'audio' && typeof copyOptions[key] === 'boolean') return;
    if (key === 'video' && typeof copyOptions[key] === 'boolean') return;
    if (0 <= audioPropertyKeys.indexOf(key) && copyOptions[key] !== null) return;
    if (0 <= videoPropertyKeys.indexOf(key) && copyOptions[key] !== null) return;
    delete copyOptions[key];
  });

  if ('audio' in copyOptions) {
    message.audio = copyOptions.audio;
  }
  const hasAudioProperty = Object.keys(copyOptions).some(key => {
    return 0 <= audioPropertyKeys.indexOf(key);
  });
  if (message.audio && hasAudioProperty) {
    message.audio = {};
    if ('audioCodecType' in copyOptions) {
      message.audio['codec_type'] = copyOptions.audioCodecType;
    }
    if ('audioBitRate' in copyOptions) {
      message.audio['bit_rate'] = copyOptions.audioBitRate;
    }
  }

  if ('video' in copyOptions) {
    message.video = copyOptions.video;
  }
  const hasVideoProperty = Object.keys(copyOptions).some(key => {
    return 0 <= videoPropertyKeys.indexOf(key);
  });
  if (message.video && hasVideoProperty) {
    message.video = {};
    if ('videoCodecType' in copyOptions) {
      message.video['codec_type'] = copyOptions.videoCodecType;
    }
    if ('videoBitRate' in copyOptions) {
      message.video['bit_rate'] = copyOptions.videoBitRate;
    }
  }

  return message;
}
<commit_msg>Unified Plan の Safari 判定を追加する<commit_after>export function trace(clientId: ?string, title: string, value: Object | string) {
  let prefix = '';
  if (window.performance) {
    prefix = '[' + (window.performance.now() / 1000).toFixed(3) + ']';
  }
  if (clientId) {
    prefix = prefix + '[' + clientId + ']';
  }

  if (isEdge()) {
    console.log(prefix + ' ' + title + '\n', value); // eslint-disable-line
  }
  else {
    console.info(prefix + ' ' + title + '\n', value); // eslint-disable-line
  }
}

function browser() {
  const ua = window.navigator.userAgent.toLocaleLowerCase();
  if (ua.indexOf('edge') !== -1) {
    return 'edge';
  }
  else if (ua.indexOf('chrome')  !== -1 && ua.indexOf('edge') === -1) {
    return 'chrome';
  }
  else if (ua.indexOf('safari')  !== -1 && ua.indexOf('chrome') === -1) {
    return 'safari';
  }
  else if (ua.indexOf('opera')   !== -1) {
    return 'opera';
  }
  else if (ua.indexOf('firefox') !== -1) {
    return 'firefox';
  }
  return ;
}

function isPlanB() {
  return browser() === 'chrome' || browser() === 'safari';
}

export function isUnifiedChrome() {
  if (browser() !== 'chrome') {
    return false;
  }
  const ua = window.navigator.userAgent.toLocaleLowerCase();
  const splitedUserAgent = /chrome\/([\d.]+)/.exec(ua);
  if (!splitedUserAgent || splitedUserAgent.length < 2) {
    return false;
  }
  return 71 <= parseInt(splitedUserAgent[1]);
}

export function isUnifiedSafari() {
  if (browser() !== 'safari') {
    return false;
  }
  const appVersion = window.navigator.appVersion.toLowerCase();
  const version = /version\/([\d.]+)/.exec(appVersion).pop();
  return 12.0 < parseFloat(version);
}

export function isEdge() {
  return browser() === 'edge';
}

export function isSafari() {
  return browser() === 'safari';
}

export function createSignalingMessage(offerSDP, role, channelId, metadata, options) {
  const message = {
    type: 'connect',
    role: role,
    channel_id: channelId,
    metadata: metadata,
    sdp: offerSDP,
    userAgent: window.navigator.userAgent,
    audio: true,
    video: true
  };
  Object.keys(message).forEach(key => {
    if (message[key] === undefined) {
      message[key] = null;
    }
  });
  // multistream
  if ('multistream' in options && options.multistream === true) {
    message.multistream = true;
    if (!isUnifiedChrome() && !isUnifiedSafari() && isPlanB()) {
      message.plan_b = true;
    }
  }
  // spotlight
  if ('spotlight' in options) {
    message.spotlight = options.spotlight;
  }
  // parse options
  const audioPropertyKeys = ['audioCodecType', 'audioBitRate'];
  const videoPropertyKeys = ['videoCodecType', 'videoBitRate'];
  const copyOptions = Object.assign({}, options);
  Object.keys(copyOptions).forEach(key => {
    if (key === 'audio' && typeof copyOptions[key] === 'boolean') return;
    if (key === 'video' && typeof copyOptions[key] === 'boolean') return;
    if (0 <= audioPropertyKeys.indexOf(key) && copyOptions[key] !== null) return;
    if (0 <= videoPropertyKeys.indexOf(key) && copyOptions[key] !== null) return;
    delete copyOptions[key];
  });

  if ('audio' in copyOptions) {
    message.audio = copyOptions.audio;
  }
  const hasAudioProperty = Object.keys(copyOptions).some(key => {
    return 0 <= audioPropertyKeys.indexOf(key);
  });
  if (message.audio && hasAudioProperty) {
    message.audio = {};
    if ('audioCodecType' in copyOptions) {
      message.audio['codec_type'] = copyOptions.audioCodecType;
    }
    if ('audioBitRate' in copyOptions) {
      message.audio['bit_rate'] = copyOptions.audioBitRate;
    }
  }

  if ('video' in copyOptions) {
    message.video = copyOptions.video;
  }
  const hasVideoProperty = Object.keys(copyOptions).some(key => {
    return 0 <= videoPropertyKeys.indexOf(key);
  });
  if (message.video && hasVideoProperty) {
    message.video = {};
    if ('videoCodecType' in copyOptions) {
      message.video['codec_type'] = copyOptions.videoCodecType;
    }
    if ('videoBitRate' in copyOptions) {
      message.video['bit_rate'] = copyOptions.videoBitRate;
    }
  }

  return message;
}
<|endoftext|>


<commit_before>import { encode as encodeSourceMap } from 'sourcemap-codec';
import { decode as decodeSourceMap } from 'sourcemap-codec';

function addNewLine( code, map, banner ) {
	map = Object.assign( {}, map );

	code = code.replace( banner, `${ banner }\n` );

	const mappings = decodeSourceMap( map.mappings );

	let codeStart = banner.match( /\n/g );
	codeStart = codeStart ? codeStart.length + 1 : 1;

	let whitespaceAtStart = code.replace( `${ banner }\n`, '' ).match( /^(\s)+/g );
	whitespaceAtStart = whitespaceAtStart ? whitespaceAtStart.length : 0;

	mappings.unshift( [] );

	if ( Array.isArray( mappings[ codeStart ] ) && mappings[ codeStart ].length ) {
		const offset = mappings[ codeStart ][ 0 ][ 0 ] - whitespaceAtStart;

		mappings[ codeStart ].forEach( ( segment ) => {
			segment[ 0 ] -= offset;
		} );
	}

	map.mappings = encodeSourceMap( mappings );

	return {
		code,
		map
	};
}

function filterMinifyOptions( options ) {
	const disallowedProperties = [
		'banner',
		'bannerNewLine',
		'sourceMap',
		'comments',
		'plugins'
	];
	const minifyOptions = {};

	Object.keys( options ).forEach( ( option ) => {
		if ( disallowedProperties.indexOf( option ) === -1 ) {
			minifyOptions[ option ] = options[ option ];
		}
	} );

	return minifyOptions;
}

function isString( v ) {
	return typeof v === 'string';
}

function isFn( v ) {
	return typeof v === 'function';
}

function isFnOrString( v ) {
	return isString( v ) || isFn( v );
}

export { addNewLine };
export { filterMinifyOptions };
export { isString };
export { isFn };
export { isFnOrString };
<commit_msg>refactor(utils): rewrite filterMinifyOptions to reduce<commit_after>import { encode as encodeSourceMap } from 'sourcemap-codec';
import { decode as decodeSourceMap } from 'sourcemap-codec';

function addNewLine( code, map, banner ) {
	map = Object.assign( {}, map );

	code = code.replace( banner, `${ banner }\n` );

	const mappings = decodeSourceMap( map.mappings );

	let codeStart = banner.match( /\n/g );
	codeStart = codeStart ? codeStart.length + 1 : 1;

	let whitespaceAtStart = code.replace( `${ banner }\n`, '' ).match( /^(\s)+/g );
	whitespaceAtStart = whitespaceAtStart ? whitespaceAtStart.length : 0;

	mappings.unshift( [] );

	if ( Array.isArray( mappings[ codeStart ] ) && mappings[ codeStart ].length ) {
		const offset = mappings[ codeStart ][ 0 ][ 0 ] - whitespaceAtStart;

		mappings[ codeStart ].forEach( ( segment ) => {
			segment[ 0 ] -= offset;
		} );
	}

	map.mappings = encodeSourceMap( mappings );

	return {
		code,
		map
	};
}

function filterMinifyOptions( options ) {
	const disallowedProperties = [
		'banner',
		'bannerNewLine',
		'sourceMap',
		'comments',
		'plugins'
	];

	return Object.keys( options ).reduce( ( newOptions, option ) => {
		if ( disallowedProperties.indexOf( option ) === -1 ) {
			newOptions[ option ] = options[ option ];
		}

		return newOptions;
	}, {} );
}

function isString( v ) {
	return typeof v === 'string';
}

function isFn( v ) {
	return typeof v === 'function';
}

function isFnOrString( v ) {
	return isString( v ) || isFn( v );
}

export { addNewLine };
export { filterMinifyOptions };
export { isString };
export { isFn };
export { isFnOrString };
<|endoftext|>


<commit_before>const childProcess = require('child_process')
const fs = require('fs')

const utils = module.exports = {}

utils.labelPullRequestWithMetricMovement = require('./labelPullRequestWithMetricMovement')

utils.version = process.env.npm_package_version
utils.versionTag = 'v' + utils.version
utils.majorVersionTag = utils.versionTag.replace(/\..+/, '-latest')
utils.minorVersionTag = utils.versionTag.replace(/(\d+\.\d+).+/, '$1-latest')
utils.name = process.env.npm_package_name
utils.ownerAndName = `holidayextras/${utils.name}`
utils.distFolder = process.env.DIST_FOLDER || 'dist'
const repo = ('' + process.env.npm_package_repository_url).split('/')
/* istanbul ignore else */
if (repo.pop() === `${utils.name}.git`) {
  utils.ownerAndName = `${repo.pop()}/${utils.name}`
  // console.info('got owner and name', utils.ownerAndName, 'from', process.env.npm_package_repository_url, 'get this from env vars?')
}

utils.getIntegrity = (file, callback) => {
  utils.getSignature(file, (err, signature) => {
    if (err) return callback(err)
    callback(null, 'sha256-' + signature)
  })
}

utils.exec = (cmd, callback) => {
  childProcess.exec(cmd, (err, stdout, stderr) => {
    if (err || stderr) console.warn(cmd, err, stdout, stderr)
    callback(err, stdout)
  })
}

utils.execAndIgnoreOutput = (cmd, callback) => {
  utils.exec(cmd, err => {
    callback(err)
  })
}

utils.createVersionedDistFile = (file, callback) => {
  if (!utils.version) return callback('Version missing - must run this as an npm script')
  const versionedFile = file.replace('.js', `.${utils.version}.js`)
  const cmd = `cp ${file} ${versionedFile}`
  utils.exec(cmd, err => {
    callback(err, versionedFile)
  })
}

utils.getSignature = (file, callback) => {
  const cmd = `cat ${file} | openssl dgst -sha256 -binary | openssl enc -base64 -A`
  utils.exec(cmd, callback)
}

utils.checkPrerequisites = callback => {
  return callback()
}

utils.getEmail = callback => {
  const cmd = 'git config --get user.email'
  utils.exec(cmd, (ignoredError, email) => {
    callback(null, email)
  })
}

utils.setEmail = (email, callback) => {
  if (email) return callback()
  const cmd = `git config user.email ${process.env.GITHUB_EMAIL}`
  utils.exec(cmd, () => {
    callback()
  })
}

utils.getUser = callback => {
  const cmd = 'git config --get user.name'
  utils.exec(cmd, (ignoredError, name) => {
    callback(null, name)
  })
}

utils.setUser = (name, callback) => {
  if (name) return callback()
  const cmd = `git config user.name ${process.env.GITHUB_USER}`
  utils.exec(cmd, () => {
    callback()
  })
}

utils.getBranch = callback => {
  const cmd = 'git rev-parse --abbrev-ref HEAD'
  utils.exec(cmd, (err, branch) => {
    callback(err, process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH || process.env.CIRCLE_BRANCH || ('' + branch).trim())
  })
}

utils.checkBranch = (releaseBranch, callback) => {
  utils.getBranch((unhandledErr, currentBranch) => {
    if (releaseBranch !== currentBranch) return callback(`skip this on ${currentBranch} (on ${releaseBranch} only)`)
    callback()
  })
}

utils.checkAlreadyReleased = callback => {
  const cmd = `git tag --list`
  utils.exec(cmd, (err, tags) => {
    if (err) return callback(err)
    tags = ('' + tags).split(/\n/)
    if (tags.includes(utils.versionTag)) return callback(`already released ${utils.version} - please ⬆  your version`)
    callback()
  })
}

utils.getCommitMessagesSinceLastRelease = callback => {
  const cmd = 'git log `git describe --tags --abbrev=0`..HEAD --oneline'
  utils.exec(cmd, (err, notes) => {
    // style it like a markdown list
    notes = '\n' + ('' + notes).trim().replace(/^/mg, '  - ') + '\n'
    callback(err, notes)
  })
}

utils.commit = callback => {
  const message = utils.commitMessageWithCIID() + ' [skip ci]'
  utils.exec(`git commit -m '${message}'`, (err, stdout, stderr) => {
    callback(err)
  })
}

utils.push = callback => {
  utils.exec(`git config --global push.default matching; git push`, (err, stdout, stderr) => {
    callback(err)
  })
}

// relies on something like # changelog being in the CHANGELOG already
utils.updateChangelog = (notes, callback) => {
  fs.readFile('CHANGELOG.md', 'utf-8', (ignoredError, contents = '') => {
    const existingLines = new RegExp(`.*${utils.versionTag}.*`, 'g')
    const newContents = contents
      .replace(existingLines, '')
      .replace(/\n\s*\n/g, '\n')
      .replace(/# Changelog/gi, '')
      .replace(/^/, `# Changelog\n\n- ${utils.versionTag}${notes}`)
    fs.writeFile('CHANGELOG.md', newContents, function (writeErr) {
      if (writeErr) return callback(writeErr)
      callback()
    })
  })
}

utils.addFile = (file, callback) => {
  if (!file) return callback(new Error('addFile expects a file'))
  utils.execAndIgnoreOutput(`git add ${file}`, callback)
}

utils.addDist = utils.addFile.bind(null, utils.distFolder)

utils.addChangelog = utils.addFile.bind(null, 'CHANGELOG.md')

utils.addSize = utils.addFile.bind(null, '.assetSize')

utils.commitMessageWithCIID = () => {
  return `:robot: Release via CI build ${process.env.CIRCLE_BUILD_NUM || process.env.TRAVIS_JOB_ID || ''}`
}

utils.commitMessageWithCILinks = () => {
  let message = utils.commitMessageWithCIID()
  if (process.env.CIRCLE_BUILD_NUM) {
    return `${message} https://circleci.com/gh/${utils.ownerAndName}/${process.env.CIRCLE_BUILD_NUM}`
  }
  if (process.env.TRAVIS_JOB_NUMBER) {
    return `${message} https://travis-ci.com/${utils.ownerAndName}/jobs/${process.env.TRAVIS_JOB_ID}`
  }
  return message
}

utils.tagVersion = (tag, notes, callback) => {
  const message = utils.commitMessageWithCILinks()
  utils.exec(`git rev-parse HEAD`, (err, sha) => {
    if (err) return callback(err)
    const body = [message, notes].join('\n').replace(/"/g, '')
    const cmd = `git tag -a ${tag} -m "${body}" ${('' + sha).trim()}; git push origin ${tag}`
    utils.execAndIgnoreOutput(cmd, callback)
  })
}

utils.tagAbsoluteVersion = utils.tagVersion.bind(utils, utils.versionTag)
utils.tagMajorVersion = utils.tagVersion.bind(utils, utils.majorVersionTag, '')
utils.tagMinorVersion = utils.tagVersion.bind(utils, utils.minorVersionTag, '')

utils.deleteTag = (tag, callback) => {
  const cmd = `git tag -d ${tag}; git push origin :refs/tags/${tag}`
  utils.exec(cmd, ignoredErr => {
    // may not exist so just call back - we are console.warning the error inside utils.exec()
    callback()
  })
}

utils.untagMajorVersion = utils.deleteTag.bind(utils, utils.majorVersionTag)
utils.untagMinorVersion = utils.deleteTag.bind(utils, utils.minorVersionTag)

// is there a better way to check we are no a feature branch?
utils.confirmOnFeatureBranch = callback => {
  utils.checkBranch('master', err => {
    if (!err) return callback('skipping this on master branch')
    utils.checkBranch('staging', err => {
      if (!err) return callback('skipping this on staging branch')
      callback()
    })
  })
}

utils.getSize = (file, callback) => {
  fs.stat(file, (err, result) => {
    if (err || !result) {
      console.warn('could not stat', file, 'did you not `npm run build`? or did you mean to set BUILT_ASSET?')
      return callback(err)
    }
    callback(null, result.size)
  })
}

utils.reportSize = (current, previous, callback) => {
  const delta = ((current - previous) / previous) * 100
  if (delta > 0) {
    console.warn(`⚠  file size has gone up from ${previous} to ${current} bytes`)
    if (delta > 1) console.warn(`🙀  this is more than ${parseInt(delta, 10)}% increase!`)
  } else if (delta < -1) {
    console.info('🐭  good file size reducing!')
  }
  let prNumber = process.env.TRAVIS_PULL_REQUEST
  if (process.env.CI_PULL_REQUEST) {
    prNumber = ('' + process.env.CI_PULL_REQUEST).split('/').pop()
  }
  prNumber = parseInt(prNumber, 10)
  /* istanbul ignore next */
  if (prNumber) {
    utils.labelPullRequestWithMetricMovement({
      githubToken: process.env.GITHUB_API_TOKEN,
      ownerAndRepo: utils.ownerAndName,
      prNumber,
      metrics: { previous, current },
      desirableTrajectory: 'DECREASE',
      minorPercentageThreshold: 1,
      minorValueThreshold: 300,
      majorPercentageThreshold: 3,
      majorValueThreshold: 2000,
      labelModifier: 'page weight'
    }).catch(err => {
      console.error('Problem applying page weight label', err)
    })
  }
  callback()
}

utils.build = callback => {
  if (!process.env.npm_package_scripts_build) return callback()
  const file = process.env.BUILT_ASSET || `${utils.distFolder}/${utils.name}.min.js`
  utils.execAndIgnoreOutput('NODE_ENV=production npm run build', err => {
    if (err) return callback(err)
    utils.getSize(file, (err, size) => {
      if (err) return callback(err)
      fs.writeFile('.assetSize', size, err => {
        if (err) return callback(err)
        utils.addFile('.assetSize', callback)
      })
    })
  })
}

utils.getPreviousSize = callback => {
  fs.readFile('.assetSize', 'utf-8', callback)
}

utils.getBuiltSizeOfBranch = (branch, callback) => {
  const file = process.env.BUILT_ASSET || `${utils.distFolder}/${utils.name}.min.js`
  utils.exec(`git checkout ${utils.distFolder} 2> /dev/null; git checkout ${branch} && NODE_ENV=production npm run build > /dev/null`, err => {
    if (err) return callback(err)
    utils.getSize(file, callback)
  })
}

// ⚠  don't be tempted to put this in postbuild or prebuild...
utils.getBuiltAssetStats = callback => {
  utils.getBranch((err, branch) => {
    if (err) return callback(err)
    utils.getPreviousSize((err, previousSize) => {
      if (err) return callback()
      utils.getBuiltSizeOfBranch(branch, (err, size) => {
        if (err) return callback(err)
        utils.reportSize(size, previousSize, callback)
      })
    })
  })
}
<commit_msg>:wrench: set email<commit_after>const childProcess = require('child_process')
const fs = require('fs')

const utils = module.exports = {}

utils.labelPullRequestWithMetricMovement = require('./labelPullRequestWithMetricMovement')

utils.version = process.env.npm_package_version
utils.versionTag = 'v' + utils.version
utils.majorVersionTag = utils.versionTag.replace(/\..+/, '-latest')
utils.minorVersionTag = utils.versionTag.replace(/(\d+\.\d+).+/, '$1-latest')
utils.name = process.env.npm_package_name
utils.ownerAndName = `holidayextras/${utils.name}`
utils.distFolder = process.env.DIST_FOLDER || 'dist'
const repo = ('' + process.env.npm_package_repository_url).split('/')
/* istanbul ignore else */
if (repo.pop() === `${utils.name}.git`) {
  utils.ownerAndName = `${repo.pop()}/${utils.name}`
  // console.info('got owner and name', utils.ownerAndName, 'from', process.env.npm_package_repository_url, 'get this from env vars?')
}

utils.getIntegrity = (file, callback) => {
  utils.getSignature(file, (err, signature) => {
    if (err) return callback(err)
    callback(null, 'sha256-' + signature)
  })
}

utils.exec = (cmd, callback) => {
  childProcess.exec(cmd, (err, stdout, stderr) => {
    if (err || stderr) console.warn(cmd, err, stdout, stderr)
    callback(err, stdout)
  })
}

utils.execAndIgnoreOutput = (cmd, callback) => {
  utils.exec(cmd, err => {
    callback(err)
  })
}

utils.createVersionedDistFile = (file, callback) => {
  if (!utils.version) return callback('Version missing - must run this as an npm script')
  const versionedFile = file.replace('.js', `.${utils.version}.js`)
  const cmd = `cp ${file} ${versionedFile}`
  utils.exec(cmd, err => {
    callback(err, versionedFile)
  })
}

utils.getSignature = (file, callback) => {
  const cmd = `cat ${file} | openssl dgst -sha256 -binary | openssl enc -base64 -A`
  utils.exec(cmd, callback)
}

utils.checkPrerequisites = callback => {
  utils.setEmail(null, callback)
}

utils.getEmail = callback => {
  const cmd = 'git config --get user.email'
  utils.exec(cmd, (ignoredError, email) => {
    callback(null, email)
  })
}

utils.setEmail = (email, callback) => {
  if (email) return callback()
  const cmd = `git config user.email ${process.env.GITHUB_EMAIL}`
  utils.exec(cmd, () => {
    callback()
  })
}

utils.getUser = callback => {
  const cmd = 'git config --get user.name'
  utils.exec(cmd, (ignoredError, name) => {
    callback(null, name)
  })
}

utils.setUser = (name, callback) => {
  if (name) return callback()
  const cmd = `git config user.name ${process.env.GITHUB_USER}`
  utils.exec(cmd, () => {
    callback()
  })
}

utils.getBranch = callback => {
  const cmd = 'git rev-parse --abbrev-ref HEAD'
  utils.exec(cmd, (err, branch) => {
    callback(err, process.env.TRAVIS_PULL_REQUEST_BRANCH || process.env.TRAVIS_BRANCH || process.env.CIRCLE_BRANCH || ('' + branch).trim())
  })
}

utils.checkBranch = (releaseBranch, callback) => {
  utils.getBranch((unhandledErr, currentBranch) => {
    if (releaseBranch !== currentBranch) return callback(`skip this on ${currentBranch} (on ${releaseBranch} only)`)
    callback()
  })
}

utils.checkAlreadyReleased = callback => {
  const cmd = `git tag --list`
  utils.exec(cmd, (err, tags) => {
    if (err) return callback(err)
    tags = ('' + tags).split(/\n/)
    if (tags.includes(utils.versionTag)) return callback(`already released ${utils.version} - please ⬆  your version`)
    callback()
  })
}

utils.getCommitMessagesSinceLastRelease = callback => {
  const cmd = 'git log `git describe --tags --abbrev=0`..HEAD --oneline'
  utils.exec(cmd, (err, notes) => {
    // style it like a markdown list
    notes = '\n' + ('' + notes).trim().replace(/^/mg, '  - ') + '\n'
    callback(err, notes)
  })
}

utils.commit = callback => {
  const message = utils.commitMessageWithCIID() + ' [skip ci]'
  utils.exec(`git commit -m '${message}'`, (err, stdout, stderr) => {
    callback(err)
  })
}

utils.push = callback => {
  utils.exec(`git config --global push.default matching; git push`, (err, stdout, stderr) => {
    callback(err)
  })
}

// relies on something like # changelog being in the CHANGELOG already
utils.updateChangelog = (notes, callback) => {
  fs.readFile('CHANGELOG.md', 'utf-8', (ignoredError, contents = '') => {
    const existingLines = new RegExp(`.*${utils.versionTag}.*`, 'g')
    const newContents = contents
      .replace(existingLines, '')
      .replace(/\n\s*\n/g, '\n')
      .replace(/# Changelog/gi, '')
      .replace(/^/, `# Changelog\n\n- ${utils.versionTag}${notes}`)
    fs.writeFile('CHANGELOG.md', newContents, function (writeErr) {
      if (writeErr) return callback(writeErr)
      callback()
    })
  })
}

utils.addFile = (file, callback) => {
  if (!file) return callback(new Error('addFile expects a file'))
  utils.execAndIgnoreOutput(`git add ${file}`, callback)
}

utils.addDist = utils.addFile.bind(null, utils.distFolder)

utils.addChangelog = utils.addFile.bind(null, 'CHANGELOG.md')

utils.addSize = utils.addFile.bind(null, '.assetSize')

utils.commitMessageWithCIID = () => {
  return `:robot: Release via CI build ${process.env.CIRCLE_BUILD_NUM || process.env.TRAVIS_JOB_ID || ''}`
}

utils.commitMessageWithCILinks = () => {
  let message = utils.commitMessageWithCIID()
  if (process.env.CIRCLE_BUILD_NUM) {
    return `${message} https://circleci.com/gh/${utils.ownerAndName}/${process.env.CIRCLE_BUILD_NUM}`
  }
  if (process.env.TRAVIS_JOB_NUMBER) {
    return `${message} https://travis-ci.com/${utils.ownerAndName}/jobs/${process.env.TRAVIS_JOB_ID}`
  }
  return message
}

utils.tagVersion = (tag, notes, callback) => {
  const message = utils.commitMessageWithCILinks()
  utils.exec(`git rev-parse HEAD`, (err, sha) => {
    if (err) return callback(err)
    const body = [message, notes].join('\n').replace(/"/g, '')
    const cmd = `git tag -a ${tag} -m "${body}" ${('' + sha).trim()}; git push origin ${tag}`
    utils.execAndIgnoreOutput(cmd, callback)
  })
}

utils.tagAbsoluteVersion = utils.tagVersion.bind(utils, utils.versionTag)
utils.tagMajorVersion = utils.tagVersion.bind(utils, utils.majorVersionTag, '')
utils.tagMinorVersion = utils.tagVersion.bind(utils, utils.minorVersionTag, '')

utils.deleteTag = (tag, callback) => {
  const cmd = `git tag -d ${tag}; git push origin :refs/tags/${tag}`
  utils.exec(cmd, ignoredErr => {
    // may not exist so just call back - we are console.warning the error inside utils.exec()
    callback()
  })
}

utils.untagMajorVersion = utils.deleteTag.bind(utils, utils.majorVersionTag)
utils.untagMinorVersion = utils.deleteTag.bind(utils, utils.minorVersionTag)

// is there a better way to check we are no a feature branch?
utils.confirmOnFeatureBranch = callback => {
  utils.checkBranch('master', err => {
    if (!err) return callback('skipping this on master branch')
    utils.checkBranch('staging', err => {
      if (!err) return callback('skipping this on staging branch')
      callback()
    })
  })
}

utils.getSize = (file, callback) => {
  fs.stat(file, (err, result) => {
    if (err || !result) {
      console.warn('could not stat', file, 'did you not `npm run build`? or did you mean to set BUILT_ASSET?')
      return callback(err)
    }
    callback(null, result.size)
  })
}

utils.reportSize = (current, previous, callback) => {
  const delta = ((current - previous) / previous) * 100
  if (delta > 0) {
    console.warn(`⚠  file size has gone up from ${previous} to ${current} bytes`)
    if (delta > 1) console.warn(`🙀  this is more than ${parseInt(delta, 10)}% increase!`)
  } else if (delta < -1) {
    console.info('🐭  good file size reducing!')
  }
  let prNumber = process.env.TRAVIS_PULL_REQUEST
  if (process.env.CI_PULL_REQUEST) {
    prNumber = ('' + process.env.CI_PULL_REQUEST).split('/').pop()
  }
  prNumber = parseInt(prNumber, 10)
  /* istanbul ignore next */
  if (prNumber) {
    utils.labelPullRequestWithMetricMovement({
      githubToken: process.env.GITHUB_API_TOKEN,
      ownerAndRepo: utils.ownerAndName,
      prNumber,
      metrics: { previous, current },
      desirableTrajectory: 'DECREASE',
      minorPercentageThreshold: 1,
      minorValueThreshold: 300,
      majorPercentageThreshold: 3,
      majorValueThreshold: 2000,
      labelModifier: 'page weight'
    }).catch(err => {
      console.error('Problem applying page weight label', err)
    })
  }
  callback()
}

utils.build = callback => {
  if (!process.env.npm_package_scripts_build) return callback()
  const file = process.env.BUILT_ASSET || `${utils.distFolder}/${utils.name}.min.js`
  utils.execAndIgnoreOutput('NODE_ENV=production npm run build', err => {
    if (err) return callback(err)
    utils.getSize(file, (err, size) => {
      if (err) return callback(err)
      fs.writeFile('.assetSize', size, err => {
        if (err) return callback(err)
        utils.addFile('.assetSize', callback)
      })
    })
  })
}

utils.getPreviousSize = callback => {
  fs.readFile('.assetSize', 'utf-8', callback)
}

utils.getBuiltSizeOfBranch = (branch, callback) => {
  const file = process.env.BUILT_ASSET || `${utils.distFolder}/${utils.name}.min.js`
  utils.exec(`git checkout ${utils.distFolder} 2> /dev/null; git checkout ${branch} && NODE_ENV=production npm run build > /dev/null`, err => {
    if (err) return callback(err)
    utils.getSize(file, callback)
  })
}

// ⚠  don't be tempted to put this in postbuild or prebuild...
utils.getBuiltAssetStats = callback => {
  utils.getBranch((err, branch) => {
    if (err) return callback(err)
    utils.getPreviousSize((err, previousSize) => {
      if (err) return callback()
      utils.getBuiltSizeOfBranch(branch, (err, size) => {
        if (err) return callback(err)
        utils.reportSize(size, previousSize, callback)
      })
    })
  })
}
<|endoftext|>


<commit_before>import base64url from 'base64url'
import eventToPromise from 'event-to-promise'
import forEach from 'lodash/forEach'
import has from 'lodash/has'
import highland from 'highland'
import humanFormat from 'human-format'
import invert from 'lodash/invert'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import keys from 'lodash/keys'
import kindOf from 'kindof'
import mapToArray from 'lodash/map'
import multiKeyHashInt from 'multikey-hash'
import pick from 'lodash/pick'
import tmp from 'tmp'
import xml2js from 'xml2js'
import { resolve } from 'path'

// Moment timezone can be loaded only one time, it's a workaround to load
// the latest version because cron module uses an old version of moment which
// does not implement `guess` function for example.
import 'moment-timezone'

import through2 from 'through2'
import { CronJob } from 'cron'
import { utcFormat, utcParse } from 'd3-time-format'
import {
  all as pAll,
  defer,
  fromCallback,
  promisify,
  reflect as pReflect
} from 'promise-toolbox'
import {
  createHash,
  randomBytes
} from 'crypto'

// ===================================================================

// Similar to map() + Promise.all() but wait for all promises to
// settle before rejecting (with the first error)
export const asyncMap = (collection, iteratee) => {
  let errorContainer
  const onError = error => {
    if (errorContainer === undefined) {
      errorContainer = { error }
    }
  }

  return Promise.all(mapToArray(collection, (item, key, collection) =>
    new Promise(resolve => {
      resolve(iteratee(item, key, collection))
    }).catch(onError)
  )).then(values => {
    if (errorContainer !== undefined) {
      throw errorContainer.error
    }
    return values
  })
}

// -------------------------------------------------------------------

export streamToBuffer from './stream-to-new-buffer'

// -------------------------------------------------------------------

export function camelToSnakeCase (string) {
  return string.replace(
    /([a-z0-9])([A-Z])/g,
    (_, prevChar, currChar) => `${prevChar}_${currChar.toLowerCase()}`
  )
}

// -------------------------------------------------------------------

// Returns an empty object without prototype (if possible).
export const createRawObject = Object.create
  ? (createObject => () => createObject(null))(Object.create)
  : () => ({})

// -------------------------------------------------------------------

// Only works with string items!
export const diffItems = (coll1, coll2) => {
  const removed = createRawObject()
  forEach(coll2, value => {
    removed[value] = true
  })

  const added = []
  forEach(coll1, value => {
    if (value in removed) {
      delete removed[value]
    } else {
      added.push(value)
    }
  })

  return [ added, keys(removed) ]
}

// -------------------------------------------------------------------

const ALGORITHM_TO_ID = {
  md5: '1',
  sha256: '5',
  sha512: '6'
}

const ID_TO_ALGORITHM = invert(ALGORITHM_TO_ID)

// Wrap a readable stream in a stream with a checksum promise
// attribute which is resolved at the end of an input stream.
// (Finally .checksum contains the checksum of the input stream)
//
// Example:
// const sourceStream = ...
// const targetStream = ...
// const checksumStream = addChecksumToReadStream(sourceStream)
// await Promise.all([
//   eventToPromise(checksumStream.pipe(targetStream), 'finish'),
//   checksumStream.checksum.then(console.log)
// ])
export const addChecksumToReadStream = (stream, algorithm = 'md5') => {
  const algorithmId = ALGORITHM_TO_ID[algorithm]

  if (!algorithmId) {
    throw new Error(`unknown algorithm: ${algorithm}`)
  }

  const hash = createHash(algorithm)
  const { promise, resolve } = defer()

  const wrapper = stream.pipe(through2(
    (chunk, enc, callback) => {
      hash.update(chunk)
      callback(null, chunk)
    },
    callback => {
      resolve(hash.digest('hex'))
      callback()
    }
  ))

  stream.on('error', error => wrapper.emit('error', error))
  wrapper.checksum = promise.then(hash => `$${algorithmId}$$${hash}`)

  return wrapper
}

// Check if the checksum of a readable stream is equals to an expected checksum.
// The given stream is wrapped in a stream which emits an error event
// if the computed checksum is not equals to the expected checksum.
export const validChecksumOfReadStream = (stream, expectedChecksum) => {
  const algorithmId = expectedChecksum.slice(1, expectedChecksum.indexOf('$', 1))

  if (!algorithmId) {
    throw new Error(`unknown algorithm: ${algorithmId}`)
  }

  const hash = createHash(ID_TO_ALGORITHM[algorithmId])

  const wrapper = stream.pipe(through2(
    { highWaterMark: 0 },
    (chunk, enc, callback) => {
      hash.update(chunk)
      callback(null, chunk)
    },
    callback => {
      const checksum = `$${algorithmId}$$${hash.digest('hex')}`

      callback(
        checksum !== expectedChecksum
          ? new Error(`Bad checksum (${checksum}), expected: ${expectedChecksum}`)
          : null
      )
    }
  ))

  stream.on('error', error => wrapper.emit('error', error))
  wrapper.checksumVerified = eventToPromise(wrapper, 'end')

  return wrapper
}

// -------------------------------------------------------------------

// Ensure the value is an array, wrap it if necessary.
export function ensureArray (value) {
  if (value === undefined) {
    return []
  }

  return isArray(value) ? value : [value]
}

// -------------------------------------------------------------------

// Returns the value of a property and removes it from the object.
export function extractProperty (obj, prop) {
  const value = obj[prop]
  delete obj[prop]
  return value
}

// -------------------------------------------------------------------

// Returns the first defined (non-undefined) value.
export const firstDefined = function () {
  const n = arguments.length
  for (let i = 0; i < n; ++i) {
    const arg = arguments[i]
    if (arg !== undefined) {
      return arg
    }
  }
}

// -------------------------------------------------------------------

export const getUserPublicProperties = user => pick(
  user.properties || user,
  'id', 'email', 'groups', 'permission', 'preferences', 'provider'
)

// -------------------------------------------------------------------

export const getPseudoRandomBytes = n => {
  const bytes = Buffer.allocUnsafe(n)

  const odd = n & 1
  for (let i = 0, m = n - odd; i < m; i += 2) {
    bytes.writeUInt16BE(Math.random() * 65536 | 0, i)
  }

  if (odd) {
    bytes.writeUInt8(Math.random() * 256 | 0, n - 1)
  }

  return bytes
}

export const generateUnsecureToken = (n = 32) => base64url(getPseudoRandomBytes(n))

// Generate a secure random Base64 string.
export const generateToken = (randomBytes => {
  return (n = 32) => randomBytes(n).then(base64url)
})(promisify(randomBytes))

// -------------------------------------------------------------------

export const formatXml = (function () {
  const builder = new xml2js.Builder({
    headless: true
  })

  return (...args) => builder.buildObject(...args)
})()

export const parseXml = (function () {
  const opts = {
    mergeAttrs: true,
    explicitArray: false
  }

  return (xml) => {
    let result

    // xml2js.parseString() use a callback for synchronous code.
    xml2js.parseString(xml, opts, (error, result_) => {
      if (error) {
        throw error
      }

      result = result_
    })

    return result
  }
})()

// -------------------------------------------------------------------

// Very light and fast set.
//
// - works only with strings
// - methods are already bound and chainable
export const lightSet = collection => {
  const data = createRawObject()
  if (collection) {
    forEach(collection, value => {
      data[value] = true
    })
    collection = null
  }

  const set = {
    add: value => {
      data[value] = true
      return set
    },
    clear: () => {
      for (const value in data) {
        delete data[value]
      }
      return set
    },
    delete: value => {
      delete data[value]
      return set
    },
    has: value => data[value],
    toArray: () => keys(data)
  }
  return set
}

// -------------------------------------------------------------------

// This function does nothing and returns undefined.
//
// It is often used to swallow promise's errors.
export const noop = () => {}

// -------------------------------------------------------------------

// Usage: pDebug(promise, name) or promise::pDebug(name)
export function pDebug (promise, name) {
  if (arguments.length === 1) {
    name = promise
    promise = this
  }

  Promise.resolve(promise).then(
    value => {
      console.log(
        '%s',
        `Promise ${name} resolved${value !== undefined ? ` with ${kindOf(value)}` : ''}`
      )
    },
    reason => {
      console.log(
        '%s',
        `Promise ${name} rejected${reason !== undefined ? ` with ${kindOf(reason)}` : ''}`
      )
    }
  )

  return promise
}

// Given a collection (array or object) which contains promises,
// return a promise that is fulfilled when all the items in the
// collection are either fulfilled or rejected.
//
// This promise will be fulfilled with a collection (of the same type,
// array or object) containing promise inspections.
//
// Usage: pSettle(promises) or promises::pSettle()
export function pSettle (promises) {
  return (this || promises)::pAll(p => p::pReflect())
}

// -------------------------------------------------------------------

export {
  all as pAll,
  delay as pDelay,
  fromCallback as pFromCallback,
  lastly as pFinally,
  promisify,
  promisifyAll,
  reflect as pReflect
} from 'promise-toolbox'

// -------------------------------------------------------------------

export function parseSize (size) {
  if (!isString(size)) {
    return size
  }

  let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
  if (bytes.unit && bytes.unit !== 'B') {
    bytes = humanFormat.parse.raw(size)

    if (bytes.unit && bytes.unit !== 'B') {
      throw new Error('invalid size: ' + size)
    }
  }
  return Math.floor(bytes.value * bytes.factor)
}

// -------------------------------------------------------------------

const _has = Object.prototype.hasOwnProperty

// Removes an own property from an object and returns its value.
export const popProperty = obj => {
  for (const prop in obj) {
    if (_has.call(obj, prop)) {
      return extractProperty(obj, prop)
    }
  }
}

// -------------------------------------------------------------------

// Format a date in ISO 8601 in a safe way to be used in filenames
// (even on Windows).
export const safeDateFormat = utcFormat('%Y%m%dT%H%M%SZ')

export const safeDateParse = utcParse('%Y%m%dT%H%M%SZ')

// -------------------------------------------------------------------

// This functions are often used throughout xo-server.
//
// Exports them from here to avoid direct dependencies on lodash/
export { default as forEach } from 'lodash/forEach'
export { default as isArray } from 'lodash/isArray'
export { default as isBoolean } from 'lodash/isBoolean'
export { default as isEmpty } from 'lodash/isEmpty'
export { default as isFunction } from 'lodash/isFunction'
export { default as isInteger } from 'lodash/isInteger'
export { default as isObject } from 'lodash/isObject'
export { default as isString } from 'lodash/isString'
export { default as mapToArray } from 'lodash/map'

// -------------------------------------------------------------------

// Special value which can be returned to stop an iteration in map()
// and mapInPlace().
export const DONE = {}

// Fill `target` by running each element in `collection` through
// `iteratee`.
//
// If `target` is undefined, it defaults to a new array if
// `collection` is array-like (has a `length` property), otherwise an
// object.
//
// The context of `iteratee` can be specified via `thisArg`.
//
// Note: the Mapping can be interrupted by returning the special value
// `DONE` provided as the fourth argument.
//
// Usage: map(collection, item => item + 1)
export function map (
  collection,
  iteratee,
  target = has(collection, 'length') ? [] : {}
) {
  forEach(collection, (item, i) => {
    const value = iteratee(item, i, collection, DONE)
    if (value === DONE) {
      return false
    }

    target[i] = value
  })

  return target
}

// -------------------------------------------------------------------

// Create a hash from multiple values.
export const multiKeyHash = (...args) => new Promise(resolve => {
  const hash = multiKeyHashInt(...args)

  const buf = Buffer.allocUnsafe(4)
  buf.writeUInt32LE(hash, 0)

  resolve(base64url(buf))
})

// -------------------------------------------------------------------

export const resolveSubpath = (root, path) =>
  resolve(root, `./${resolve('/', path)}`)

// -------------------------------------------------------------------

export const streamToArray = (stream, {
  filter,
  mapper
} = {}) => new Promise((resolve, reject) => {
  stream = highland(stream).stopOnError(reject)
  if (filter) {
    stream = stream.filter(filter)
  }
  if (mapper) {
    stream = stream.map(mapper)
  }
  stream.toArray(resolve)
})

// -------------------------------------------------------------------

export const scheduleFn = (cronTime, fn, timeZone) => {
  let running = false

  const job = new CronJob({
    cronTime,
    onTick: async () => {
      if (running) {
        return
      }

      running = true

      try {
        await fn()
      } catch (error) {
        console.error('[WARN] scheduled function:', (error && error.stack) || error)
      } finally {
        running = false
      }
    },
    start: true,
    timeZone
  })

  return () => {
    job.stop()
  }
}

// -------------------------------------------------------------------

// Create a serializable object from an error.
export const serializeError = error => ({
  message: error.message,
  stack: error.stack,
  ...error // Copy enumerable properties.
})

// -------------------------------------------------------------------

// Create an array which contains the results of one thunk function.
// Only works with synchronous thunks.
export const thunkToArray = thunk => {
  const values = []
  thunk(::values.push)
  return values
}

// -------------------------------------------------------------------

// Creates a new function which throws an error.
//
// ```js
// promise.catch(throwFn('an error has occured'))
//
// function foo (param = throwFn('param is required')()) {}
// ```
export const throwFn = error => () => {
  throw (
    isString(error)
      ? new Error(error)
      : error
  )
}

// -------------------------------------------------------------------

export const tmpDir = () => fromCallback(cb => tmp.dir(cb))

// -------------------------------------------------------------------

// Wrap a value in a function.
export const wrap = value => () => value

// -------------------------------------------------------------------

export const mapFilter = (collection, iteratee) => {
  const result = []
  forEach(collection, (...args) => {
    const value = iteratee(...args)
    if (value) {
      result.push(value)
    }
  })
  return result
}

// -------------------------------------------------------------------

export const splitFirst = (string, separator) => {
  const i = string.indexOf(separator)
  return i === -1 ? null : [
    string.slice(0, i),
    string.slice(i + separator.length)
  ]
}

// -------------------------------------------------------------------

export const getFirstPropertyName = object => {
  for (const key in object) {
    if (Object.prototype.hasOwnProperty.call(object, key)) {
      return key
    }
  }
}
<commit_msg>feat(utils/asyncMap): collection can be a promise<commit_after>import base64url from 'base64url'
import eventToPromise from 'event-to-promise'
import forEach from 'lodash/forEach'
import has from 'lodash/has'
import highland from 'highland'
import humanFormat from 'human-format'
import invert from 'lodash/invert'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import keys from 'lodash/keys'
import kindOf from 'kindof'
import mapToArray from 'lodash/map'
import multiKeyHashInt from 'multikey-hash'
import pick from 'lodash/pick'
import tmp from 'tmp'
import xml2js from 'xml2js'
import { resolve } from 'path'

// Moment timezone can be loaded only one time, it's a workaround to load
// the latest version because cron module uses an old version of moment which
// does not implement `guess` function for example.
import 'moment-timezone'

import through2 from 'through2'
import { CronJob } from 'cron'
import { utcFormat, utcParse } from 'd3-time-format'
import {
  all as pAll,
  defer,
  fromCallback,
  isPromise,
  promisify,
  reflect as pReflect
} from 'promise-toolbox'
import {
  createHash,
  randomBytes
} from 'crypto'

// ===================================================================

// Similar to map() + Promise.all() but wait for all promises to
// settle before rejecting (with the first error)
export const asyncMap = (collection, iteratee) => {
  if (isPromise(collection)) {
    return collection.then(collection => asyncMap(collection, iteratee))
  }

  let errorContainer
  const onError = error => {
    if (errorContainer === undefined) {
      errorContainer = { error }
    }
  }

  return Promise.all(mapToArray(collection, (item, key, collection) =>
    new Promise(resolve => {
      resolve(iteratee(item, key, collection))
    }).catch(onError)
  )).then(values => {
    if (errorContainer !== undefined) {
      throw errorContainer.error
    }
    return values
  })
}

// -------------------------------------------------------------------

export streamToBuffer from './stream-to-new-buffer'

// -------------------------------------------------------------------

export function camelToSnakeCase (string) {
  return string.replace(
    /([a-z0-9])([A-Z])/g,
    (_, prevChar, currChar) => `${prevChar}_${currChar.toLowerCase()}`
  )
}

// -------------------------------------------------------------------

// Returns an empty object without prototype (if possible).
export const createRawObject = Object.create
  ? (createObject => () => createObject(null))(Object.create)
  : () => ({})

// -------------------------------------------------------------------

// Only works with string items!
export const diffItems = (coll1, coll2) => {
  const removed = createRawObject()
  forEach(coll2, value => {
    removed[value] = true
  })

  const added = []
  forEach(coll1, value => {
    if (value in removed) {
      delete removed[value]
    } else {
      added.push(value)
    }
  })

  return [ added, keys(removed) ]
}

// -------------------------------------------------------------------

const ALGORITHM_TO_ID = {
  md5: '1',
  sha256: '5',
  sha512: '6'
}

const ID_TO_ALGORITHM = invert(ALGORITHM_TO_ID)

// Wrap a readable stream in a stream with a checksum promise
// attribute which is resolved at the end of an input stream.
// (Finally .checksum contains the checksum of the input stream)
//
// Example:
// const sourceStream = ...
// const targetStream = ...
// const checksumStream = addChecksumToReadStream(sourceStream)
// await Promise.all([
//   eventToPromise(checksumStream.pipe(targetStream), 'finish'),
//   checksumStream.checksum.then(console.log)
// ])
export const addChecksumToReadStream = (stream, algorithm = 'md5') => {
  const algorithmId = ALGORITHM_TO_ID[algorithm]

  if (!algorithmId) {
    throw new Error(`unknown algorithm: ${algorithm}`)
  }

  const hash = createHash(algorithm)
  const { promise, resolve } = defer()

  const wrapper = stream.pipe(through2(
    (chunk, enc, callback) => {
      hash.update(chunk)
      callback(null, chunk)
    },
    callback => {
      resolve(hash.digest('hex'))
      callback()
    }
  ))

  stream.on('error', error => wrapper.emit('error', error))
  wrapper.checksum = promise.then(hash => `$${algorithmId}$$${hash}`)

  return wrapper
}

// Check if the checksum of a readable stream is equals to an expected checksum.
// The given stream is wrapped in a stream which emits an error event
// if the computed checksum is not equals to the expected checksum.
export const validChecksumOfReadStream = (stream, expectedChecksum) => {
  const algorithmId = expectedChecksum.slice(1, expectedChecksum.indexOf('$', 1))

  if (!algorithmId) {
    throw new Error(`unknown algorithm: ${algorithmId}`)
  }

  const hash = createHash(ID_TO_ALGORITHM[algorithmId])

  const wrapper = stream.pipe(through2(
    { highWaterMark: 0 },
    (chunk, enc, callback) => {
      hash.update(chunk)
      callback(null, chunk)
    },
    callback => {
      const checksum = `$${algorithmId}$$${hash.digest('hex')}`

      callback(
        checksum !== expectedChecksum
          ? new Error(`Bad checksum (${checksum}), expected: ${expectedChecksum}`)
          : null
      )
    }
  ))

  stream.on('error', error => wrapper.emit('error', error))
  wrapper.checksumVerified = eventToPromise(wrapper, 'end')

  return wrapper
}

// -------------------------------------------------------------------

// Ensure the value is an array, wrap it if necessary.
export function ensureArray (value) {
  if (value === undefined) {
    return []
  }

  return isArray(value) ? value : [value]
}

// -------------------------------------------------------------------

// Returns the value of a property and removes it from the object.
export function extractProperty (obj, prop) {
  const value = obj[prop]
  delete obj[prop]
  return value
}

// -------------------------------------------------------------------

// Returns the first defined (non-undefined) value.
export const firstDefined = function () {
  const n = arguments.length
  for (let i = 0; i < n; ++i) {
    const arg = arguments[i]
    if (arg !== undefined) {
      return arg
    }
  }
}

// -------------------------------------------------------------------

export const getUserPublicProperties = user => pick(
  user.properties || user,
  'id', 'email', 'groups', 'permission', 'preferences', 'provider'
)

// -------------------------------------------------------------------

export const getPseudoRandomBytes = n => {
  const bytes = Buffer.allocUnsafe(n)

  const odd = n & 1
  for (let i = 0, m = n - odd; i < m; i += 2) {
    bytes.writeUInt16BE(Math.random() * 65536 | 0, i)
  }

  if (odd) {
    bytes.writeUInt8(Math.random() * 256 | 0, n - 1)
  }

  return bytes
}

export const generateUnsecureToken = (n = 32) => base64url(getPseudoRandomBytes(n))

// Generate a secure random Base64 string.
export const generateToken = (randomBytes => {
  return (n = 32) => randomBytes(n).then(base64url)
})(promisify(randomBytes))

// -------------------------------------------------------------------

export const formatXml = (function () {
  const builder = new xml2js.Builder({
    headless: true
  })

  return (...args) => builder.buildObject(...args)
})()

export const parseXml = (function () {
  const opts = {
    mergeAttrs: true,
    explicitArray: false
  }

  return (xml) => {
    let result

    // xml2js.parseString() use a callback for synchronous code.
    xml2js.parseString(xml, opts, (error, result_) => {
      if (error) {
        throw error
      }

      result = result_
    })

    return result
  }
})()

// -------------------------------------------------------------------

// Very light and fast set.
//
// - works only with strings
// - methods are already bound and chainable
export const lightSet = collection => {
  const data = createRawObject()
  if (collection) {
    forEach(collection, value => {
      data[value] = true
    })
    collection = null
  }

  const set = {
    add: value => {
      data[value] = true
      return set
    },
    clear: () => {
      for (const value in data) {
        delete data[value]
      }
      return set
    },
    delete: value => {
      delete data[value]
      return set
    },
    has: value => data[value],
    toArray: () => keys(data)
  }
  return set
}

// -------------------------------------------------------------------

// This function does nothing and returns undefined.
//
// It is often used to swallow promise's errors.
export const noop = () => {}

// -------------------------------------------------------------------

// Usage: pDebug(promise, name) or promise::pDebug(name)
export function pDebug (promise, name) {
  if (arguments.length === 1) {
    name = promise
    promise = this
  }

  Promise.resolve(promise).then(
    value => {
      console.log(
        '%s',
        `Promise ${name} resolved${value !== undefined ? ` with ${kindOf(value)}` : ''}`
      )
    },
    reason => {
      console.log(
        '%s',
        `Promise ${name} rejected${reason !== undefined ? ` with ${kindOf(reason)}` : ''}`
      )
    }
  )

  return promise
}

// Given a collection (array or object) which contains promises,
// return a promise that is fulfilled when all the items in the
// collection are either fulfilled or rejected.
//
// This promise will be fulfilled with a collection (of the same type,
// array or object) containing promise inspections.
//
// Usage: pSettle(promises) or promises::pSettle()
export function pSettle (promises) {
  return (this || promises)::pAll(p => p::pReflect())
}

// -------------------------------------------------------------------

export {
  all as pAll,
  delay as pDelay,
  fromCallback as pFromCallback,
  lastly as pFinally,
  promisify,
  promisifyAll,
  reflect as pReflect
} from 'promise-toolbox'

// -------------------------------------------------------------------

export function parseSize (size) {
  if (!isString(size)) {
    return size
  }

  let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
  if (bytes.unit && bytes.unit !== 'B') {
    bytes = humanFormat.parse.raw(size)

    if (bytes.unit && bytes.unit !== 'B') {
      throw new Error('invalid size: ' + size)
    }
  }
  return Math.floor(bytes.value * bytes.factor)
}

// -------------------------------------------------------------------

const _has = Object.prototype.hasOwnProperty

// Removes an own property from an object and returns its value.
export const popProperty = obj => {
  for (const prop in obj) {
    if (_has.call(obj, prop)) {
      return extractProperty(obj, prop)
    }
  }
}

// -------------------------------------------------------------------

// Format a date in ISO 8601 in a safe way to be used in filenames
// (even on Windows).
export const safeDateFormat = utcFormat('%Y%m%dT%H%M%SZ')

export const safeDateParse = utcParse('%Y%m%dT%H%M%SZ')

// -------------------------------------------------------------------

// This functions are often used throughout xo-server.
//
// Exports them from here to avoid direct dependencies on lodash/
export { default as forEach } from 'lodash/forEach'
export { default as isArray } from 'lodash/isArray'
export { default as isBoolean } from 'lodash/isBoolean'
export { default as isEmpty } from 'lodash/isEmpty'
export { default as isFunction } from 'lodash/isFunction'
export { default as isInteger } from 'lodash/isInteger'
export { default as isObject } from 'lodash/isObject'
export { default as isString } from 'lodash/isString'
export { default as mapToArray } from 'lodash/map'

// -------------------------------------------------------------------

// Special value which can be returned to stop an iteration in map()
// and mapInPlace().
export const DONE = {}

// Fill `target` by running each element in `collection` through
// `iteratee`.
//
// If `target` is undefined, it defaults to a new array if
// `collection` is array-like (has a `length` property), otherwise an
// object.
//
// The context of `iteratee` can be specified via `thisArg`.
//
// Note: the Mapping can be interrupted by returning the special value
// `DONE` provided as the fourth argument.
//
// Usage: map(collection, item => item + 1)
export function map (
  collection,
  iteratee,
  target = has(collection, 'length') ? [] : {}
) {
  forEach(collection, (item, i) => {
    const value = iteratee(item, i, collection, DONE)
    if (value === DONE) {
      return false
    }

    target[i] = value
  })

  return target
}

// -------------------------------------------------------------------

// Create a hash from multiple values.
export const multiKeyHash = (...args) => new Promise(resolve => {
  const hash = multiKeyHashInt(...args)

  const buf = Buffer.allocUnsafe(4)
  buf.writeUInt32LE(hash, 0)

  resolve(base64url(buf))
})

// -------------------------------------------------------------------

export const resolveSubpath = (root, path) =>
  resolve(root, `./${resolve('/', path)}`)

// -------------------------------------------------------------------

export const streamToArray = (stream, {
  filter,
  mapper
} = {}) => new Promise((resolve, reject) => {
  stream = highland(stream).stopOnError(reject)
  if (filter) {
    stream = stream.filter(filter)
  }
  if (mapper) {
    stream = stream.map(mapper)
  }
  stream.toArray(resolve)
})

// -------------------------------------------------------------------

export const scheduleFn = (cronTime, fn, timeZone) => {
  let running = false

  const job = new CronJob({
    cronTime,
    onTick: async () => {
      if (running) {
        return
      }

      running = true

      try {
        await fn()
      } catch (error) {
        console.error('[WARN] scheduled function:', (error && error.stack) || error)
      } finally {
        running = false
      }
    },
    start: true,
    timeZone
  })

  return () => {
    job.stop()
  }
}

// -------------------------------------------------------------------

// Create a serializable object from an error.
export const serializeError = error => ({
  message: error.message,
  stack: error.stack,
  ...error // Copy enumerable properties.
})

// -------------------------------------------------------------------

// Create an array which contains the results of one thunk function.
// Only works with synchronous thunks.
export const thunkToArray = thunk => {
  const values = []
  thunk(::values.push)
  return values
}

// -------------------------------------------------------------------

// Creates a new function which throws an error.
//
// ```js
// promise.catch(throwFn('an error has occured'))
//
// function foo (param = throwFn('param is required')()) {}
// ```
export const throwFn = error => () => {
  throw (
    isString(error)
      ? new Error(error)
      : error
  )
}

// -------------------------------------------------------------------

export const tmpDir = () => fromCallback(cb => tmp.dir(cb))

// -------------------------------------------------------------------

// Wrap a value in a function.
export const wrap = value => () => value

// -------------------------------------------------------------------

export const mapFilter = (collection, iteratee) => {
  const result = []
  forEach(collection, (...args) => {
    const value = iteratee(...args)
    if (value) {
      result.push(value)
    }
  })
  return result
}

// -------------------------------------------------------------------

export const splitFirst = (string, separator) => {
  const i = string.indexOf(separator)
  return i === -1 ? null : [
    string.slice(0, i),
    string.slice(i + separator.length)
  ]
}

// -------------------------------------------------------------------

export const getFirstPropertyName = object => {
  for (const key in object) {
    if (Object.prototype.hasOwnProperty.call(object, key)) {
      return key
    }
  }
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2016, Globo.com (https://github.com/globocom)
 * Copyright (c) 2016, Andrew Coelho <info@andrewcoelho.com>
 *
 * License: MIT
 */

import {
  convertToRaw,
  convertFromRaw,
  EditorState,
  getVisibleSelectionRect} from "draft-js";

import decorator from "./decorator";

export function editorStateToJSON(editorState) {
  if (editorState) {
    const content = editorState.getCurrentContent();
    return JSON.stringify(convertToRaw(content), null, 2);
  }
}

export function editorStateFromRaw(rawContent) {
  const content = convertFromRaw(rawContent);
  return EditorState.createWithContent(content, decorator);
}

export function getSelectedBlockElement(range) {
  let node = range.startContainer;
  do {
    const nodeIsDataBlock = node.getAttribute
                            ? node.getAttribute("data-block")
                            : null;
    if (nodeIsDataBlock) {
      return node;
    }
    node = node.parentNode;
  } while (node !== null);
  return null;
}

export function getSelectionCoords(editor, toolbar) {
  const editorBounds = editor.getBoundingClientRect();
  const rangeBounds = getVisibleSelectionRect(window);

  if (!rangeBounds) {
    return null;
  }

  const rangeWidth = rangeBounds.right - rangeBounds.left;

  const toolbarHeight = toolbar.offsetHeight;
  // const rangeHeight = rangeBounds.bottom - rangeBounds.top;
  const offsetLeft = (rangeBounds.left - editorBounds.left)
            + (rangeWidth / 2);
  const offsetTop = rangeBounds.top - editorBounds.top - (toolbarHeight + 14);
  return { offsetLeft, offsetTop };
}
<commit_msg>Create empty state with editorStateFromRaw<commit_after>/*
 * Copyright (c) 2016, Globo.com (https://github.com/globocom)
 * Copyright (c) 2016, Andrew Coelho <info@andrewcoelho.com>
 *
 * License: MIT
 */

import {
  convertToRaw,
  convertFromRaw,
  EditorState,
  getVisibleSelectionRect} from "draft-js";

import decorator from "./decorator";

export function editorStateToJSON(editorState) {
  if (editorState) {
    const content = editorState.getCurrentContent();
    return JSON.stringify(convertToRaw(content), null, 2);
  }
}

export function editorStateFromRaw(rawContent) {
  if (rawContent) {
    const content = convertFromRaw(rawContent);
    return EditorState.createWithContent(content, decorator);
  } else {
    return EditorState.createEmpty();
  }
}

export function getSelectedBlockElement(range) {
  let node = range.startContainer;
  do {
    const nodeIsDataBlock = node.getAttribute
                            ? node.getAttribute("data-block")
                            : null;
    if (nodeIsDataBlock) {
      return node;
    }
    node = node.parentNode;
  } while (node !== null);
  return null;
}

export function getSelectionCoords(editor, toolbar) {
  const editorBounds = editor.getBoundingClientRect();
  const rangeBounds = getVisibleSelectionRect(window);

  if (!rangeBounds) {
    return null;
  }

  const rangeWidth = rangeBounds.right - rangeBounds.left;

  const toolbarHeight = toolbar.offsetHeight;
  // const rangeHeight = rangeBounds.bottom - rangeBounds.top;
  const offsetLeft = (rangeBounds.left - editorBounds.left)
            + (rangeWidth / 2);
  const offsetTop = rangeBounds.top - editorBounds.top - (toolbarHeight + 14);
  return { offsetLeft, offsetTop };
}
<|endoftext|>