<commit_before>/**
    This plugin gives us the ability to insert the output of
    a program in a code-block in the docs, by using the following
    syntax:

        ```text [rasa --help]
        ```
    It is inspired by `remark-source` and also relies on a pre-build phase,
    before docusaurus is started (or built). It allows us to support separate
    versions of the docs (and of the program outputs).
*/
const fs = require('fs');
const globby = require('globby');
const visitChildren = require('unist-util-visit-children');
const { promisify } = require('util');

const exec = promisify(require('child_process').exec);
const { readFile, writeFile } = fs.promises;

const PROGRAM_OUTPUT_RE = /```[a-z]+ \[([^\]]+)\]\n```/;

const defaultOptions = {
    docsDir: './docs',
    sourceDir: './docs/sources',
    include: ['**.mdx', '**.md'],
    commandPrefix: '',
};

/**
    This function is use to get output of programs
    requested in the docs. It parses all the docs files,
    generates outputs and save them as files.

    Options:
    - docsDir:        the directory containing the docs files
    - sourceDir:      the directory that will contain the program outputs
    - include:        list of patterns to look for doc files
    - commandPrefix:  a prefix to be prepended before each command
*/
async function getProgramOutputs(options) {

    options = { ...defaultOptions, ...options };
    const { docsDir, include, sourceDir, commandPrefix } = options;
    // first, gather all the docs files
    const docsFiles = await globby(include, {
      cwd: docsDir,
    });
    const seen = new Set();
    // second, read every file source
    let commands = await Promise.all(docsFiles.map(async (source) => {
        const data = await readFile(`${docsDir}/${source}`);
        const commands = [];
        let group, command, stdout;
        // third, find out if there is a program output to be generated
        // there can be multiple outputs in the same file
        const re = new RegExp(PROGRAM_OUTPUT_RE, 'gi');
        while ((group = re.exec(data)) !== null) {
            command = group[1];
            if (seen.has(command)) {
                continue;
            }
            seen.add(command);
            // fourth, call the command to generate the output
            output = await exec(`${commandPrefix} ${command}`);
            commands.push([command, output.stdout]);
        }
        return commands;
    }));
    commands = commands.flat().filter(pair => pair.length > 0);

    // finally, write all the command outputs as files in the `sourceDir`
    return await Promise.all(commands.map(async ([command, output]) => {
        return await writeFile(`${sourceDir}/${commandToFilename(command)}`, output);
    }));
};

/**
    Custom remark plugin to replace the following blocks:

    ```text [rasa --help]
    ```

    with the actual output of the program (here `rasa --help`).
    It relies on the output of `getProgramOutputs()` above,
    and is inspired by `remark-sources` plugin.
*/
function remarkProgramOutput(options = {}) {
    options = { ...defaultOptions, ...options };
    return (root) => {
        visitChildren((node, index, parent) => {
            if (node && node.type === 'code') {
                const content = readCommandOutput(node.meta, options);
                if (content !== undefined) {
                    node.value = content;
                }
            }
        })(root);
    };
}

function readCommandOutput(meta, { sourceDir }) {
    if (!meta) {
        return undefined;
    }
    if (meta[0] !== '[' || meta[meta.length - 1] !== ']') {
        return undefined;
    }
    meta = meta.slice(1, -1);
    try {
        return fs.readFileSync(`${sourceDir}/${commandToFilename(meta)}`, { encoding: 'utf8' });
    } catch (e) {
        throw new Error(`Failed to read file: ${meta}`);
    }
}

function commandToFilename(command) {
    return command.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '.txt';
}

module.exports = getProgramOutputs;
module.exports.remarkProgramOutput = remarkProgramOutput;
<commit_msg>make remarkProgramOutput() version-aware<commit_after>/**
    This plugin gives us the ability to insert the output of
    a program in a code-block in the docs, by using the following
    syntax:

        ```text [rasa --help]
        ```
    It is inspired by `remark-source` and also relies on a pre-build phase,
    before docusaurus is started (or built). It allows us to support separate
    versions of the docs (and of the program outputs).

    Caveat: this plugin operates at 2 separate points in time:
        1. During the "pre-build" phase, pre-generating files inside the
           sources/ folder. This phase is run on the "main" version of the repo
           (master branch, release branches, tags, etc...), which doesn't have
           several versions of the docs site. This is why `getProgramOutputs()` is
           version-agnostic and simply outputs files in `mainSourceDir`.
        2. At the build phase, happening on the `documentation` branch. In this context,
           there are multiple versions of the docs, and the plugins needs to support that.
           This is why the `remarkProgramOutput()` is version-aware and takes care
           of loading the files from a versioned path.
*/
const fs = require('fs');
const globby = require('globby');
const visitChildren = require('unist-util-visit-children');
const { promisify } = require('util');

const exec = promisify(require('child_process').exec);
const { readFile, writeFile } = fs.promises;

const PROGRAM_OUTPUT_RE = /```[a-z]+ \[([^\]]+)\]\n```/;

const VERSIONED_DOCS_PATH_RE = /(\/versioned_docs\/version-\d+\.\d+\.x)\//;

const defaultOptions = {
    docsDir: './docs',
    sourceDirectoryName: 'sources',
    mainSourceDir: './docs/sources',
    include: ['**.mdx', '**.md'],
    commandPrefix: '',
};

/**
    This function is use to get output of programs
    requested in the docs. It parses all the docs files,
    generates outputs and save them as files.

    Options:
    - docsDir:              the directory containing the docs files
    - mainSourceDir:        the directory that will contain the program outputs
    - sourceDirectoryName:  the name (relative) of the source directory
    - include:              list of patterns to look for doc files
    - commandPrefix:        a prefix to be prepended before each command
*/
async function getProgramOutputs(options) {

    options = { ...defaultOptions, ...options };
    const { docsDir, include, mainSourceDir, commandPrefix } = options;
    // first, gather all the docs files
    const docsFiles = await globby(include, {
      cwd: docsDir,
    });
    const seen = new Set();
    // second, read every file source
    let commands = await Promise.all(docsFiles.map(async (source) => {
        const data = await readFile(`${docsDir}/${source}`);
        const commands = [];
        let group, command, stdout;
        // third, find out if there is a program output to be generated
        // there can be multiple outputs in the same file
        const re = new RegExp(PROGRAM_OUTPUT_RE, 'gi');
        while ((group = re.exec(data)) !== null) {
            command = group[1];
            if (seen.has(command)) {
                continue;
            }
            seen.add(command);
            // fourth, call the command to generate the output
            output = await exec(`${commandPrefix} ${command}`);
            commands.push([command, output.stdout]);
        }
        return commands;
    }));
    commands = commands.flat().filter(pair => pair.length > 0);

    // finally, write all the command outputs as files in the `mainSourceDir`
    return await Promise.all(commands.map(async ([command, output]) => {
        return await writeFile(`${mainSourceDir}/${commandToFilename(command)}`, output);
    }));
};

/**
    Custom remark plugin to replace the following blocks:

    ```text [rasa --help]
    ```

    with the actual output of the program (here `rasa --help`).
    It relies on the output of `getProgramOutputs()` above,
    and is inspired by `remark-sources` plugin.
*/
function remarkProgramOutput(options = {}) {
    options = { ...defaultOptions, ...options };
    return (root, { history }) => {
        visitChildren((node, index, parent) => {
            if (node && node.type === 'code') {
                const content = readCommandOutput(node.meta, options, history[0]);
                if (content !== undefined) {
                    node.value = content;
                }
            }
        })(root);
    };
}

function readCommandOutput(meta, { mainSourceDir, sourceDirectoryName }, filename) {
    if (!meta) {
        return undefined;
    }
    if (meta[0] !== '[' || meta[meta.length - 1] !== ']') {
        return undefined;
    }
    meta = meta.slice(1, -1);
    const sourceFile = `${getVersionedSourceDir(mainSourceDir, sourceDirectoryName, filename)}/${commandToFilename(meta)}`
    try {
        return fs.readFileSync(sourceFile, { encoding: 'utf8' });
    } catch (e) {
        throw new Error(`Failed to read file: ${sourceFile} for meta ${meta}`);
    }
}

function commandToFilename(command) {
    return command.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '.txt';
}

/**
    By analyzing the `filename` argument, we can guess if the file we want
    to load is in a specific version of the docs, or the main one.
*/
function getVersionedSourceDir(mainSourceDir, sourceDirectoryName, filename) {
    const re = new RegExp(VERSIONED_DOCS_PATH_RE, 'gi');
    const match = re.exec(filename);
    return match === null ? mainSourceDir : `.${match[1]}/${sourceDirectoryName}`;
}

module.exports = getProgramOutputs;
module.exports.remarkProgramOutput = remarkProgramOutput;
<|endoftext|>


<commit_before>/**
 * @module Router
 * @private
 * @mixin
 * @description Exports Router.
 */

import { Arr } from './Arr';
import { Elem, win } from './Elem';
import { self } from './Func';
import { Str } from './Str';
import { Super } from './Super';
import { switcher } from './Switcher';
import { assign, isNil, isRegExp, isString } from './helpers';
import constructURL from './helpers/constructURL';
import resolveURL from './helpers/resolveURL';

const Routes = new Arr([]);
const subscribers = new Super({});
const {
  history,
  location,
  location: {
    origin,
    href
  }
} = global;
let initialized;
let pushed;
let wasRoot;
let wasDefault;
let rootRoute;
let redirectRoute;
let RedirectRoute;
let defaultRoute;
let DefaultRoute;
let currentRoute;
let currentRouteParams;

const pathSwitcher = switcher('call', () => {
  throw new Error('State path must be a string, a regular expression or undefined! (at registerState)');
})
  .case(isRegExp, (path) => ({
    path: path.source.replace(/\\\//g, '/'),
    url: path,
    params: {}
  }))
  .case(isNil, () => ({
    path: '/',
    url: '/',
    params: {}
  }))
  .case(isString, (path) => {
    if (path.indexOf('/')) {
      throw new Error('If route path is a string it must start with "/"! (at registerState)');
    }

    const index = path.indexOf('?');
    const params = new Super({});
    let newURL = '';
    const newPath = new Str(path)
      .slice(0, index === -1 ? path.length : index)
      .replace(/^\/|\/$/g)
      .split(/\//)
      .map((part, i, array) => {
        if (!part && array.length > 1) {
          throw new Error('If route path is a string it must not contain "//" or end with "/"! (at makeRoute)');
        }

        const index = part.indexOf(':');

        if (index > 0) {
          throw new Error('If route path is a string resource part must be either a string or an URL parameter! (at makeRoute)');
        }

        if (index === -1) {
          return {
            url: part,
            value: part
          };
        }

        const {
          name,
          regexp = /[^/]*/
        } = resolveParameter(
          part.slice(1),
          'URL parameter must not be an empty string or contain characters besides "a-zA-Z_$"! (at makeRoute)',
          'URL parameter regexp validator must be within parentheses (e.g. :userId(\\d+) and not contain ones)! (at makeRoute)'
        );

        params.$[name] = params.count;

        return {
          type: 'param',
          url: `:${ name }`,
          value: regexp
        };
      })
      .word(({ type, url, value }) => {
        let newPath;

        if (type === 'param') {
          newPath = `(${ value.source.replace(/\\\//g, '/') })`;
        } else {
          newPath = new Str(value).escapeRegExp().$;
        }

        newURL += `/${ url }`;

        return `/${ newPath }`;
      });

    return {
      path: newPath,
      url: newURL,
      params: params.$
    };
  });

class Route {
  constructor(options) {
    options = options || {};

    const {
      name,
      path = '/',
      abstract = false,
      parent,
      decodeQuery = true,
      encodeQuery = true,
      decodeParams = true,
      encodeParams = true
    } = options || {};
    const {
      url: relativeURL,
      path: relativePath,
      params
    } = pathSwitcher(path);
    const query = {};

    new Super(this).assign({
      name,
      parentName: parent,
      abstract: !!abstract,
      children: new Arr([]),
      decodeParams: !!decodeParams,
      decodeQuery: !!decodeQuery,
      encodeParams: !!encodeParams,
      encodeQuery: !!encodeQuery,
      params,
      query,
      relativePath,
      relativeURL
    });

    const index = isString(path) ? path.indexOf('?') : -1;

    if (index !== -1) {
      new Str(path)
        .replace(/&$/)
        .slice(index + 1)
        .split('&')
        .forEach((param) => {
          const {
            name,
            regexp = /[\s\S]*/
          } = resolveParameter(
            param,
            'Query parameter must not be an empty string or contain characters besides "a-zA-Z_$"! (at makeRoute)',
            'Query parameter regexp validator must be within parentheses (e.g. :userId(\\d+)) and not contain them! (at makeRoute)'
          );

          query[name] = new RegExp(`^${ regexp.source.replace(/\\\//g, '/') }$`);
        });
    }

    if (name === defaultRoute && (
        new Super(params).count
        || new Super(query).count
      )) {
      throw new Error('Default route must not have URL or query params! (at makeRoute)');
    }
  }
}

const baseRoute = new Route();

function initRouter() {
  if (initialized) {
    return;
  }

  initialized = true;
  RedirectRoute = (Routes.find(({ name }) => name === redirectRoute) || {}).value;
  DefaultRoute = (Routes.find(({ name }) => name === defaultRoute) || {}).value;

  if (redirectRoute && !RedirectRoute) {
    throw new Error(`There is no specified fallback route ("${ redirectRoute }")! (at initRouter)`);
  }

  Routes
    .forEach((route) => {
      const {
        parentName,
        name
      } = route;
      const ParentName = parentName || rootRoute;
      const { value: parent } = Routes.find(({ name }) => name === ParentName) || {};

      if (!parent) {
        throw new Error(`No such parent route ("${ ParentName }") found for the route ("${ name }")! (at initRouter)`);
      }

      if (!parent.abstract && name !== rootRoute) {
        throw new Error(`Parent route must be abstract (for "${ name }")! (at initRouter)`);
      }

      if (name !== rootRoute) {
        route.parentName = ParentName;
      }

      route.parent = name === rootRoute
        ? baseRoute
        : parent;
    })
    .forEach((route) => {
      const {
        name,
        parent: {
          params: parentParams,
          query: parentQuery,
          path
        },
        params,
        query,
        relativeURL,
        relativePath
      } = route;
      let proto = route;
      let count = 0;
      let newPath = relativePath;
      let newURL = '';

      if (isRegExp(path)) {
        throw new Error('URL regexp route cannot be extended! (at initRouter)');
      }

      while (proto = proto.parent) {
        count += new Super(proto.params).count;
        newPath = proto.relativePath + newPath;
        newURL = proto.relativeURL + newURL;

        proto.children.push(route);
      }

      newPath = new RegExp(`^${ newPath.replace(/\/+/g, '/').replace(/\/$/, '') || '/' }$`);
      newURL = isRegExp(relativeURL)
        ? newPath
        : (newURL + relativeURL).replace(/\/+/g, '/').replace(/\/$/, '') || '/';

      new Super(query).proto(parentQuery);
      new Super(params)
        .proto(parentParams)
        .forEach((value, key, params) => {
          params[key] += count;
        });

      if (name === defaultRoute && (
          new Super(params).count
          || new Super(query).count
        )) {
        throw new Error('Default route must not have URL or query params! (at initRouter)');
      }

      route.url = newURL;
      route.validatePath = newPath;
    });

  changeRoute();

  win.on({
    popstate() {
      if (location.href !== href) {
        pushed = true;
      }

      if (pushed) {
        changeRoute();
      }
    },
    click(e) {
      const closestLink = new Elem(e.target).closest('a');

      if (closestLink.length && closestLink.attr('target') !== '_blank') {
        e.preventDefault();

        forward(closestLink.attr('href') || '', true);
      }
    }
  });
}

function makeRoute(options) {
  const {
    name,
    path,
    abstract,
    root,
    fallbackTo,
    default: isDefault
  } = options || {};

  if (initialized) {
    console.warn('Router was already initialized (at makeRoute)');

    return self;
  }

  if (wasRoot && root) {
    throw new Error(`There can't be two root routes ("${ rootRoute }" and "${ name }")! (at makeRoute)`);
  }

  if (wasDefault && isDefault) {
    throw new Error(`There can't be two default routes ("${ defaultRoute }" and "${ name }")! (at makeRoute)`);
  }

  if (!name) {
    throw new Error('State must have a non-empty string "name" property! (at makeRoute)');
  }

  if (Routes.some(({ name: Name }) => Name === name)) {
    throw new Error('State must have unique "name" property! (at makeRoute)');
  }

  if (root) {
    wasRoot = true;
    rootRoute = name;
    options.parent = null;

    if (fallbackTo) {
      redirectRoute = fallbackTo;
    }
  }

  if (isDefault) {
    wasDefault = true;
    defaultRoute = name;

    if (abstract) {
      throw new Error('Default route can\'t be abstract! (at makeRoute)');
    }

    if (isRegExp(path)) {
      throw new Error('Default route can\'t have a regexp path! (at makeRoute)');
    }
  }

  const route = new Route(options);

  Routes.push(route);

  let unsubscribe;
  let routeLoaded;

  return (Block) => (
    class extends Block {
      static template = `<d-if if="{__isCurrentRoute__}">${ Block.template }</d-if>`;

      constructor(opts) {
        super(opts);

        if (root) {
          initRouter();

          const router = {
            buildURL(name, options = {}) {
              const { value: route } = Routes.find(({ name: n }) => n === name) || {};

              if (!route) {
                throw new Error(`There are no routes with name "${ name }"! (at router.buildURL)`);
              }

              const {
                url,
                encodeParams,
                encodeQuery
              } = route;

              if (isRegExp(url)) {
                throw new Error('URL can be built only from the string URLs! (at router.buildURL)');
              }

              const {
                params = {},
                query = {},
                hash = ''
              } = options;

              return constructURL(origin, url, params, query, hash, {
                params: encodeParams,
                query: encodeQuery
              });
            },
            go(name, options) {
              forward(router.buildURL(name, options), true);
            },
            goToURL(url) {
              forward(url, true);
            },
            pushURL(url) {
              changeHistory(url, true);
            },
            redirect(name, options) {
              forward(router.buildURL(name, options));
            },
            redirectToURL(url) {
              forward(url);
            },
            replaceURL(url) {
              changeHistory(url);
            }
          };

          this.global.router = router;
        }

        this.__isCurrentRoute__ = (
          route === currentRoute
          || route.children.indexOf(currentRoute) !== -1
        );
        this.args.route = currentRouteParams;

        routeLoaded = false;
        unsubscribe = subscribe(name, () => {
          const isCurrentRoute = (
            route === currentRoute
            || route.children.indexOf(currentRoute) !== -1
          );

          if (isCurrentRoute) {
            callBeforeLoad(this);
          } else {
            callBeforeLeave(this);
          }

          this.args.route = currentRouteParams;
          this.__isCurrentRoute__ = isCurrentRoute;
        });

        if (this.__isCurrentRoute__) {
          callBeforeLoad(this);
        }
      }

      beforeRemove() {
        unsubscribe();
        unsubscribe = null;
        callBeforeLeave(this);
      }
    }
  );

  function callBeforeLoad(route) {
    if (route.beforeLoadRoute && !routeLoaded) {
      try {
        route.beforeLoadRoute();
      } catch (err) {
        console.error(`Uncaught error in ${ name }#beforeLeave:`, err);
      }
    }

    routeLoaded = true;
  }

  function callBeforeLeave(route) {
    if (route.beforeLeaveRoute && routeLoaded) {
      try {
        route.beforeLeaveRoute();
      } catch (err) {
        console.error(`Uncaught error in ${ name }#beforeLeave:`, err);
      }
    }

    routeLoaded = false;
  }
}

function subscribe(name, callback) {
  subscribers.$[name] = callback;

  return () => {
    subscribers.delete(name);
  };
}

function forward(url, push) {
  changeHistory(url, push);
  changeRoute();
}

function changeRoute() {
  const route = findRouteByURL();

  if (route) {
    ({
      route: currentRoute,
      ...currentRouteParams
    } = route);
    assign(currentRouteParams, {
      name: currentRoute.name,
      host: location.host,
      hostname: location.hostname,
      href: location.href,
      origin: location.origin,
      pathname: location.pathname,
      port: location.port,
      protocol: location.protocol,
      search: location.search
    });
  } else {
    if (redirectRoute) {
      const {
        url,
        encodeParams,
        encodeQuery
      } = RedirectRoute;

      return forward(constructURL(origin, url, {}, {}, '', {
        params: encodeParams,
        query: encodeQuery
      }));
    }

    currentRoute = null;
    currentRouteParams = null;
  }

  subscribers.forEach((callback) => callback());
}

function findRouteByURL() {
  const pathname = location.pathname || '/';
  const search = location.search || '';
  let urlParams;

  Routes.some((route) => {
    if (route.abstract) {
      return;
    }

    const {
      url: routeURL,
      validatePath,
      params,
      query: requiredQuery,
      decodeParams,
      decodeQuery
    } = route;
    const resolved = resolveURL(decodeQuery);
    const query = new Super(resolved.query);
    const eventualParams = {};
    const match = (
      (pathname.replace(/\/$/, '') || '/') +
      (isRegExp(routeURL) ? search : '')
    ).match(validatePath);

    if (!match) {
      return;
    }

    /* eslint guard-for-in: 0 */
    for (const param in requiredQuery) {
      if (!query.hasOwn(param) || !requiredQuery[param].test(query.$[param])) {
        return;
      }
    }

    match.shift();

    for (const param in params) {
      eventualParams[param] = decode(match[params[param]], decodeParams);
    }

    urlParams = {
      route,
      params: eventualParams,
      query: query.$,
      hash: resolved.hash
    };

    return true;
  });

  if (urlParams) {
    return urlParams;
  }

  if (!defaultRoute) {
    return;
  }

  return {
    route: DefaultRoute,
    params: {},
    ...resolveURL(DefaultRoute.decodeQuery)
  };
}

function decode(string, decodeParams) {
  return decodeParams
    ? decodeURIComponent(string)
    : string;
}

function changeHistory(url, push) {
  try {
    history[push ? 'pushState' : 'replaceState'](null, null, url);
    pushed = true;
  } catch (err) {
    location.href = url;
  }
}

function resolveParameter(param, nameErrorName, valueErrorName) {
  const nameMatch = param.match(/^[a-z_$]+/i);

  if (!nameMatch) {
    throw new Error(nameErrorName);
  }

  const name = nameMatch[0];
  const value = param.slice(name.length);
  let regexp;

  if (value && (value.indexOf('(') || value.indexOf(')') !== value.length - 1)) {
    throw new Error(valueErrorName);
  }

  if (value) {
    regexp = new RegExp(value.slice(1, -1));
  }

  return {
    name,
    regexp
  };
}

export { makeRoute };
<commit_msg>Router: replace d-if with div(d-show).<commit_after>/**
 * @module Router
 * @private
 * @mixin
 * @description Exports Router.
 */

import { Arr } from './Arr';
import { Elem, win } from './Elem';
import { self } from './Func';
import { Str } from './Str';
import { Super } from './Super';
import { switcher } from './Switcher';
import { assign, isNil, isRegExp, isString } from './helpers';
import constructURL from './helpers/constructURL';
import resolveURL from './helpers/resolveURL';

const Routes = new Arr([]);
const subscribers = new Super({});
const {
  history,
  location,
  location: {
    origin,
    href
  }
} = global;
let initialized;
let pushed;
let wasRoot;
let wasDefault;
let rootRoute;
let redirectRoute;
let RedirectRoute;
let defaultRoute;
let DefaultRoute;
let currentRoute;
let currentRouteParams;

const pathSwitcher = switcher('call', () => {
  throw new Error('State path must be a string, a regular expression or undefined! (at registerState)');
})
  .case(isRegExp, (path) => ({
    path: path.source.replace(/\\\//g, '/'),
    url: path,
    params: {}
  }))
  .case(isNil, () => ({
    path: '/',
    url: '/',
    params: {}
  }))
  .case(isString, (path) => {
    if (path.indexOf('/')) {
      throw new Error('If route path is a string it must start with "/"! (at registerState)');
    }

    const index = path.indexOf('?');
    const params = new Super({});
    let newURL = '';
    const newPath = new Str(path)
      .slice(0, index === -1 ? path.length : index)
      .replace(/^\/|\/$/g)
      .split(/\//)
      .map((part, i, array) => {
        if (!part && array.length > 1) {
          throw new Error('If route path is a string it must not contain "//" or end with "/"! (at makeRoute)');
        }

        const index = part.indexOf(':');

        if (index > 0) {
          throw new Error('If route path is a string resource part must be either a string or an URL parameter! (at makeRoute)');
        }

        if (index === -1) {
          return {
            url: part,
            value: part
          };
        }

        const {
          name,
          regexp = /[^/]*/
        } = resolveParameter(
          part.slice(1),
          'URL parameter must not be an empty string or contain characters besides "a-zA-Z_$"! (at makeRoute)',
          'URL parameter regexp validator must be within parentheses (e.g. :userId(\\d+) and not contain ones)! (at makeRoute)'
        );

        params.$[name] = params.count;

        return {
          type: 'param',
          url: `:${ name }`,
          value: regexp
        };
      })
      .word(({ type, url, value }) => {
        let newPath;

        if (type === 'param') {
          newPath = `(${ value.source.replace(/\\\//g, '/') })`;
        } else {
          newPath = new Str(value).escapeRegExp().$;
        }

        newURL += `/${ url }`;

        return `/${ newPath }`;
      });

    return {
      path: newPath,
      url: newURL,
      params: params.$
    };
  });

class Route {
  constructor(options) {
    options = options || {};

    const {
      name,
      path = '/',
      abstract = false,
      parent,
      decodeQuery = true,
      encodeQuery = true,
      decodeParams = true,
      encodeParams = true
    } = options || {};
    const {
      url: relativeURL,
      path: relativePath,
      params
    } = pathSwitcher(path);
    const query = {};

    new Super(this).assign({
      name,
      parentName: parent,
      abstract: !!abstract,
      children: new Arr([]),
      decodeParams: !!decodeParams,
      decodeQuery: !!decodeQuery,
      encodeParams: !!encodeParams,
      encodeQuery: !!encodeQuery,
      params,
      query,
      relativePath,
      relativeURL
    });

    const index = isString(path) ? path.indexOf('?') : -1;

    if (index !== -1) {
      new Str(path)
        .replace(/&$/)
        .slice(index + 1)
        .split('&')
        .forEach((param) => {
          const {
            name,
            regexp = /[\s\S]*/
          } = resolveParameter(
            param,
            'Query parameter must not be an empty string or contain characters besides "a-zA-Z_$"! (at makeRoute)',
            'Query parameter regexp validator must be within parentheses (e.g. :userId(\\d+)) and not contain them! (at makeRoute)'
          );

          query[name] = new RegExp(`^${ regexp.source.replace(/\\\//g, '/') }$`);
        });
    }

    if (name === defaultRoute && (
        new Super(params).count
        || new Super(query).count
      )) {
      throw new Error('Default route must not have URL or query params! (at makeRoute)');
    }
  }
}

const baseRoute = new Route();

function initRouter() {
  if (initialized) {
    return;
  }

  initialized = true;
  RedirectRoute = (Routes.find(({ name }) => name === redirectRoute) || {}).value;
  DefaultRoute = (Routes.find(({ name }) => name === defaultRoute) || {}).value;

  if (redirectRoute && !RedirectRoute) {
    throw new Error(`There is no specified fallback route ("${ redirectRoute }")! (at initRouter)`);
  }

  Routes
    .forEach((route) => {
      const {
        parentName,
        name
      } = route;
      const ParentName = parentName || rootRoute;
      const { value: parent } = Routes.find(({ name }) => name === ParentName) || {};

      if (!parent) {
        throw new Error(`No such parent route ("${ ParentName }") found for the route ("${ name }")! (at initRouter)`);
      }

      if (!parent.abstract && name !== rootRoute) {
        throw new Error(`Parent route must be abstract (for "${ name }")! (at initRouter)`);
      }

      if (name !== rootRoute) {
        route.parentName = ParentName;
      }

      route.parent = name === rootRoute
        ? baseRoute
        : parent;
    })
    .forEach((route) => {
      const {
        name,
        parent: {
          params: parentParams,
          query: parentQuery,
          path
        },
        params,
        query,
        relativeURL,
        relativePath
      } = route;
      let proto = route;
      let count = 0;
      let newPath = relativePath;
      let newURL = '';

      if (isRegExp(path)) {
        throw new Error('URL regexp route cannot be extended! (at initRouter)');
      }

      while (proto = proto.parent) {
        count += new Super(proto.params).count;
        newPath = proto.relativePath + newPath;
        newURL = proto.relativeURL + newURL;

        proto.children.push(route);
      }

      newPath = new RegExp(`^${ newPath.replace(/\/+/g, '/').replace(/\/$/, '') || '/' }$`);
      newURL = isRegExp(relativeURL)
        ? newPath
        : (newURL + relativeURL).replace(/\/+/g, '/').replace(/\/$/, '') || '/';

      new Super(query).proto(parentQuery);
      new Super(params)
        .proto(parentParams)
        .forEach((value, key, params) => {
          params[key] += count;
        });

      if (name === defaultRoute && (
          new Super(params).count
          || new Super(query).count
        )) {
        throw new Error('Default route must not have URL or query params! (at initRouter)');
      }

      route.url = newURL;
      route.validatePath = newPath;
    });

  changeRoute();

  win.on({
    popstate() {
      if (location.href !== href) {
        pushed = true;
      }

      if (pushed) {
        changeRoute();
      }
    },
    click(e) {
      const closestLink = new Elem(e.target).closest('a');

      if (closestLink.length && closestLink.attr('target') !== '_blank') {
        e.preventDefault();

        forward(closestLink.attr('href') || '', true);
      }
    }
  });
}

function makeRoute(options) {
  const {
    name,
    path,
    abstract,
    root,
    fallbackTo,
    default: isDefault
  } = options || {};

  if (initialized) {
    console.warn('Router was already initialized (at makeRoute)');

    return self;
  }

  if (wasRoot && root) {
    throw new Error(`There can't be two root routes ("${ rootRoute }" and "${ name }")! (at makeRoute)`);
  }

  if (wasDefault && isDefault) {
    throw new Error(`There can't be two default routes ("${ defaultRoute }" and "${ name }")! (at makeRoute)`);
  }

  if (!name) {
    throw new Error('State must have a non-empty string "name" property! (at makeRoute)');
  }

  if (Routes.some(({ name: Name }) => Name === name)) {
    throw new Error('State must have unique "name" property! (at makeRoute)');
  }

  if (root) {
    wasRoot = true;
    rootRoute = name;
    options.parent = null;

    if (fallbackTo) {
      redirectRoute = fallbackTo;
    }
  }

  if (isDefault) {
    wasDefault = true;
    defaultRoute = name;

    if (abstract) {
      throw new Error('Default route can\'t be abstract! (at makeRoute)');
    }

    if (isRegExp(path)) {
      throw new Error('Default route can\'t have a regexp path! (at makeRoute)');
    }
  }

  const route = new Route(options);

  Routes.push(route);

  let unsubscribe;
  let routeLoaded;

  return (Block) => (
    class extends Block {
      static template = `<div class="route-${ name }" d-show="{__isCurrentRoute__}">${ Block.template }</div>`;

      constructor(opts) {
        super(opts);

        if (root) {
          initRouter();

          const router = {
            buildURL(name, options = {}) {
              const { value: route } = Routes.find(({ name: n }) => n === name) || {};

              if (!route) {
                throw new Error(`There are no routes with name "${ name }"! (at router.buildURL)`);
              }

              const {
                url,
                encodeParams,
                encodeQuery
              } = route;

              if (isRegExp(url)) {
                throw new Error('URL can be built only from the string URLs! (at router.buildURL)');
              }

              const {
                params = {},
                query = {},
                hash = ''
              } = options;

              return constructURL(origin, url, params, query, hash, {
                params: encodeParams,
                query: encodeQuery
              });
            },
            go(name, options) {
              forward(router.buildURL(name, options), true);
            },
            goToURL(url) {
              forward(url, true);
            },
            pushURL(url) {
              changeHistory(url, true);
            },
            redirect(name, options) {
              forward(router.buildURL(name, options));
            },
            redirectToURL(url) {
              forward(url);
            },
            replaceURL(url) {
              changeHistory(url);
            }
          };

          this.global.router = router;
        }

        this.__isCurrentRoute__ = (
          route === currentRoute
          || route.children.indexOf(currentRoute) !== -1
        );
        this.args.route = currentRouteParams;

        routeLoaded = false;
        unsubscribe = subscribe(name, () => {
          const isCurrentRoute = (
            route === currentRoute
            || route.children.indexOf(currentRoute) !== -1
          );

          if (isCurrentRoute) {
            callBeforeLoad(this);
          } else {
            callBeforeLeave(this);
          }

          this.args.route = currentRouteParams;
          this.__isCurrentRoute__ = isCurrentRoute;
        });

        if (this.__isCurrentRoute__) {
          callBeforeLoad(this);
        }
      }

      beforeRemove() {
        unsubscribe();
        unsubscribe = null;
        callBeforeLeave(this);
      }
    }
  );

  function callBeforeLoad(route) {
    if (route.beforeLoadRoute && !routeLoaded) {
      try {
        route.beforeLoadRoute();
      } catch (err) {
        console.error(`Uncaught error in ${ name }#beforeLeave:`, err);
      }
    }

    routeLoaded = true;
  }

  function callBeforeLeave(route) {
    if (route.beforeLeaveRoute && routeLoaded) {
      try {
        route.beforeLeaveRoute();
      } catch (err) {
        console.error(`Uncaught error in ${ name }#beforeLeave:`, err);
      }
    }

    routeLoaded = false;
  }
}

function subscribe(name, callback) {
  subscribers.$[name] = callback;

  return () => {
    subscribers.delete(name);
  };
}

function forward(url, push) {
  changeHistory(url, push);
  changeRoute();
}

function changeRoute() {
  const route = findRouteByURL();

  if (route) {
    ({
      route: currentRoute,
      ...currentRouteParams
    } = route);
    assign(currentRouteParams, {
      name: currentRoute.name,
      host: location.host,
      hostname: location.hostname,
      href: location.href,
      origin: location.origin,
      pathname: location.pathname,
      port: location.port,
      protocol: location.protocol,
      search: location.search
    });
  } else {
    if (redirectRoute) {
      const {
        url,
        encodeParams,
        encodeQuery
      } = RedirectRoute;

      return forward(constructURL(origin, url, {}, {}, '', {
        params: encodeParams,
        query: encodeQuery
      }));
    }

    currentRoute = null;
    currentRouteParams = null;
  }

  subscribers.forEach((callback) => callback());
}

function findRouteByURL() {
  const pathname = location.pathname || '/';
  const search = location.search || '';
  let urlParams;

  Routes.some((route) => {
    if (route.abstract) {
      return;
    }

    const {
      url: routeURL,
      validatePath,
      params,
      query: requiredQuery,
      decodeParams,
      decodeQuery
    } = route;
    const resolved = resolveURL(decodeQuery);
    const query = new Super(resolved.query);
    const eventualParams = {};
    const match = (
      (pathname.replace(/\/$/, '') || '/') +
      (isRegExp(routeURL) ? search : '')
    ).match(validatePath);

    if (!match) {
      return;
    }

    /* eslint guard-for-in: 0 */
    for (const param in requiredQuery) {
      if (!query.hasOwn(param) || !requiredQuery[param].test(query.$[param])) {
        return;
      }
    }

    match.shift();

    for (const param in params) {
      eventualParams[param] = decode(match[params[param]], decodeParams);
    }

    urlParams = {
      route,
      params: eventualParams,
      query: query.$,
      hash: resolved.hash
    };

    return true;
  });

  if (urlParams) {
    return urlParams;
  }

  if (!defaultRoute) {
    return;
  }

  return {
    route: DefaultRoute,
    params: {},
    ...resolveURL(DefaultRoute.decodeQuery)
  };
}

function decode(string, decodeParams) {
  return decodeParams
    ? decodeURIComponent(string)
    : string;
}

function changeHistory(url, push) {
  try {
    history[push ? 'pushState' : 'replaceState'](null, null, url);
    pushed = true;
  } catch (err) {
    location.href = url;
  }
}

function resolveParameter(param, nameErrorName, valueErrorName) {
  const nameMatch = param.match(/^[a-z_$]+/i);

  if (!nameMatch) {
    throw new Error(nameErrorName);
  }

  const name = nameMatch[0];
  const value = param.slice(name.length);
  let regexp;

  if (value && (value.indexOf('(') || value.indexOf(')') !== value.length - 1)) {
    throw new Error(valueErrorName);
  }

  if (value) {
    regexp = new RegExp(value.slice(1, -1));
  }

  return {
    name,
    regexp
  };
}

export { makeRoute };
<|endoftext|>


<commit_before>const { Boards }    = require('boards');
const fileExists    = require('file-exists');
const procurator    = require('procurator');
const { Homefront } = require('homefront');
const path          = require('path');
const glob          = require('glob');

class Runner {
  constructor(config) {
    this.config = Homefront.merge({ tasks: {}, appRoot: '.', templateRoot: '.' }, config);
    this.boards = new Boards({ discoveryConfig: { prefix: 'boards-preset' } });
  }

  composeTask(task, parameters) {
    if (this.config.tasks[task]) {
      return this.config.tasks[task];
    }

    // Preset. Check if exists.
    const parts      = task.split(':');
    const presetName = parts.shift();
    const presets    = this.boards.getPlugins();
    const preset     = presets[presetName];
    task             = parts[0];

    if (!preset) {
      throw new Error(`Preset "${presetName}" not found.`);
    }

    parameters.fallbackSourceDirectory = preset.templateRoot;

    return preset.tasks[task];
  }

  run(task, parameters) {
    parameters = Homefront.merge({}, this.config.parameters || {}, parameters);

    return this.apply('default:prepare', parameters).then(() => {
      return this.apply(task, parameters)
    });
  }

  apply(task, parameters) {
    let instructions = task;

    if (typeof task === 'string') {
      instructions = this.composeTask(task, parameters);

      if (!instructions) {
        return Promise.reject(new Error(`Instructions for task "${task}" not found.`));
      }
    }

    if (!instructions) {
      return Promise.reject(new Error(`Invalid instructions provided.`));
    }

    if (!Array.isArray(instructions)) {
      instructions = [instructions];
    }

    let previousTask = null;

    return Promise.all(instructions.map(instruction => {
      let runner;

      if (previousTask) {
        runner       = previousTask.then(() => this.runTask(instruction, parameters));
        previousTask = null;
      } else {
        runner = this.runTask(instruction, parameters);
      }

      if (instruction.sync) {
        previousTask = runner;
      }

      return runner;
    }));
  }

  prepareParams(method, params) {
    const preparedParams = method(params);

    if (typeof preparedParams === 'object') {
      return preparedParams;
    }

    return params;
  }

  runTask(instruction, parameters) {
    instruction.from = instruction.from || '';

    // Prepare for step. Copy parameters to not affect other tasks.
    if (typeof instruction.prepare === 'function') {
      parameters = this.prepareParams(instruction.prepare, Homefront.merge({}, parameters));
    }

    // Allow dynamic tasks to be supplied.
    if (typeof instruction.dynamicTask === 'function') {
      return Promise.resolve(instruction.dynamicTask(parameters)).then(tasks => {
        this.apply(tasks, parameters);
      });
    }

    if (typeof instruction.definedTask !== 'undefined') {
      if (typeof instruction.definedTask !== 'string') {
        throw new Error(`definedTask must be a string. Got ${typeof instruction.definedTask}.`);
      }

      if (instruction.isolated && typeof instruction.prepare !== 'function') {
        parameters = Homefront.merge({}, parameters);
      }

      return this.apply(instruction.definedTask, parameters);
    }

    if (typeof instruction.task === 'function') {
      return instruction.task(parameters, this.boards);
    }

    if (typeof this[instruction.task] !== 'function') {
      throw new Error(`Invalid task "${instruction.task}" supplied`);
    }

    return this[instruction.task](instruction, parameters);
  }

  modify(instruction, parameters) {
    return this.boards.generate('ModificationGenerator', Object.assign({}, parameters, {
      sourceDirectory: this.config.appRoot,
      targetDirectory: this.config.appRoot,
      sourceFile     : this.getTarget(instruction.target, parameters),
      modify         : { patch: instruction.patch }
    }));
  }

  applyGenerate(template, target, sourceDirectory, instruction, parameters) {
    const parsed = path.parse(this.getTarget(target, parameters));

    return this.boards.generate('TemplateGenerator', Object.assign({}, parameters, {
      targetFile     : parsed.base,
      targetDirectory: path.join(this.config.appRoot, parsed.dir || '.'),
      sourceFile     : template,
      sourceDirectory
    }));
  }

  generate(instruction, parameters) {
    if (!instruction.glob) {
      const template = this.getTarget(instruction.template, parameters);

      return this.applyGenerate(template, instruction.target, this.getTemplateDirectory(instruction, parameters), instruction, parameters);
    }

    const glob         = this.getTarget(instruction.glob, parameters);
    const globPromises = [];

    if (this.config.templateRoot) {
      globPromises.push(this.glob(glob, path.join(this.config.templateRoot, instruction.from)));
    } else {
      // Push empty array so we can use array position further down
      globPromises.push(Promise.resolve([]));
    }

    if (parameters.fallbackSourceDirectory) {
      globPromises.push(this.glob(glob, path.join(parameters.fallbackSourceDirectory, instruction.from)));
    }

    return Promise.all(globPromises)
      .then(globbed => {
        const projectMatches = globbed[0];
        const generates      = [];
        const makeApply      = (match, dir) => {
          const target = path.join(instruction.target, match);

          generates.push(this.applyGenerate(target, target, path.join(dir, instruction.from), instruction, parameters));
        };

        projectMatches.forEach(match => makeApply(match, this.config.templateRoot));

        if (globbed[1]) {
          globbed[1]
            .filter(match => !projectMatches.includes(match))
            .forEach(match => makeApply(match, parameters.fallbackSourceDirectory));
        }

        return Promise.all(generates);
      });
  }

  getTemplateDirectory(instruction, parameters) {
    if (fileExists.sync(path.resolve(this.config.templateRoot, instruction.template))) {
      return this.config.templateRoot;
    }

    const fallback = parameters.fallbackSourceDirectory;

    if (!fallback) {
      throw new Error(`Template "${instruction.template}" not found.`);
    }

    if (!fileExists.sync(path.resolve(fallback, instruction.template))) {
      throw new Error(`Preset template "${instruction.template}" not found.`);
    }

    return fallback;
  }

  glob(pattern, cwd) {
    return new Promise((resolve, reject) => {
      glob(pattern, { nodir: true, cwd }, (error, results) => {
        if (error) {
          return reject(error);
        }

        return resolve(results);
      });
    });
  }

  getTarget(target, parameters) {
    if (typeof target === 'function') {
      return target(parameters);
    }

    return procurator.sync(target, parameters);
  }
}

module.exports.Runner = Runner;
<commit_msg>fix(Runner): load default preset and glob to target<commit_after>const { Boards }    = require('boards');
const fileExists    = require('file-exists');
const procurator    = require('procurator');
const { Homefront } = require('homefront');
const path          = require('path');
const glob          = require('glob');

class Runner {
  constructor(config) {
    this.config = Homefront.merge({ tasks: {}, appRoot: '.', templateRoot: '.' }, config);
    this.boards = new Boards({ discoveryConfig: { prefix: 'boards-preset' } });
  }

  composeTask(task, parameters) {
    if (this.config.tasks[task]) {
      return this.config.tasks[task];
    }

    // Preset. Check if exists.
    const parts      = task.split(':');
    const presetName = parts.shift();
    const presets    = this.getPresets();
    const preset     = presets[presetName];
    task             = parts[0];

    if (!preset) {
      throw new Error(`Preset "${presetName}" not found.`);
    }

    parameters.fallbackSourceDirectory = preset.templateRoot;

    return preset.tasks[task];
  }

  getPresets() {
    if (!this.presets) {
      this.presets         = this.boards.getPlugins();
      this.presets.default = require('boards-preset-default');
    }

    return this.presets;
  }

  run(task, parameters) {
    parameters = Homefront.merge({}, this.config.parameters || {}, parameters);

    return this.apply('default:prepare', parameters).then(() => {
      return this.apply(task, parameters)
    });
  }

  apply(task, parameters) {
    let instructions = task;

    if (typeof task === 'string') {
      instructions = this.composeTask(task, parameters);

      if (!instructions) {
        return Promise.reject(new Error(`Instructions for task "${task}" not found.`));
      }
    }

    if (!instructions) {
      return Promise.reject(new Error(`Invalid instructions provided.`));
    }

    if (!Array.isArray(instructions)) {
      instructions = [instructions];
    }

    let previousTask = null;

    return Promise.all(instructions.map(instruction => {
      let runner;

      if (previousTask) {
        runner       = previousTask.then(() => this.runTask(instruction, parameters));
        previousTask = null;
      } else {
        runner = this.runTask(instruction, parameters);
      }

      if (instruction.sync) {
        previousTask = runner;
      }

      return runner;
    }));
  }

  prepareParams(method, params) {
    const preparedParams = method(params);

    if (typeof preparedParams === 'object') {
      return preparedParams;
    }

    return params;
  }

  runTask(instruction, parameters) {
    instruction.from = instruction.from || '';

    // Prepare for step. Copy parameters to not affect other tasks.
    if (typeof instruction.prepare === 'function') {
      parameters = this.prepareParams(instruction.prepare, Homefront.merge({}, parameters));
    }

    // Allow dynamic tasks to be supplied.
    if (typeof instruction.dynamicTask === 'function') {
      return Promise.resolve(instruction.dynamicTask(parameters)).then(tasks => {
        this.apply(tasks, parameters);
      });
    }

    if (typeof instruction.definedTask !== 'undefined') {
      if (typeof instruction.definedTask !== 'string') {
        throw new Error(`definedTask must be a string. Got ${typeof instruction.definedTask}.`);
      }

      if (instruction.isolated && typeof instruction.prepare !== 'function') {
        parameters = Homefront.merge({}, parameters);
      }

      return this.apply(instruction.definedTask, parameters);
    }

    if (typeof instruction.task === 'function') {
      return instruction.task(parameters, this.boards);
    }

    if (typeof this[instruction.task] !== 'function') {
      throw new Error(`Invalid task "${instruction.task}" supplied`);
    }

    return this[instruction.task](instruction, parameters);
  }

  modify(instruction, parameters) {
    return this.boards.generate('ModificationGenerator', Object.assign({}, parameters, {
      sourceDirectory: this.config.appRoot,
      targetDirectory: this.config.appRoot,
      sourceFile     : this.getTarget(instruction.target, parameters),
      modify         : { patch: instruction.patch }
    }));
  }

  applyGenerate(template, target, sourceDirectory, instruction, parameters) {
    const parsed = path.parse(this.getTarget(target, parameters));

    return this.boards.generate('TemplateGenerator', Object.assign({}, parameters, {
      targetFile     : parsed.base,
      targetDirectory: path.join(this.config.appRoot, parsed.dir || '.'),
      sourceFile     : this.getTarget(template, parameters),
      sourceDirectory
    }));
  }

  generate(instruction, parameters) {
    if (!instruction.glob) {
      const template = this.getTarget(instruction.template, parameters);

      return this.applyGenerate(template, instruction.target, this.getTemplateDirectory(instruction, parameters), instruction, parameters);
    }

    const glob         = this.getTarget(instruction.glob, parameters);
    const globPromises = [];

    if (this.config.templateRoot) {
      globPromises.push(this.glob(glob, path.join(this.config.templateRoot, instruction.from)));
    } else {
      // Push empty array so we can use array position further down
      globPromises.push(Promise.resolve([]));
    }

    if (parameters.fallbackSourceDirectory) {
      globPromises.push(this.glob(glob, path.join(parameters.fallbackSourceDirectory, instruction.from)));
    }

    return Promise.all(globPromises)
      .then(globbed => {
        const projectMatches = globbed[0];
        const generates      = [];
        const makeApply      = (match, dir) => {
          const target = path.join(instruction.target, match);

          generates.push(this.applyGenerate(match, target, path.join(dir, instruction.from), instruction, parameters));
        };

        projectMatches.forEach(match => makeApply(match, this.config.templateRoot));

        if (globbed[1]) {
          globbed[1]
            .filter(match => !projectMatches.includes(match))
            .forEach(match => makeApply(match, parameters.fallbackSourceDirectory));
        }

        return Promise.all(generates);
      });
  }

  getTemplateDirectory(instruction, parameters) {
    if (fileExists.sync(path.resolve(this.config.templateRoot, instruction.template))) {
      return this.config.templateRoot;
    }

    const fallback = parameters.fallbackSourceDirectory;

    if (!fallback) {
      throw new Error(`Template "${instruction.template}" not found.`);
    }

    if (!fileExists.sync(path.resolve(fallback, instruction.template))) {
      throw new Error(`Preset template "${instruction.template}" not found.`);
    }

    return fallback;
  }

  glob(pattern, cwd) {
    return new Promise((resolve, reject) => {
      glob(pattern, { nodir: true, cwd }, (error, results) => {
        if (error) {
          return reject(error);
        }

        return resolve(results);
      });
    });
  }

  getTarget(target, parameters) {
    if (typeof target === 'function') {
      return target(parameters);
    }

    return procurator.sync(target, parameters);
  }
}

module.exports.Runner = Runner;
<|endoftext|>


<commit_before>'use strict'
const log = require('debug')
var debug = log('netcat:server')
const fs = require('fs')
const Netcat = require('./Netcat')
const tcpServer = require('./tcpServer')
const udpServer = require('./udpServer')
const stream = require('stream')

function noop () {}

class Server extends Netcat {
  constructor (opts) {
    opts = opts || {}
    super(opts)

    debug = opts.verbose ? function () {
      var args = Array.prototype.slice.call(arguments)
      process.stderr.write(args.join(' ') + '\n')
    } : debug
    this.debug = debug
    this._address = opts.address || '0.0.0.0'
    this._keepalive = false
    this._clients = {}
  }

  keepalive (bool) {
    this._keepalive = typeof bool !== 'undefined' ? bool : true
    debug('Keepalive connection.')
    return this /* chainable method */
  }

  k (bool) { /* keepalive alias */
    return this.keepalive(bool) /* chainable method */
  }

  close (cb) {
    debug('Server: closing sockets...')
    cb = cb || noop
    for (var c in this._clients) {
      this._clients[c].destroy() // closing existing sockets
    }
    if (this.server) this.server.close(cb)
    return this
  }

  listen () {
    debug('listen called')
    if (this._protocol === 'tcp') {
      tcpServer.call(this, debug)
      this.server.listen(this._unixSocket || {host: this._address, port: this._port})
    } else { // udp
      if (!this._port) throw Error('listen(): setting a port is mandatory.')
      udpServer.call(this, debug)
      this.server.bind(this._port, this._address)
    }

    return this /* chainable method */
  }

  pipe (outputStream) {
    debug('Pipe method called')
    this.passThrough.pipe(outputStream, { end: !this._keepalive })
    return this
  }

  send (msg, host) {
    if (this._protocol !== 'udp') throw Error('Cannot use send() in TCP, use serve() instead')
    if (this.server) {
      this.server.write(msg, host)
      debug('Sending', msg, host ? 'to ' + host : 'to 255.255.255.255')
    } else {
      debug('send(): server instance does not exists!')
    }
    return this
  }

  serve (input) {
    if (typeof input === 'string') { // is a file
      debug('Serving given file:', input)
      fs.statSync(input) // check that file exists
      this._serveFile = input
    } else if (Buffer.isBuffer(input)) { // is a Buffer
      debug('Serving given buffer:', input)
      this._serveStream = input
    } else if (input instanceof stream.Stream) { // is a stream
      debug('Serving given stream')
      this._serveStream = input
    }
    return this /* chainable method */
  }

  getClients () {
    if (this._protocol !== 'tcp') throw Error('Cannot use getClients() in UDP')
    return this._clients
  }

  proxy (duplexStream) {
    if (this._protocol !== 'tcp') throw Error('Cannot use proxy() in UDP')
    debug('Proxying requests...')
    this.serve(duplexStream)
    this.pipe(duplexStream)
    return this
  }
}

module.exports = Server

if (!module.parent) {
  const nc = new Server()
  // nc.port(2389).listen().k().serve(process.stdin).pipe(process.stdout)

  nc.port(2389).k().listen()
}
<commit_msg>udp server.bind moved to udpServer file<commit_after>'use strict'
const log = require('debug')
var debug = log('netcat:server')
const fs = require('fs')
const Netcat = require('./Netcat')
const tcpServer = require('./tcpServer')
const udpServer = require('./udpServer')
const stream = require('stream')

function noop () {}

class Server extends Netcat {
  constructor (opts) {
    opts = opts || {}
    super(opts)

    debug = opts.verbose ? function () {
      var args = Array.prototype.slice.call(arguments)
      process.stderr.write(args.join(' ') + '\n')
    } : debug
    this.debug = debug
    this._address = opts.address || '0.0.0.0'
    this._keepalive = false
    this._clients = {}
  }

  keepalive (bool) {
    this._keepalive = typeof bool !== 'undefined' ? bool : true
    debug('Keepalive connection.')
    return this /* chainable method */
  }

  k (bool) { /* keepalive alias */
    return this.keepalive(bool) /* chainable method */
  }

  close (cb) {
    debug('Server: closing sockets...')
    cb = cb || noop
    for (var c in this._clients) {
      this._clients[c].destroy() // closing existing sockets
    }
    if (this.server) this.server.close(cb)
    return this
  }

  listen () {
    debug('listen called')
    if (this._protocol === 'tcp') {
      tcpServer.call(this, debug)
      this.server.listen(this._unixSocket || {host: this._address, port: this._port})
    } else { // udp
      if (!this._port) throw Error('listen(): setting a port is mandatory.')
      udpServer.call(this, debug)
    }

    return this /* chainable method */
  }

  pipe (outputStream) {
    debug('Pipe method called')
    this.passThrough.pipe(outputStream, { end: !this._keepalive })
    return this
  }

  send (msg, host) {
    if (this._protocol !== 'udp') throw Error('Cannot use send() in TCP, use serve() instead')
    if (this.server) {
      this.server.write(msg, host)
      debug('Sending', msg, host ? 'to ' + host : 'to 255.255.255.255')
    } else {
      debug('send(): server instance does not exists!')
    }
    return this
  }

  serve (input) {
    if (typeof input === 'string') { // is a file
      debug('Serving given file:', input)
      fs.statSync(input) // check that file exists
      this._serveFile = input
    } else if (Buffer.isBuffer(input)) { // is a Buffer
      debug('Serving given buffer:', input)
      this._serveStream = input
    } else if (input instanceof stream.Stream) { // is a stream
      debug('Serving given stream')
      this._serveStream = input
    }
    return this /* chainable method */
  }

  getClients () {
    if (this._protocol !== 'tcp') throw Error('Cannot use getClients() in UDP')
    return this._clients
  }

  proxy (duplexStream) {
    if (this._protocol !== 'tcp') throw Error('Cannot use proxy() in UDP')
    debug('Proxying requests...')
    this.serve(duplexStream)
    this.pipe(duplexStream)
    return this
  }
}

module.exports = Server

if (!module.parent) {
  const nc = new Server()
  // nc.port(2389).listen().k().serve(process.stdin).pipe(process.stdout)

  nc.port(2389).k().listen()
}
<|endoftext|>


<commit_before>let express = require('express');
let Mustache = require('mustache');
let fs = require('fs');
let app = express();

class PresentationServer {

    constructor(presentations, config) {

        config = config ? config : {};
        presentations = presentations ? presentations : [];

        this.config = Object.assign({
            resources: __dirname + '/../node_modules',
            lib: __dirname + '/../node_modules/reveal.js/lib',
            themes: __dirname + '/../node_modules/reveal.js/css/theme',
            styles: __dirname + '/../node_modules/highlight.js/src/styles'
        }, config);

        this.presentationContainer = this.loadContainer(__dirname + '/../templates/presentationContainer.mustache');
        this.configurationContainer = this.loadContainer(__dirname + '/../templates/configurationContainer.mustache');

        presentations.unshift({
            id: '404',
            title: 'Presentation not found',
            path: __dirname + '/static/not-found.html'
        });

        presentations.unshift({
            id: 'demo',
            title: 'Demonstration',
            path: __dirname + '/static/demo.html'
        });

        presentations.forEach(function(presentation) {
            if (presentations.filter(p => p.id == presentation.id).length > 1) {
                throw "Duplicate presentation id detected, please rename '" + presentation.id + "'";
            }
        });

        this.presentations = presentations.map(presentation => {
            try {
                presentation.slides = fs.readFileSync(presentation.path).toString();
            } catch (err) {
                throw "Unable to load presentation path -> '" + presentation.path + "'";
            }
            return presentation;
        }).map(presentations => this.buildConfig(presentations));

        this.themes = this.loadCSSResources(this.config.themes);
        this.styles = this.loadCSSResources(this.config.styles);
    }

    loadContainer(path) {
        try {
            return fs.readFileSync(path).toString();
        } catch (err) {
            throw "Unable to load container '" + path + "' -> " + err;
        }
    }

    loadCSSResources(path) {
        try {
            return fs.readdirSync(path)
                .toString()
                .split(',')
                .filter(file => file.endsWith('.css'))
                .map(file => file.replace('.css', ''));
        } catch (err) {
            throw "Unable to load resource directory '" + path + "' -> " + err;
        }
    }

    clone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }

    buildConfig(config) {
        return Object.assign({
            title: 'Presentation',
            theme: 'black',
            style: 'vs2015',
            controls: false,
            loop: false
        }, config);
    }

    generateSection(presentation, config) {
        return '
' +
            '' +
            (config ?
                '
Use your left/right arrows to switch between different themes, while the up/down arrows to control the code highlight style
' +
                '
Theme: default Style: default
' +
                'Start'
                :
                '
Use your left/right arrows to switch between different presentations
' +
                'Start | ' +
                'Configure'
            ) +
            '
';
    }

    constructHomePage() {
        return this.presentations
            .filter(presentation => presentation.id != '404')
            .reduce((arr, presentation) => {
                arr.push(this.generateSection(presentation, false));
                return arr;
            }, []).join('\n');
    }

    constructConfigPage(presentation) {
        return [this.generateSection(presentation, true), this.generateSection(presentation, true)].join('\n');
    }

    getPresentation(req) {

        let findPresentation = function (id) {
            return this.presentations.find(presentation => {
                return presentation.id == id;
            });
        }.bind(this);

        let presentation = findPresentation(req.params.id);

        if (!presentation) {
            presentation = findPresentation('404');
        }

        presentation = this.clone(presentation);

        presentation.theme = req.query.theme && this.themes.indexOf(req.query.theme) > -1 ? req.query.theme : presentation.theme;
        presentation.style = req.query.style && this.styles.indexOf(req.query.style) > -1 ? req.query.style : presentation.style;
        return presentation;
    }

    start(port) {

        port = port ? port : 8080;

        Object.keys(this.config).map(resource => {
            app.use('/' + resource, express.static(this.config[resource]));
        });

        app.get('/api/themes', (req, res) => {
            res.json(this.themes);
        });

        app.get('/api/styles', (req, res) => {
            res.json(this.styles);
        });

        app.get('/api/presentations', (req, res) => {
            res.json(this.clone(this.presentations)
                .filter(presentation => presentation.id != '404')
                .map(presentation => {
                    delete presentation.slides;
                    delete presentation.path;
                    return presentation
                }));
        });

        app.get('/', (req, res) => {
            res.redirect('/presentations');
        });

        app.get('/presentations', (req, res) => {
            var presentation = this.buildConfig({
                title: 'Home',
                theme: 'night',
                slides: this.constructHomePage(),
                loop: true
            });
            var output = Mustache.render(this.presentationContainer, presentation);
            res.contentType('text/html');
            res.send(output);
        });

        app.get('/presentations/:id', (req, res) => {
            let presentation = this.getPresentation(req);
            var output = Mustache.render(this.presentationContainer, presentation);
            res.contentType('text/html');
            res.send(output);
        });

        app.get('/presentations/:id/configure', (req, res) => {
            let presentation = this.getPresentation(req);
            var content = this.buildConfig({
                id: presentation.id,
                title: 'Presentation Configuration',
                theme: 'night',
                defaultTheme: presentation.theme,
                defaultStyle: presentation.style,
                slides: this.constructConfigPage(presentation)
            });
            var output = Mustache.render(this.configurationContainer, content);
            res.contentType('text/html');
            res.send(output);
        });

        app.listen(port, (err) => {
            console.info("Presentation Server is running on port " + port);
        });

    }

}

module.exports = (presentations, config) => new PresentationServer(presentations, config);<commit_msg>fixed resource paths<commit_after>let express = require('express');
let Mustache = require('mustache');
let fs = require('fs');
let app = express();

class PresentationServer {

    constructor(presentations, config) {

        config = config ? config : {};
        presentations = presentations ? presentations : [];

        this.config = Object.assign({
            resources: __dirname + '/../..',
            lib: __dirname + '/../../reveal.js/lib',
            themes: __dirname + '/../../reveal.js/css/theme',
            styles: __dirname + '/../../highlight.js/src/styles'
        }, config);

        this.presentationContainer = this.loadContainer(__dirname + '/../templates/presentationContainer.mustache');
        this.configurationContainer = this.loadContainer(__dirname + '/../templates/configurationContainer.mustache');

        presentations.unshift({
            id: '404',
            title: 'Presentation not found',
            path: __dirname + '/../static/not-found.html'
        });

        presentations.unshift({
            id: 'demo',
            title: 'Demonstration',
            path: __dirname + '/../static/demo.html'
        });

        presentations.forEach(function(presentation) {
            if (presentations.filter(p => p.id == presentation.id).length > 1) {
                throw "Duplicate presentation id detected, please rename '" + presentation.id + "'";
            }
        });

        this.presentations = presentations.map(presentation => {
            try {
                presentation.slides = fs.readFileSync(presentation.path).toString();
            } catch (err) {
                throw "Unable to load presentation path -> '" + presentation.path + "'";
            }
            return presentation;
        }).map(presentations => this.buildConfig(presentations));

        this.themes = this.loadCSSResources(this.config.themes);
        this.styles = this.loadCSSResources(this.config.styles);
    }

    loadContainer(path) {
        try {
            return fs.readFileSync(path).toString();
        } catch (err) {
            throw "Unable to load container '" + path + "' -> " + err;
        }
    }

    loadCSSResources(path) {
        try {
            return fs.readdirSync(path)
                .toString()
                .split(',')
                .filter(file => file.endsWith('.css'))
                .map(file => file.replace('.css', ''));
        } catch (err) {
            throw "Unable to load resource directory '" + path + "' -> " + err;
        }
    }

    clone(obj) {
        return JSON.parse(JSON.stringify(obj));
    }

    buildConfig(config) {
        return Object.assign({
            title: 'Presentation',
            theme: 'black',
            style: 'vs2015',
            controls: false,
            loop: false
        }, config);
    }

    generateSection(presentation, config) {
        return '
' +
            '' +
            (config ?
                '
Use your left/right arrows to switch between different themes, while the up/down arrows to control the code highlight style
' +
                '
Theme: default Style: default
' +
                'Start'
                :
                '
Use your left/right arrows to switch between different presentations
' +
                'Start | ' +
                'Configure'
            ) +
            '
';
    }

    constructHomePage() {
        return this.presentations
            .filter(presentation => presentation.id != '404')
            .reduce((arr, presentation) => {
                arr.push(this.generateSection(presentation, false));
                return arr;
            }, []).join('\n');
    }

    constructConfigPage(presentation) {
        return [this.generateSection(presentation, true), this.generateSection(presentation, true)].join('\n');
    }

    getPresentation(req) {

        let findPresentation = function (id) {
            return this.presentations.find(presentation => {
                return presentation.id == id;
            });
        }.bind(this);

        let presentation = findPresentation(req.params.id);

        if (!presentation) {
            presentation = findPresentation('404');
        }

        presentation = this.clone(presentation);

        presentation.theme = req.query.theme && this.themes.indexOf(req.query.theme) > -1 ? req.query.theme : presentation.theme;
        presentation.style = req.query.style && this.styles.indexOf(req.query.style) > -1 ? req.query.style : presentation.style;
        return presentation;
    }

    start(port) {

        port = port ? port : 8080;

        Object.keys(this.config).map(resource => {
            app.use('/' + resource, express.static(this.config[resource]));
        });

        app.get('/api/themes', (req, res) => {
            res.json(this.themes);
        });

        app.get('/api/styles', (req, res) => {
            res.json(this.styles);
        });

        app.get('/api/presentations', (req, res) => {
            res.json(this.clone(this.presentations)
                .filter(presentation => presentation.id != '404')
                .map(presentation => {
                    delete presentation.slides;
                    delete presentation.path;
                    return presentation
                }));
        });

        app.get('/', (req, res) => {
            res.redirect('/presentations');
        });

        app.get('/presentations', (req, res) => {
            var presentation = this.buildConfig({
                title: 'Home',
                theme: 'night',
                slides: this.constructHomePage(),
                loop: true
            });
            var output = Mustache.render(this.presentationContainer, presentation);
            res.contentType('text/html');
            res.send(output);
        });

        app.get('/presentations/:id', (req, res) => {
            let presentation = this.getPresentation(req);
            var output = Mustache.render(this.presentationContainer, presentation);
            res.contentType('text/html');
            res.send(output);
        });

        app.get('/presentations/:id/configure', (req, res) => {
            let presentation = this.getPresentation(req);
            var content = this.buildConfig({
                id: presentation.id,
                title: 'Presentation Configuration',
                theme: 'night',
                defaultTheme: presentation.theme,
                defaultStyle: presentation.style,
                slides: this.constructConfigPage(presentation)
            });
            var output = Mustache.render(this.configurationContainer, content);
            res.contentType('text/html');
            res.send(output);
        });

        app.listen(port, (err) => {
            console.info("Presentation Server is running on port " + port);
        });

    }

}

module.exports = (presentations, config) => new PresentationServer(presentations, config);<|endoftext|>


<commit_before>'use strict';

var _           = require('lodash'),
    util        = require('util'),
    Promise     = require('bluebird'),
    events      = require('events'),
    functionify = require('functionify'),

    Thread      = require('./Thread'),
    Job         = require('./Job'),
    pending     = require('./PendingPromise');

/**
 * Contains the options for the Worqer constructor
 * @typedef {Object} WorqerOptions
 * @property {function|number} [timeout=5000] Either a timeout in ms or a function that returns the timeout
 * @property {number} [concurrency=1] The number of jobs that can run simultaneously
 * @property {WorqerStateOperator} [open] Function that opens the Worqer
 * @property {WorqerStateOperator} [close] Function that closes the Worqer
 */

/**
 * Function that handles jobs in a Worqer
 * @typedef {function} WorqerProcessor
 * @param {*} data The input data assigned to the job
 * @param {number} threadNo The number of the thread the job was assigned to
 * @return {Promise<*>|*} The output data
 */

/**
 * Function that opens or closes a Worqer
 * @typedef {function} WorqerStateOperator
 * @return {Promise|void} If a promise is returned, the Worqer will wait until the promise is resolved
 */

/**
 * Creates a new Worqer
 * @param {WorqerProcessor} [processor] Function that handles the jobs passed to this Worqer. If not provided, any
 *     value, function or promise can be passed to this Worqer.
 * @param {WorqerOptions} [options] Options for the Worqer
 * @constructor
 */
var Worqer = function (processor, options) {

    if (!_.isFunction(processor) && _.isUndefined(options)) {
        processor = undefined;
        options = processor;
    }

    var o = _.assign({
        timeout:      0,
        concurrency:  1,
        passThreadNo: false
    }, options);

    this._state = Worqer.states.CLOSED;
    this._pendingState = null;
    this._queue = [];

    this._threads = _.times(o.concurrency, function () {
        return new Thread();
    });

    this._fn = {
        process: typeof processor !== typeof undefined ? processor : function (obj) {
            return functionify(obj)();
        },
        open:    functionify(o.open),
        close:   functionify(o.close)
    };

    this._passThreadNo = o.passThreadNo;
    this._getTimeout = functionify(o.timeout);
    this._pendingClose = null;
};

/**
 * State constants for Worqer
 * @private
 */
Worqer.states = {
    CLOSED:  0,
    OPENING: 1,
    OPEN:    2,
    CLOSING: 3
};

/**
 * State constants for Worqer
 * @private
 */
Worqer.events = {
    OPENED: 'opened',
    CLOSED: 'closed',
    IDLE:   'idle',
    START:  'start'
};

util.inherits(Worqer, events.EventEmitter);

/**
 * Starts the job in the thread
 * @param {Thread} thread
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._runThread = function (thread) {
    var job = thread.job;
    job.emit(Worqer.events.START);
    var threadNo = _.indexOf(this._threads, thread);
    var args = job.getData().concat(this._passThreadNo ? [threadNo] : []);
    var jobResult = Promise.resolve(this._fn.process.apply(this, args));
    jobResult.then(function (result) {
        job.emit('result', result);
    }).catch(function (err) {
        job.emit('error', err);
    }).done(function () {
        thread.job = null;
        this._processQueue();
        this._setPendingClose();
    }.bind(this));
    return this;
};

/**
 * Processes the job queue. This method is invoked every time a job finishes running and when a new job is added
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._processQueue = function () {
    if (this.hasJobsPending()) {
        this._safeOpen().then(function () {
            var thread;
            while ((thread = this._getOpenThread()) !== undefined && this.hasJobsPending()) {
                thread.job = this._queue.shift();
                this._runThread(thread);
            }
        }.bind(this)).done();
    } else if (!this.hasActiveThreads()) {
        this.emit(Worqer.events.IDLE);
    }
    return this;
};

/**
 * Sets a new pending promise for the Worqer to close after the specified timeout
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._setPendingClose = function () {
    this._cancelPendingClose();
    this._pendingClose = pending(this._getTimeout(), true);
    this._pendingClose.then(function () {
        if (!this.hasActiveThreads()) {
            this._safeClose();
        }
    }.bind(this)).catch(function (reason) {
        // The pending close was cancelled
    }).done();
    return this;
};

/**
 * If there is still a close pending, cancels it
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._cancelPendingClose = function () {
    if (this._pendingClose !== null) {
        this._pendingClose.cancel(false);
        this._pendingClose = null;
    }
    return this;
};

/**
 * Facilitates a transition from one state to another, if applicable
 * @param {number} fromState The state to transition from
 * @param {number} transitioningState The state the Worqer will be set to while transitioning
 * @param {number} toState The state to transition to
 * @param {function} func The function that will change the state
 * @param {string} transitionedEvent The event that will be fired once transitioned
 * @param {function} [callback] A function to call once transitioned
 * @returns {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._transitionState = function (fromState, transitioningState, toState, func, transitionedEvent, callback) {
    if (this._pendingState === toState && this._state === fromState) {
        this._state = transitioningState;
        Promise.resolve(func()).then(function () {
            this._state = toState;
            functionify(callback)();
            this.emit(transitionedEvent);
            this._processPendingState();
        }.bind(this)).done();
    }
    return this;
};

/**
 * Processes the pending state, if applicable
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._processPendingState = function () {
    this._transitionState(
        Worqer.states.CLOSED, Worqer.states.OPENING, Worqer.states.OPEN, this._fn.open, Worqer.events.OPENED,
        function () {
            this._setPendingClose();
        }.bind(this));
    this._transitionState(
        Worqer.states.OPEN, Worqer.states.CLOSING, Worqer.states.CLOSED, this._fn.close, Worqer.events.CLOSED);
    return this;
};

/**
 * Gets the first open thread in the thread stack, or if there are no open threads, returns undefined
 * @returns {Thread|undefined}
 * @private
 */
Worqer.prototype._getOpenThread = function () {
    return _.find(this._threads, function (thread) {
        return thread.isOpen();
    });
};

/**
 * Promises to change the state.
 * @param {number} state The target state
 * @param {number} pendingState The state once the open/close action has begun
 * @param {string} event The event that fires once the state has changed
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeChangeState = function (state, pendingState, event) {
    if (this._state === state)
        return Promise.resolve();
    if (this._state === pendingState)
        return new Promise(function (resolve) {
            this.once(event, resolve);
        }.bind(this));
    else
        return new Promise(function (resolve) {
            this._pendingState = state;
            this._processPendingState()
                .once(event, resolve);
        }.bind(this));
};

/**
 * Safe function for requesting the handle to open. Returns a promise for opening the handle.
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeOpen = function () {
    return this._safeChangeState(Worqer.states.OPEN, Worqer.states.OPENING, Worqer.events.OPENED);
};

/**
 * Safe function for requesting the handle to close. Returns a promise for closing the handle.
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeClose = function () {
    this._cancelPendingClose();
    return this._safeChangeState(Worqer.states.CLOSED, Worqer.states.CLOSING, Worqer.events.CLOSED);
};

/**
 * Adds the given data to the job queue, returns a promise for the output data.
 * Any arguments passed to this function will be passed to the Worqer's process function in the same order.
 * @return {Promise<*>} A promise for the data returned by this Worqer's process function
 */
Worqer.prototype.process = function () {
    var args = [].splice.call(arguments, 0);
    return new Promise(function (resolve, reject) {
        var job = new Job(args);
        this._queue.push(job);
        this._processQueue();
        job.once('result', function (result) {
            resolve(result);
        }).once('error', function (err) {
            reject(err);
        });
    }.bind(this));
};

/**
 * Determines whether any of the threads is running a job
 * @returns {boolean}
 */
Worqer.prototype.hasActiveThreads = function () {
    return _.some(this._threads, function (thread) {
        return thread.job !== null;
    });
};

/**
 * Returns whether there are jobs waiting to be handled in the queue
 * @returns {boolean}
 */
Worqer.prototype.hasJobsPending = function () {
    return !!this._queue.length;
};

/**
 * Returns whether the handle is currently open
 * @returns {boolean}
 */
Worqer.prototype.isOpen = function () {
    return this._state === Worqer.states.OPEN;
};

/**
 * Returns whether the handle is currently closed
 * @returns {boolean}
 */
Worqer.prototype.isClosed = function () {
    return this._state === Worqer.states.CLOSED;
};

/**
 * Requests for the handle to close
 * @param {boolean} [graceful = true] Whether to let jobs in the job queue run first. If not, only lets
 *     currently running jobs finish and rejects any other jobs in the the job queue.
 * @returns {Promise}
 */
Worqer.prototype.close = function (graceful) {
    graceful = typeof graceful === typeof true ? graceful : true;
    if (!graceful) {
        _.each(this._queue, function (job) {
            job.emit('error', new Error('The handle was closed forcefully'));
        });
        this._queue = [];
    }
    if (this.hasActiveThreads() || this.hasJobsPending()) {
        return new Promise(function (resolve) {
            this.once(Worqer.events.IDLE, function () {
                resolve(this._safeClose());
            }.bind(this));
        }.bind(this));
    } else {
        return this._safeClose();
    }
};

module.exports = Worqer;<commit_msg>Set _pendingState to null after state has changed<commit_after>'use strict';

var _           = require('lodash'),
    util        = require('util'),
    Promise     = require('bluebird'),
    events      = require('events'),
    functionify = require('functionify'),

    Thread      = require('./Thread'),
    Job         = require('./Job'),
    pending     = require('./PendingPromise');

/**
 * Contains the options for the Worqer constructor
 * @typedef {Object} WorqerOptions
 * @property {function|number} [timeout=5000] Either a timeout in ms or a function that returns the timeout
 * @property {number} [concurrency=1] The number of jobs that can run simultaneously
 * @property {WorqerStateOperator} [open] Function that opens the Worqer
 * @property {WorqerStateOperator} [close] Function that closes the Worqer
 */

/**
 * Function that handles jobs in a Worqer
 * @typedef {function} WorqerProcessor
 * @param {*} data The input data assigned to the job
 * @param {number} threadNo The number of the thread the job was assigned to
 * @return {Promise<*>|*} The output data
 */

/**
 * Function that opens or closes a Worqer
 * @typedef {function} WorqerStateOperator
 * @return {Promise|void} If a promise is returned, the Worqer will wait until the promise is resolved
 */

/**
 * Creates a new Worqer
 * @param {WorqerProcessor} [processor] Function that handles the jobs passed to this Worqer. If not provided, any
 *     value, function or promise can be passed to this Worqer.
 * @param {WorqerOptions} [options] Options for the Worqer
 * @constructor
 */
var Worqer = function (processor, options) {

    if (!_.isFunction(processor) && _.isUndefined(options)) {
        processor = undefined;
        options = processor;
    }

    var o = _.assign({
        timeout:      0,
        concurrency:  1,
        passThreadNo: false
    }, options);

    this._state = Worqer.states.CLOSED;
    this._pendingState = null;
    this._queue = [];

    this._threads = _.times(o.concurrency, function () {
        return new Thread();
    });

    this._fn = {
        process: typeof processor !== typeof undefined ? processor : function (obj) {
            return functionify(obj)();
        },
        open:    functionify(o.open),
        close:   functionify(o.close)
    };

    this._passThreadNo = o.passThreadNo;
    this._getTimeout = functionify(o.timeout);
    this._pendingClose = null;
};

/**
 * State constants for Worqer
 * @private
 */
Worqer.states = {
    CLOSED:  0,
    OPENING: 1,
    OPEN:    2,
    CLOSING: 3
};

/**
 * State constants for Worqer
 * @private
 */
Worqer.events = {
    OPENED: 'opened',
    CLOSED: 'closed',
    IDLE:   'idle',
    START:  'start'
};

util.inherits(Worqer, events.EventEmitter);

/**
 * Starts the job in the thread
 * @param {Thread} thread
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._runThread = function (thread) {
    var job = thread.job;
    job.emit(Worqer.events.START);
    var threadNo = _.indexOf(this._threads, thread);
    var args = job.getData().concat(this._passThreadNo ? [threadNo] : []);
    var jobResult = Promise.resolve(this._fn.process.apply(this, args));
    jobResult.then(function (result) {
        job.emit('result', result);
    }).catch(function (err) {
        job.emit('error', err);
    }).done(function () {
        thread.job = null;
        this._processQueue();
        this._setPendingClose();
    }.bind(this));
    return this;
};

/**
 * Processes the job queue. This method is invoked every time a job finishes running and when a new job is added
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._processQueue = function () {
    if (this.hasJobsPending()) {
        this._safeOpen().then(function () {
            var thread;
            while ((thread = this._getOpenThread()) !== undefined && this.hasJobsPending()) {
                thread.job = this._queue.shift();
                this._runThread(thread);
            }
        }.bind(this)).done();
    } else if (!this.hasActiveThreads()) {
        this.emit(Worqer.events.IDLE);
    }
    return this;
};

/**
 * Sets a new pending promise for the Worqer to close after the specified timeout
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._setPendingClose = function () {
    this._cancelPendingClose();
    this._pendingClose = pending(this._getTimeout(), true);
    this._pendingClose.then(function () {
        if (!this.hasActiveThreads()) {
            this._safeClose();
        }
    }.bind(this)).catch(function (reason) {
        // The pending close was cancelled
    }).done();
    return this;
};

/**
 * If there is still a close pending, cancels it
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._cancelPendingClose = function () {
    if (this._pendingClose !== null) {
        this._pendingClose.cancel(false);
        this._pendingClose = null;
    }
    return this;
};

/**
 * Facilitates a transition from one state to another, if applicable
 * @param {number} fromState The state to transition from
 * @param {number} transitioningState The state the Worqer will be set to while transitioning
 * @param {number} toState The state to transition to
 * @param {function} func The function that will change the state
 * @param {string} transitionedEvent The event that will be fired once transitioned
 * @param {function} [callback] A function to call once transitioned
 * @returns {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._transitionState = function (fromState, transitioningState, toState, func, transitionedEvent, callback) {
    if (this._pendingState === toState && this._state === fromState) {
        this._state = transitioningState;
        Promise.resolve(func()).then(function () {
            this._state = toState;
            this._pendingState = null;
            functionify(callback)();
            this.emit(transitionedEvent);
            this._processPendingState();
        }.bind(this)).done();
    }
    return this;
};

/**
 * Processes the pending state, if applicable
 * @return {Worqer} this The instance on which the method was called, for chaining purposes
 * @private
 */
Worqer.prototype._processPendingState = function () {
    this._transitionState(
        Worqer.states.CLOSED, Worqer.states.OPENING, Worqer.states.OPEN, this._fn.open, Worqer.events.OPENED,
        function () {
            this._setPendingClose();
        }.bind(this));
    this._transitionState(
        Worqer.states.OPEN, Worqer.states.CLOSING, Worqer.states.CLOSED, this._fn.close, Worqer.events.CLOSED);
    return this;
};

/**
 * Gets the first open thread in the thread stack, or if there are no open threads, returns undefined
 * @returns {Thread|undefined}
 * @private
 */
Worqer.prototype._getOpenThread = function () {
    return _.find(this._threads, function (thread) {
        return thread.isOpen();
    });
};

/**
 * Promises to change the state.
 * @param {number} state The target state
 * @param {number} pendingState The state once the open/close action has begun
 * @param {string} event The event that fires once the state has changed
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeChangeState = function (state, pendingState, event) {
    if (this._state === state)
        return Promise.resolve();
    if (this._state === pendingState)
        return new Promise(function (resolve) {
            this.once(event, resolve);
        }.bind(this));
    else
        return new Promise(function (resolve) {
            this._pendingState = state;
            this._processPendingState()
                .once(event, resolve);
        }.bind(this));
};

/**
 * Safe function for requesting the handle to open. Returns a promise for opening the handle.
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeOpen = function () {
    return this._safeChangeState(Worqer.states.OPEN, Worqer.states.OPENING, Worqer.events.OPENED);
};

/**
 * Safe function for requesting the handle to close. Returns a promise for closing the handle.
 * @returns {Promise}
 * @private
 */
Worqer.prototype._safeClose = function () {
    this._cancelPendingClose();
    return this._safeChangeState(Worqer.states.CLOSED, Worqer.states.CLOSING, Worqer.events.CLOSED);
};

/**
 * Adds the given data to the job queue, returns a promise for the output data.
 * Any arguments passed to this function will be passed to the Worqer's process function in the same order.
 * @return {Promise<*>} A promise for the data returned by this Worqer's process function
 */
Worqer.prototype.process = function () {
    var args = [].splice.call(arguments, 0);
    return new Promise(function (resolve, reject) {
        var job = new Job(args);
        this._queue.push(job);
        this._processQueue();
        job.once('result', function (result) {
            resolve(result);
        }).once('error', function (err) {
            reject(err);
        });
    }.bind(this));
};

/**
 * Determines whether any of the threads is running a job
 * @returns {boolean}
 */
Worqer.prototype.hasActiveThreads = function () {
    return _.some(this._threads, function (thread) {
        return thread.job !== null;
    });
};

/**
 * Returns whether there are jobs waiting to be handled in the queue
 * @returns {boolean}
 */
Worqer.prototype.hasJobsPending = function () {
    return !!this._queue.length;
};

/**
 * Returns whether the handle is currently open
 * @returns {boolean}
 */
Worqer.prototype.isOpen = function () {
    return this._state === Worqer.states.OPEN;
};

/**
 * Returns whether the handle is currently closed
 * @returns {boolean}
 */
Worqer.prototype.isClosed = function () {
    return this._state === Worqer.states.CLOSED;
};

/**
 * Requests for the handle to close
 * @param {boolean} [graceful = true] Whether to let jobs in the job queue run first. If not, only lets
 *     currently running jobs finish and rejects any other jobs in the the job queue.
 * @returns {Promise}
 */
Worqer.prototype.close = function (graceful) {
    graceful = typeof graceful === typeof true ? graceful : true;
    if (!graceful) {
        _.each(this._queue, function (job) {
            job.emit('error', new Error('The handle was closed forcefully'));
        });
        this._queue = [];
    }
    if (this.hasActiveThreads() || this.hasJobsPending()) {
        return new Promise(function (resolve) {
            this.once(Worqer.events.IDLE, function () {
                resolve(this._safeClose());
            }.bind(this));
        }.bind(this));
    } else {
        return this._safeClose();
    }
};

module.exports = Worqer;<|endoftext|>