streamEditor.js

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const _ = require("highland");
const lineStream_1 = require("./lineStream");
const bind_1 = require("./bind");
const files_1 = require("./files");
function toLine(obj) {
    return `${obj.data}\n`;
}
function toObject() {
    let count = 0;
    return (line) => {
        return {
            data: line.toString(),
            // tslint:disable-next-line:no-increment-decrement
            num: ++count
        };
    };
}
function reset() {
    let count = 0;
    return _.map((line) => {
        // tslint:disable-next-line:no-increment-decrement
        line.num = ++count;
        return line;
    });
}
function setline(lineNumber, to) {
    return _.map((line) => {
        if (line.num === lineNumber) {
            line.data = to;
        }
        return line;
    });
}
function map(fn) {
    return _.map((line) => {
        line.data = fn(line.data);
        return line;
    });
}
function filter(fn) {
    return _.filter((line) => {
        return fn(line.data);
    });
}
function deleteLine(n) {
    return _.filter((line) => {
        return line.num !== n;
    });
}
function replace(from, to) {
    return _.map((line) => {
        line.data = line.data.replace(from, to);
        return line;
    });
}
/** @class */
/** @implements {Editor} */
class StreamEditor {
    constructor(filename) {
        this.filename = filename;
        this._prepend = [];
        this._append = [];
        this.transforms = [];
        bind_1.default(this);
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * sets a `line` at a given line number `N`
     *
     * @method
     * set
     * @param {string} line
     * @param {number} n
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .set(1, line)
     *  .set(10, anotherline)
     *  .save();
     */
    set(i, line) {
        this.transforms.push(setline(i, line));
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Prepends a given `line` to a file
     *
     * @method
     * prepend
     * @param {string} line
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .prepend(line)
     *  .save();
     */
    prepend(line) {
        if (line !== undefined && line !== null) {
            this._prepend.push(`${line}`);
        }
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Appends a given `line` to a file
     *
     * @method
     * append
     * @param {string} line
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .append(line)
     *  .save();
     */
    append(line) {
        if (line !== undefined && line !== null) {
            this._append.push(`${line}\n`);
        }
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * replaces a given string or regex for every line
     *
     * @method
     * replace
     * @param {string|RegExp} source
     * @param {string} replacement
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .replace('x', 'y')
     *  .replace(/x/, 'y')
     *  .save();
     */
    replace(x, y) {
        this.transforms.push(replace(x, y));
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Maps each line using a given function
     *
     * @method
     * map
     * @param {function} fn
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .map((line) => {
     *      return line.toLowerCase() < 10;
     *  })
     *  .save();
     */
    map(fn) {
        this.transforms.push(map(fn));
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Filters the file contents using a given function
     *
     * @method
     * filter
     * @param {function} fn
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .filter((line) => {
     *      return line.length < 10;
     *  })
     *  .save();
     */
    filter(fn) {
        this.transforms.push(filter(fn));
        this.transforms.push(reset());
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Deletes line by line number
     *
     * @method
     * delete
     * @param {function} fn
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .delete(10)
     *  .save(); // delete line 10
     */
    delete(n) {
        this.transforms.push(deleteLine(n));
        this.transforms.push(reset());
        return this;
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Writes any modifications to the file
     *
     * @method
     * save
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .set(1, line)
     *  .filter(fn)
     *  .map(fn)
     *  .save();
     */
    save() {
        return __awaiter(this, void 0, void 0, function* () {
            yield files_1.overwrite(this.modify, this.filename);
        });
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Writes any modifications to a given file
     *
     * @method
     * saveAs
     * @param {string} filename
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .set(1, line)
     *  .filter(fn)
     *  .map(fn)
     *  .saveAs('myFile');
     */
    saveAs(file) {
        return __awaiter(this, void 0, void 0, function* () {
            return files_1.save(this.filename, file, this.modify);
        });
    }
    // tslint:disable-next-line:valid-jsdoc
    /**
     * Writes changes to stdout without modifying the source file. Useful for testing changes.
     *
     * @method
     * preview
     * @return Editor
     * @example
     * import FileSurgeon from 'FileSurgeon';
     *
     * const contents = FileSurgeon.edit(filename)
     *  .set(1, line)
     *  .filter(fn)
     *  .map(fn)
     *  .preview(); // writes changes to stdout
     */
    preview() {
        return __awaiter(this, void 0, void 0, function* () {
            let source;
            const dest = process.stdout;
            try {
                source = this.createSourceStream();
                yield this.modify(source, dest);
            }
            finally {
                source.destroy();
            }
        });
    }
    createPipeline() {
        const transforms = [
            _.map(toObject()),
            ...this.transforms,
            _.map(toLine)
        ];
        this.transforms = [];
        return _.pipeline(...transforms);
    }
    createSourceStream() {
        const source = lineStream_1.createStream(this.filename);
        return source;
    }
    consume(source) {
        let dest = _();
        let count = 0;
        let last;
        return new Promise((resolve, reject) => {
            source.on('readable', () => {
                let line;
                // tslint:disable-next-line:no-conditional-assignment
                while (null !== (line = source.read())) {
                    if (line instanceof Buffer) {
                        line = line.toString('utf8');
                    }
                    last = line;
                    // tslint:disable-next-line:no-increment-decrement
                    count++;
                    dest.write(line);
                }
            });
            source.on('end', () => {
                dest.end();
                if (last === '') { // remove extra blank line
                    dest = _(dest).take(count - 1);
                }
                resolve({
                    contents: dest,
                    length: count
                });
            });
        });
    }
    modify(destination, source) {
        return __awaiter(this, void 0, void 0, function* () {
            const { contents, length } = yield this.consume(source);
            return new Promise((resolve) => {
                if (length > 0) {
                    _(this._prepend)
                        .concat(contents)
                        .pipe(this.createPipeline())
                        .concat(_(this._append))
                        .pipe(destination);
                }
                else if (this._append.length > 0 || this._prepend.length > 0) {
                    _(this._prepend.concat(this._append))
                        .pipe(destination);
                }
                else {
                    resolve();
                }
                destination.on('finish', () => {
                    resolve();
                });
            });
        });
    }
}
exports.StreamEditor = StreamEditor;