all files / src/ index.ts

100% Statements 60/60
83.33% Branches 20/24
100% Functions 12/12
100% Lines 59/59
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120          81×       44× 27× 27×                                             44×       48× 15× 15×     42×       27× 27× 15×           27× 27× 27×   27×       27×           27× 26× 26×                  
import * as fs from "fs";
import * as stream from "stream";
import * as parser2 from "htmlparser2";
 
type StringTransformer = (text: string, matched: RegExpExecArray) => string;
type Transforms = [RegExp, StringTransformer][];
 
function isSelfClosedTag(tagname: string) {
    return tagname === "br" || tagname === "img";
}
 
export class Transformer extends stream.Transform {
    constructor(option?: stream.TransformOptions) {
        super(option);
        this.parser = new parser2.Parser({
            ontext: text => this._onText(text),
            onopentag: (name, attrib) => this._onOpenTag(name, attrib),
            onclosetag: name => this._onCloseTag(name)
        }, {
            recognizeSelfClosing: false
        });
    }
 
    protected buffer: string = "";
    protected parser: parser2.Parser;
 
    protected tagModifier: {[key:string]:{[key:string]:Transforms}} = {};
    onTag(tagname: string, attribute: string, pattern: RegExp, transformer: StringTransformer) {
        tagname = tagname.toLowerCase();
        Eif (this.tagModifier[tagname] == null) {
            this.tagModifier[tagname] = {};
        }
        attribute = attribute.toLowerCase();
        Eif (this.tagModifier[tagname][attribute] == null) {
            this.tagModifier[tagname][attribute] = [];
        }
        this.tagModifier[tagname][attribute].push([pattern, transformer]);
 
        return this;
    }
 
    protected beforeCloseTags: {[key:string]: (()=>string)[]} = {};
    onBeforeClosingTag(tagname: string, transformer: () => string) {
        tagname = tagname.toLowerCase();
        Eif (this.beforeCloseTags[tagname] == null) {
            this.beforeCloseTags[tagname] = [];
        }
        this.beforeCloseTags[tagname].push(transformer);
        return this;
    }
 
    protected textTransforms: Transforms = [];
    onText(pattern: RegExp, transformer: StringTransformer) {
        this.textTransforms.push([pattern, transformer]);
 
        return this;
    }
 
    protected _onText(text: string) {
        this.push(Transformer._transformString(text, this.textTransforms));
    }
 
    protected static _transformString(text: string, transforms: Transforms): string {
        for (const transform of transforms) {
            const execRes = transform[0].exec(text);
            if (execRes) {
                return transform[1](text, execRes);
            }
        }
        return text;
    }
 
    protected _onOpenTag(name: string, attrib: {[key: string]: string}) {
        const lowerName = name.toLowerCase();
        for (const tagKey of Object.keys(this.tagModifier)
            .filter(v => v === lowerName || v === "*")) {
            const attribModifiers = this.tagModifier[tagKey];
 
            for (const attribKey of Object.keys(attrib)) {
                for (const attribTarget of Object.keys(attribModifiers)
                    .filter(v => v === attribKey.toLowerCase() || v === "*")) {
                    const valueModifiers = attribModifiers[attribTarget];
                    attrib[attribKey] = Transformer._transformString(attrib[attribKey], valueModifiers);
                }
            }
        }
 
        const attribsStr = Object.keys(attrib).map(k => ` ${k}=\"${attrib[k]}\"`).join("");
        const trailling = isSelfClosedTag(name) ? "/" : "";
        if (isSelfClosedTag(name)) {
            this._onProcessCloseTag(name);
        }
        this.push(`<${name}${attribsStr}${trailling}>`);
    }
 
    protected _onProcessCloseTag(name: string) {
        if (this.beforeCloseTags[name]) {
            for(const transformer of this.beforeCloseTags[name]) {
                this.push(transformer());
            }
        }
    }
 
    protected _onCloseTag(name: string) {
        if (!isSelfClosedTag(name)) {
            this._onProcessCloseTag(name);
            this.push(`</${name}>`);
        }
    }
 
    protected _transform(chunk: string | Buffer, encoding: string, callback: (error?: Error, data?: string) => void): void {
        const chunkStr = typeof chunk === "string" ? chunk : chunk.toString('utf-8');
        this.parser.write(chunkStr);
        callback();
    }
 
    protected _flush(callback: () => void): void {
        this.parser.end();
    }
}