yoda-seo-tips
Version:
SEO tips from Yoda
101 lines (82 loc) • 2.67 kB
text/typescript
import * as cheerio from 'cheerio';
import { Stream, Writable } from 'stream';
import { createReadStream, createWriteStream } from 'fs';
import { promisify } from 'util';
export type Input = string | Stream;
export type Output = string | {write(msg: string): void} | null;
export interface Rule {
($: CheerioStatic, report: (msg: string) => void): void;
};
export class HtmlValidator {
constructor(private rules: Rule[]) {}
async process(input: Input, output: Output) {
let inputStream =
typeof input === 'string' ? createReadStream(input)
: input && typeof input.on === 'function' ? input
: error('input: expecting filename or readable stream as input')
var writer =
!output ? new ConsoleWriter
: typeof output === 'string' ? new FileWriter(output)
: typeof output.write === 'function' ? new StreamWriter(output)
: error('output: expecting filename or writable stream');
let data = await consumeStream(inputStream);
let cheerioDom = cheerio.load(data);
for(let rule of this.rules) {
rule(cheerioDom, msg => {
writer.write(msg + '\n');
});
}
await writer.close();
}
}
class StreamWriter {
constructor(private stream: {write(msg: string): void}) {}
write(msg : string) {
this.stream.write(msg);
}
async close() {}
}
class ConsoleWriter {
async write(msg: string) {
console.log(msg.replace(/\s*$/, ''));
}
async close() {}
}
class FileWriter {
error?: any;
stream = createWriteStream(this.filename, {flags: 'w'});
constructor(private filename: string) {
this.stream.on('error', err => { this.error = err });
}
write(msg: string) {
if(this.error) throw this.error;
this.stream.write(msg);
}
async close() {
if(this.error) throw this.error;
return new Promise((resolve, reject) => {
this.stream.on('error', reject);
this.stream.on('finish', () => {
this.stream.close();
this.stream.destroy();
resolve();
});
this.stream.end();
})
}
}
function consumeStream(stream: Stream) : Promise<string> {
return new Promise((resolve, reject) => {
let data = '';
stream.on('data', chunk => {
data += chunk;
});
stream.on('end', () => {
resolve(data);
});
stream.on('error', reject);
});
}
function error(msg: string): never {
throw new Error(msg);
}