lib/hexo/router.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { EventEmitter } from 'events';
import Promise from 'bluebird';
import Stream from 'stream';
const { Readable } = Stream;

interface Data {
  data: any;
  modified: boolean;
}

declare module 'stream' {
  export default class _Stream extends Stream {
    readable: boolean;
  }
}

class RouteStream extends Readable {
  public _data: any;
  public _ended: boolean;
  public modified: boolean;

  constructor(data: Data) {
    super({ objectMode: true });

    this._data = data.data;
    this._ended = false;
    this.modified = data.modified;
  }

  // Assume we only accept Buffer, plain object, or string
  _toBuffer(data: Buffer | object | string): Buffer | null {
    if (data instanceof Buffer) {
      return data;
    }
    if (typeof data === 'object') {
      data = JSON.stringify(data);
    }
    if (typeof data === 'string') {
      return Buffer.from(data); // Assume string is UTF-8 encoded string
    }
    return null;
  }

  _read(): boolean {
    const data = this._data;

    if (typeof data !== 'function') {
      const bufferData = this._toBuffer(data);
      if (bufferData) {
        this.push(bufferData);
      }
      this.push(null);
      return;
    }

    // Don't read it twice!
    if (this._ended) return false;
    this._ended = true;

    data().then(data => {
      if (data instanceof Stream && data.readable) {
        data.on('data', d => {
          this.push(d);
        });

        data.on('end', () => {
          this.push(null);
        });

        data.on('error', err => {
          this.emit('error', err);
        });
      } else {
        const bufferData = this._toBuffer(data);
        if (bufferData) {
          this.push(bufferData);
        }
        this.push(null);
      }
    }).catch(err => {
      this.emit('error', err);
      this.push(null);
    });
  }
}

const _format = (path?: string): string => {
  path = path || '';
  if (typeof path !== 'string') throw new TypeError('path must be a string!');

  path = path
    .replace(/^\/+/, '') // Remove prefixed slashes
    .replace(/\\/g, '/') // Replaces all backslashes
    .replace(/\?.*$/, ''); // Remove query string

  // Appends `index.html` to the path with trailing slash
  if (!path || path.endsWith('/')) {
    path += 'index.html';
  }

  return path;
};

class Router extends EventEmitter {
  public routes: {
    [key: string]: Data | null;
  };
  public emit: any;

  constructor() {
    super();

    this.routes = {};
  }

  list(): string[] {
    const { routes } = this;
    return Object.keys(routes).filter(key => routes[key]);
  }

  format(path?: string): string {
    return _format(path);
  }

  get(path: string): RouteStream {
    if (typeof path !== 'string') throw new TypeError('path must be a string!');

    const data = this.routes[this.format(path)];
    if (data == null) return;

    return new RouteStream(data);
  }

  isModified(path: string): boolean {
    if (typeof path !== 'string') throw new TypeError('path must be a string!');

    const data = this.routes[this.format(path)];
    return data ? data.modified : false;
  }

  set(path: string, data: any): this {
    if (typeof path !== 'string') throw new TypeError('path must be a string!');
    if (data == null) throw new TypeError('data is required!');

    let obj: Data;

    if (typeof data === 'object' && data.data != null) {
      obj = data;
    } else {
      obj = {
        data,
        modified: true
      };
    }

    if (typeof obj.data === 'function') {
      if (obj.data.length) {
        obj.data = Promise.promisify(obj.data);
      } else {
        obj.data = Promise.method(obj.data);
      }
    }

    path = this.format(path);

    this.routes[path] = {
      data: obj.data,
      modified: obj.modified == null ? true : obj.modified
    };

    this.emit('update', path);

    return this;
  }

  remove(path: string): this {
    if (typeof path !== 'string') throw new TypeError('path must be a string!');
    path = this.format(path);

    this.routes[path] = null;
    this.emit('remove', path);

    return this;
  }
}

export = Router;