<commit_before>/*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
regexp:true, undef:true, strict:true, trailing:true, white:true */
/*global X:true, XM:true, XT:true, console:true, _:true*/

/*!
 * Connect - xTuple - PostgreSQL Store
 *
 * connect-xt-pg is a PostgreSQL session store that uses xTuple's glabal database
 * to persist session data.  To speed up session reads, Express's Session MemoryStore
 * is used as a cache.
 *
 * Modeled off of:
 * - https://github.com/visionmedia/connect-redis/blob/master/lib/connect-redis.js
 * - https://github.com/kcbanner/connect-mongo/blob/master/lib/connect-mongo.js
 * - https://github.com/jebas/connect-pg/blob/master/lib/connect-pg.js
 */

/**
 * Return the `XTPGStore` extending `connect`'s session Store.
 *
 * @param {object} connect
 * @return {Function}
 * @api public
 */

module.exports = function (connect) {
  "use strict";

  /**
   * Connect's Store.
   */
  var Store = connect.session.Store,
      // TODO - If we ever run multiple processes/servers, MemoryStore must be replaced with something
      // all processes/servers can use/share and keep in sync like Redis.
      MemoryStore = new connect.session.MemoryStore();

  /**
   * Initialize XTPGStore with the given `options`.
   *
   * @param {Object} options
   * @api public
   */
  function XTPGStore(options) {
    var self = this;

    options = options || {};
    Store.call(this, options);
    this.prefix = null == options.prefix ? 'sess:' : options.prefix;
    this.hybridCache = null == options.hybridCache ? false : options.hybridCache;

    X.debug("this.hybridCache = ", this.hybridCache);

    // Load all the data from XM.SessionStore into the Express MemoryStore for caching.
    this.loadCache = function (options) {
      // TODO - options could be used to only load parital dataset of recently active sessions.
      // It could also be used to help process/server syncing if we move to something like Redis.

      if (self.hybridCache) {
        var fetchOptions = {};

        fetchOptions.success = function (sessions) {
          var callback,
              sid,
              sess;

          callback = function () {
            // TODO - Don't need to do anything here, but MemoryStore.set() needs a callback.
          };

          _.each(sessions, function (model) {
            sid = model.id;
            sess = JSON.parse(model.session);

            // Store the session data in the Express MemeoryStore for caching.
            MemoryStore.set(sid, sess, callback);
          });
        };
        fetchOptions.error = function (sessions, err) {
          X.debug("Session Collection fetch failed.");
        };

        // TODO - This is REALLY SLOW if there are 10,000 sessions in the table and we use collection.fetch().
        // var sessionsCollection = new XM.SessionStoreCollection();
        //sessionsCollection.fetch(fetchOptions);

        // Fetch all records from XM.SessionStore and load them into the Express MemoryStore.
        fetchOptions.query = {
          requestType: "fetch",
          recordType: "XM.SessionStore"
        };

        // fetchOptions.username = GLOBAL_USERNAME; // TODO
        fetchOptions.username = 'node';
        XT.dataSource.fetch(fetchOptions);
      }
    };
  }

  /**
   * Inherit from `Store`.
   */
  XTPGStore.prototype = new Store();

  /**
   * Attempt to fetch session by the given `sid`.
   *
   * @param {String} sid
   * @param {Function} fn
   * @api public
   */
  XTPGStore.prototype.get = function (sid, done) {
    try {
      var fetchCache,
          fetchDB,
          that = this;

      sid = this.prefix + sid;

      fetchDB = function () {
        var fetchOptions = {},
            result,
            sessionStore = {};

        sessionStore = new XM.SessionStore();
        fetchOptions.id = sid;

        fetchOptions.success = function (model) {
          // We have a matching session store cookie

          // TODO - update lastModified time to extend timeout?
          // Doing this will complicate things with a hybridCache MemoryStore.
          // Do we really need to set that here for just in XM.Session and have CleanupTask
          // clean up XM.SessionStore on timeouts?
          //model.set("lastModified", new Date().getTime());
          //model.save(null, saveOptions);

          if (model.get("session")) {
            result = JSON.parse(model.get("session"));
          }

          if (that.hybridCache) {
            // We fetched from the database because this session was not in the MemoryStore.
            // Load this data into the MemoryStore to speed this up next time.
            MemoryStore.set(sid, result, function (err, cache) {
              if (err) {
                // Could not set. Return error and move along...
                console.trace("MemoryStore.set error:");
                done && done(err);
              } else {
                // Return the session data back to Express to be used by routes and functors.
                return done(null, result);
              }
            });
          } else {
            // Return the session data back to Express to be used by routes and functors.
            return done(null, result);
          }
        };
        fetchOptions.error = function (model, err) {
          // Session was not found. This can happen if cookie is still in the browser, but
          // db record was removed by CleanupTask because it has timed out.

          // This is called when MemoryStore did not find session data.
          // No need to touch MemoryStore here.

          // Nothing found anywhere, return error and move along...
          done && done(err);
        };

        // Try to fetch a session matching the user's cookie sid.
        sessionStore.fetch(fetchOptions);
      };

      if (this.hybridCache) {
        // Try to get session data from MemoryStore.
        fetchCache = function (err, cache) {
          if (cache && !err) {
            return done(null, cache);
          } else {
            fetchDB();
          }
        };
        MemoryStore.get(sid, fetchCache);
      } else {
        fetchDB();
      }
    } catch (err) {
      done && done(err);
    }
  };

  /**
   * Commit the given `sess` object associated with the given `sid`.
   *
   * @param {String} sid
   * @param {Session} sess
   * @param {Function} fn
   * @api public
   */
  XTPGStore.prototype.set = function (sid, sess, done) {
    try {
      var fetchCache,
          fetchDB,
          that = this;

      sid = this.prefix + sid;
      sess = JSON.stringify(sess);

      fetchDB = function () {
        var fetchOptions = {},
            saveOptions = {},
            sessionAttributes = {},
            sessionStore = {};

        sessionStore = new XM.SessionStore();

        saveOptions.success = function (model) {
          if (that.hybridCache) {
            // Overwrite the MemoryStore with the new data, replacing old or creating new entry.
            MemoryStore.set(sid, JSON.parse(sess), function (err, cache) {
              if (err) {
                // Could not set. Return error and move along.
                console.trace("MemoryStore.set error:");
                done && done();
              } else {
                // Success, MemoryStore updated, move along.
                done && done();
              }
            });
          } else {
            // Success, move along.
            done && done();
          }
        };
        saveOptions.error = function (model, err) {
          // This shouldn't happen. How did we get here? Log trace.
          console.trace("XM.SessionStore save error. This shouldn't happen.");

          if (that.hybridCache) {
            // Delete any match in MemoryStore.
            MemoryStore.destroy(sid, function (err, cache) {
              if (err) {
                // Could not destroy. Return error and move along.
                console.trace("MemoryStore.destroy error:");
                done && done(err);
              } else {
                // MemoryStore destroyed, move along.
                done && done(err);
              }
            });
          } else {
            // Send error and move along.
            done && done(err);
          }
        };

        fetchOptions.id = sid;

        fetchOptions.success = function (model) {
          // Fetch found this session, update it and save.
          model.set("session", sess);

          // Set gets called a lot. There isn't always a change to save and save will fail.
          // Check if this model has changed before trying to save it.
          if (model.getStatusString() === "READY_CLEAN") {
            if (that.hybridCache) {
              // MemoryStore did not have a matching session, update existing or add new.
              MemoryStore.set(sid, JSON.parse(sess), function (err, cache) {
                if (err) {
                  // Could not set. Return error and move along.
                  console.trace("MemoryStore.set error:");
                  done && done();
                } else {
                  // Nothing to save, MemoryStore updated, move along.
                  done && done();
                }
              });
            } else {
              // Nothing to save, move along.
              done && done();
            }
          } else if (model.getStatusString() === "READY_DIRTY") {
            // Try to save XM.SessionStore to database.
            model.save(null, saveOptions);
          }
        };
        fetchOptions.error = function (model, err) {
          // Fetch did not find this session, initialize new and save.
          // Create new XM.SessionStore object.
          sessionStore = new XM.SessionStore();
          // TODO - Is this redundant?  Can I just call model.initialize...???
          sessionStore.initialize(null, {isNew: true});

          sessionAttributes = {
            id: sid,
            session: sess
          };

          // Try to save XM.SessionStore to database.
          sessionStore.save(sessionAttributes, saveOptions);
        };

        // Try to fetch a session matching the user's cookie sid to
        // see if we need to make a new one or update the old one.
        sessionStore.fetch(fetchOptions);
      };

      if (this.hybridCache) {
        // Try to get session data from MemoryStore.
        fetchCache = function (err, cache) {
          if (cache && !err) {
            if (JSON.stringify(cache, null, null) === sess) {
              // No change to the session data, nothing to save, move along.
              done && done();
            } else {
              fetchDB();
            }
          } else {
            fetchDB();
          }
        };
        MemoryStore.get(sid, fetchCache);
      } else {
        fetchDB();
      }
    } catch (err) {
      done && done(err);
    }
  };

  /**
   * Destroy the session associated with the given `sid`.
   *
   * @param {String} sid
   * @api public
   */
  XTPGStore.prototype.destroy = function (sid, done) {
    try {
      var sessionStore = {},
          fetchOptions = {},
          that = this;

      sid = this.prefix + sid;
      sessionStore = new XM.SessionStore();
      fetchOptions.id = sid;

      fetchOptions.success = function (model) {
        // Delete this session from the db store.
        model.destroy();

        if (that.hybridCache) {
          // Delete this session from the MemoryStore as well.
          MemoryStore.destroy(sid, function (err, cache) {
            if (err) {
              // Could not destroy. Return error and move along.
              console.trace("MemoryStore.destroy error:");
              done && done(err);
            } else {
              // MemoryStore destroyed, move along.
              done && done();
            }
          });
        } else {
          done && done();
        }
      };
      fetchOptions.error = function (model, err) {
        if (that.hybridCache) {
          // Did not find a match in the db, delete this session from the MemoryStore, it's invalid.
          MemoryStore.destroy(sid, function (err, cache) {
            if (err) {
              // Could not destroy. Return error and move along.
              console.trace("MemoryStore.destroy error:");
              done && done(err);
            } else {
              // MemoryStore destroyed, move along.
              done && done(err);
            }
          });
        } else {
          done && done(err);
        }
      };

      // Try to fetch a session matching the user's cookie sid.
      sessionStore.fetch(fetchOptions);
    } catch (err) {
      done && done(err);
    }
  };

  return XTPGStore;
};<commit_msg>Issue #19285 Cleanup session store and change some error returns.<commit_after>/*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
regexp:true, undef:true, strict:true, trailing:true, white:true */
/*global X:true, XM:true, XT:true, console:true, _:true*/

/*!
 * Connect - xTuple - PostgreSQL Store
 *
 * connect-xt-pg is a PostgreSQL session store that uses xTuple's glabal database
 * to persist session data.  To speed up session reads, Express's Session MemoryStore
 * is used as a cache.
 *
 * Modeled off of:
 * - https://github.com/visionmedia/connect-redis/blob/master/lib/connect-redis.js
 * - https://github.com/kcbanner/connect-mongo/blob/master/lib/connect-mongo.js
 * - https://github.com/jebas/connect-pg/blob/master/lib/connect-pg.js
 */

/**
 * Return the `XTPGStore` extending `connect`'s session Store.
 *
 * @param {object} connect
 * @return {Function}
 * @api public
 */

module.exports = function (connect) {
  "use strict";

  /**
   * Connect's Store.
   */
  var Store = connect.session.Store,
      // TODO - If we ever run multiple processes/servers, MemoryStore must be replaced with something
      // all processes/servers can use/share and keep in sync like Redis.
      MemoryStore = new connect.session.MemoryStore();

  /**
   * Initialize XTPGStore with the given `options`.
   *
   * @param {Object} options
   * @api public
   */
  function XTPGStore(options) {
    var self = this;

    options = options || {};
    Store.call(this, options);
    this.prefix = null == options.prefix ? 'sess:' : options.prefix;
    this.hybridCache = null == options.hybridCache ? false : options.hybridCache;

    X.debug("this.hybridCache = ", this.hybridCache);

    // Load all the data from XM.SessionStore into the Express MemoryStore for caching.
    this.loadCache = function (options) {
      // TODO - options could be used to only load parital dataset of recently active sessions.
      // It could also be used to help process/server syncing if we move to something like Redis.

      if (self.hybridCache) {
        var fetchOptions = {};

        fetchOptions.success = function (sessions) {
          var callback,
              sid,
              sess;

          callback = function () {
            // TODO - Don't need to do anything here, but MemoryStore.set() needs a callback.
          };

          _.each(sessions, function (model) {
            sid = model.id;
            sess = JSON.parse(model.session);

            // Store the session data in the Express MemeoryStore for caching.
            MemoryStore.set(sid, sess, callback);
          });
        };
        fetchOptions.error = function (sessions, err) {
          X.debug("Session Collection fetch failed.");
        };

        // TODO - This is REALLY SLOW if there are 10,000 sessions in the table and we use collection.fetch().
        // var sessionsCollection = new XM.SessionStoreCollection();
        //sessionsCollection.fetch(fetchOptions);

        // Fetch all records from XM.SessionStore and load them into the Express MemoryStore.
        fetchOptions.query = {
          requestType: "fetch",
          recordType: "XM.SessionStore"
        };

        // fetchOptions.username = GLOBAL_USERNAME; // TODO
        fetchOptions.username = 'node';
        XT.dataSource.fetch(fetchOptions);
      }
    };
  }

  /**
   * Inherit from `Store`.
   */
  XTPGStore.prototype = new Store();

  /**
   * Attempt to fetch session by the given `sid`.
   *
   * @param {String} sid
   * @param {Function} fn
   * @api public
   */
  XTPGStore.prototype.get = function (sid, done) {
    try {
      var fetchCache,
          fetchDB,
          that = this;

      sid = this.prefix + sid;

      fetchDB = function () {
        var fetchOptions = {},
            result,
            sessionStore = {};

        sessionStore = new XM.SessionStore();
        fetchOptions.id = sid;

        fetchOptions.success = function (model) {
          // We have a matching session store cookie

          // TODO - update lastModified time to extend timeout?
          // Doing this will complicate things with a hybridCache MemoryStore.
          // Do we really need to set that here for just in XM.Session and have CleanupTask
          // clean up XM.SessionStore on timeouts?
          //model.set("lastModified", new Date().getTime());
          //model.save(null, saveOptions);

          if (model.get("session")) {
            result = JSON.parse(model.get("session"));
          }

          if (that.hybridCache) {
            // We fetched from the database because this session was not in the MemoryStore.
            // Load this data into the MemoryStore to speed this up next time.
            MemoryStore.set(sid, result, function (err, cache) {
              if (err) {
                // Could not set. This shouldn't happen. Return error and move along...
                console.trace("MemoryStore.set error:");
                done && done(err);
              } else {
                // Return the session data back to Express to be used by routes and functors.
                return done(null, result);
              }
            });
          } else {
            // Return the session data back to Express to be used by routes and functors.
            return done(null, result);
          }
        };
        fetchOptions.error = function (model, err) {
          // Session was not found. This can happen if cookie is still in the browser, but
          // db record was removed by CleanupTask because it has timed out.

          // This is called when MemoryStore did not find session data.
          // No need to touch MemoryStore here.

          // Nothing found anywhere, no need to return the err, return nothing and move along...
          done && done();
        };

        // Try to fetch a session matching the user's cookie sid.
        sessionStore.fetch(fetchOptions);
      };

      if (this.hybridCache) {
        // Try to get session data from MemoryStore.
        fetchCache = function (err, cache) {
          if (cache && !err) {
            return done(null, cache);
          } else {
            fetchDB();
          }
        };
        MemoryStore.get(sid, fetchCache);
      } else {
        fetchDB();
      }
    } catch (err) {
      done && done(err);
    }
  };

  /**
   * Commit the given `sess` object associated with the given `sid`.
   *
   * @param {String} sid
   * @param {Session} sess
   * @param {Function} fn
   * @api public
   */
  XTPGStore.prototype.set = function (sid, sess, done) {
    try {
      var fetchCache,
          fetchDB,
          that = this;

      sid = this.prefix + sid;
      sess = JSON.stringify(sess);

      fetchDB = function () {
        var fetchOptions = {},
            saveOptions = {},
            sessionAttributes = {},
            sessionStore = {};

        sessionStore = new XM.SessionStore();

        saveOptions.success = function (model) {
          if (that.hybridCache) {
            // Overwrite the MemoryStore with the new data, replacing old or creating new entry.
            MemoryStore.set(sid, JSON.parse(sess), function (err, cache) {
              if (err) {
                // Could not set. Return error and move along.
                console.trace("MemoryStore.set error:");
                done && done(err);
              } else {
                // Success, MemoryStore updated, move along.
                done && done();
              }
            });
          } else {
            // Success, move along.
            done && done();
          }
        };
        saveOptions.error = function (model, err) {
          var that = this;
          // This shouldn't happen. How did we get here? Log trace.
          console.trace("XM.SessionStore save error. This shouldn't happen.");

          if (that.hybridCache) {
            // Delete any match in MemoryStore.
            MemoryStore.destroy(sid, function (err, cache) {
              if (err) {
                // Could not destroy. This shouldn't happen. Return error and move along.
                console.trace("MemoryStore.destroy error:");
                done && done(err);
              } else {
                // TODO - This might throw an error because our err object does not includes a stack.
                // https://github.com/senchalabs/connect/blob/master/lib/middleware/errorHandler.js#L48
                // MemoryStore destroyed, move along.
                done && done(that.err);
              }
            });
          } else {
            // TODO - This might throw an error because our err object does not includes a stack.
            // https://github.com/senchalabs/connect/blob/master/lib/middleware/errorHandler.js#L48
            // Return nothing and move along.
            done && done(err);
          }
        };

        fetchOptions.id = sid;

        fetchOptions.success = function (model) {
          // Fetch found this session, update it and save.
          model.set("session", sess);

          // Set gets called a lot. There isn't always a change to save and save will fail.
          // Check if this model has changed before trying to save it.
          if (model.getStatusString() === "READY_CLEAN") {
            if (that.hybridCache) {
              // MemoryStore did not have a matching session, update existing or add new.
              MemoryStore.set(sid, JSON.parse(sess), function (err, cache) {
                if (err) {
                  // Could not set. This shouldn't happen. Return error and move along.
                  console.trace("MemoryStore.set error:");
                  done && done(err);
                } else {
                  // Nothing to save, MemoryStore updated, move along.
                  done && done();
                }
              });
            } else {
              // Nothing to save, move along.
              done && done();
            }
          } else if (model.getStatusString() === "READY_DIRTY") {
            // Try to save XM.SessionStore to database.
            model.save(null, saveOptions);
          }
        };
        fetchOptions.error = function (model, err) {
          // Fetch did not find this session, initialize new and save.
          // Create new XM.SessionStore object.
          sessionStore = new XM.SessionStore();
          // TODO - Is this redundant?  Can I just call model.initialize...???
          sessionStore.initialize(null, {isNew: true});

          sessionAttributes = {
            id: sid,
            session: sess
          };

          // Try to save XM.SessionStore to database.
          sessionStore.save(sessionAttributes, saveOptions);
        };

        // Try to fetch a session matching the user's cookie sid to
        // see if we need to make a new one or update the old one.
        sessionStore.fetch(fetchOptions);
      };

      if (this.hybridCache) {
        // Try to get session data from MemoryStore.
        fetchCache = function (err, cache) {
          if (cache && !err) {
            if (JSON.stringify(cache, null, null) === sess) {
              // No change to the session data, nothing to save, move along.
              done && done();
            } else {
              fetchDB();
            }
          } else {
            fetchDB();
          }
        };
        MemoryStore.get(sid, fetchCache);
      } else {
        fetchDB();
      }
    } catch (err) {
      done && done(err);
    }
  };

  /**
   * Destroy the session associated with the given `sid`.
   *
   * @param {String} sid
   * @api public
   */
  XTPGStore.prototype.destroy = function (sid, done) {
    try {
      var sessionStore = {},
          fetchOptions = {},
          that = this;

      sid = this.prefix + sid;
      sessionStore = new XM.SessionStore();
      fetchOptions.id = sid;

      fetchOptions.success = function (model) {
        // Delete this session from the db store.
        model.destroy();

        if (that.hybridCache) {
          // Delete this session from the MemoryStore as well.
          MemoryStore.destroy(sid, function (err, cache) {
            if (err) {
              // Could not destroy. This shouldn't happen. Return error and move along.
              console.trace("MemoryStore.destroy error:");
              done && done(err);
            } else {
              // MemoryStore destroyed, move along.
              done && done();
            }
          });
        } else {
          done && done();
        }
      };
      fetchOptions.error = function (model, err) {
        if (that.hybridCache) {
          // Did not find a match in the db, delete this session from the MemoryStore, it's invalid.
          MemoryStore.destroy(sid, function (err, cache) {
            if (err) {
              // Could not destroy. This shouldn't happen. Return error and move along.
              console.trace("MemoryStore.destroy error:");
              done && done(err);
            } else {
              // MemoryStore destroyed. Return nothing and move along.
              done && done();
            }
          });
        } else {
          // Return nothing and move along.
          done && done();
        }
      };

      // Try to fetch a session matching the user's cookie sid.
      sessionStore.fetch(fetchOptions);
    } catch (err) {
      done && done(err);
    }
  };

  return XTPGStore;
};<|endoftext|>


<commit_before>const cheerio = require("cheerio");
const moment = require("moment/min/moment-with-locales");
const axios = require("axios");

const PAPER_URL = "https://www.clarin.com";

const MINUTE = 60; // Seconds
const CACHE_DURATION = 10 * MINUTE;
const CACHE_DURATION_ARTICLE = 30 * MINUTE;
const CACHE_DURATION_ERROR = 60 * MINUTE;

moment.locale("es");

let template = `
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{title}}</title>
    <link href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700|Lora:700" rel="stylesheet">
    <link rel="stylesheet" href="/css/main.css">
    <link rel="icon" href="/favicon.png" type="image/png">
    <link rel="apple-touch-icon" href="/favicon.png">
    <link rel="shortcut" href="/favicon.ico" type="image/x-icon">

Clean Paper

{{date}} · {{time}} hs

      {{body}}

`;

const fetchContent = (url, res) =>
  axios(url)
    .then(({ data, headers }) => {
      // Solo parsear html
      if (headers["content-type"].indexOf("text/html") !== 0) {
        throw Error();
      }

      return data;
    })
    .catch((e) => {
      res.setHeader(
        "Cache-Control",
        `max-age=0, s-maxage=${CACHE_DURATION_ERROR}`
      );

      console.log({
        type: "error",
        module: "fetching",
        location: url,
        error: e.message,
      });
      return Promise.reject();
    });

const parseHome = (res, withImages) => {
  res.setHeader("Cache-Control", `max-age=0, s-maxage=${CACHE_DURATION}`);

  return fetchContent(PAPER_URL, res)
    .then((body) => {
      if (!res.headersSent) {
        return Promise.resolve(body);
      }
      return Promise.reject();
    })
    .then((body) => {
      const $ = cheerio.load(body);

      // Limpiar el contenido, solo dejar los contenidos validos de content home
      const homeContent = $(".content.home .container.father");
      $("body").empty().append(homeContent);

      // Borrar ads
      $("[data-name=advert]").remove();

      // Borrar artículos de sólo fotos
      $(".fotonoticia").remove();

      if (!withImages) {
        $(".wrap-figure, img").remove();
      } else {
        $(".img-responsive").each((idx, node) => {
          const elem = $(node);
          elem.attr("src", elem.data("srcset"));
        });
        $("img")
          .removeAttr("alt")
          .removeAttr("class")
          .removeAttr("width")
          .removeAttr("height")
          .removeAttr("srcset")
          .attr("height", "140");
        $("source").remove();
      }

      // Borrar Ultimo Momento
      $(".last-moment").remove();

      // Borrar links de contenido relacionado
      $(".content-related").remove();

      // Limpio el markup de los artículos
      $("article").each((i, elem) => {
        let volanta = $(elem).find("p.volanta").first().remove();
        const title = $(elem).find("h1,h2,h3").remove();
        const url = $(elem).find("a").first().attr("href");
        const link = $("").append(title).attr("href", url);

        const image = $(elem)
          .find(withImages ? "picture" : "")
          .remove();

        const bajada = $(elem).find("p.summary").remove();

        // Si no hay volanta, usamos el autor como volanta
        if (!volanta.length) {
          volanta = $(elem).find(".author-name").first().remove();
        }

        // El orden siempre va a ser titulo - imagen - bajada
        $(elem).empty().append(volanta, link, image, bajada);
        $(elem).removeAttr("style");
        $(elem).removeAttr("class");

        // Los articulos de BrandStudio son contenido patrocinado
        if (url.indexOf("brandstudio") !== -1) {
          $(elem).addClass("remove");
        }

        // Sacamos links a Elle
        if (url.indexOf("elle.") !== -1) {
          $(elem).addClass("remove");
        }
      });
      $(".remove").remove();

      const content = $("
");
      content.append($("article"));

      const timeClarin = moment().utcOffset(-180);
      let date = `${timeClarin.format("dddd DD MMMM")} de `;
      date += `${timeClarin.format("YYYY")}`;
      date = `${date[0].toUpperCase()}${date.substr(1)}`;
      const time = timeClarin.format("HH:mm");

      const htmlContent = template
        .replace("{{body}}", content.html())
        .replace("{{title}}", "Clarin Limpio")
        .replace("{{date}}", date)
        .replace("{{time}}", time);

      res.send(htmlContent);
    })
    .catch((e) => {
      console.log({
        type: "error",
        module: "parsing",
        location: "/",
        error: e.message,
      });
      return Promise.reject();
    });
};

const parseArticle = (res, url, withImages) => {
  res.setHeader(
    "Cache-Control",
    `max-age=0, s-maxage=${CACHE_DURATION_ARTICLE}`
  );

  return fetchContent(`${PAPER_URL}/${url}`, res)
    .then((body) => {
      if (!res.headersSent) {
        return Promise.resolve(body);
      }
      return Promise.reject();
    })
    .then((body) => {
      const $ = cheerio.load(body);

      $("header").remove();
      $("footer").remove();
      $(".latest-news").remove();
      $(".most-viewed").remove();

      if (!withImages) {
        // Las imágenes suelen venir adentro de un link en los articulos
        $("article > a").has("img").remove();
        $("img").remove();
        $("#galeria-trigger, .image-trigger").remove();
        // Eliminar todos los embeds que no sean tweets
        $("[class^=embed-]").each((i, elem) => {
          if (!$(elem).find(".twitter-tweet").length) {
            $(elem).remove();
          }
        });
      } else {
        $(".img-responsive").each((idx, node) => {
          const elem = $(node);
          elem.attr("src", elem.data("small"));
        });

        $("article").each((i, elem) => {
          if ($(elem).find("img").length > 0) {
            $(elem).prepend($(elem).find(".txt").first().remove());
          }
        });
      }

      const content = $("
");

      $("[id^=bannerId_]").remove();
      $("[id^=sas_]").remove();
      $('[itemprop="video"]').remove();

      // Parsear mvHero
      const mvHero = $("#mnhead");
      if (mvHero) {
        try {
          const script = mvHero.find("script").html();
          const title = script
            .replace(/\n/g, "")
            .match(/title: +["']([^"']+)["'],/)[1];
          const divTitle = $("

")
            .addClass("title")
            .append($(`<h1>${title}</h1>`))
            .append($(".bajada"));
          $(".nota-unica").prepend(divTitle);
        } catch (e) {
          // Ignore error
        }
      }

      // Outlinks
      const outlinks = $('

');
      outlinks.append($(".box-content-new, .content-new").remove());
      outlinks.find("span").remove();

      // Heurísticas:
      // Clickbait en la bajada
      $(".bajada a").each((i, elem) => {
        const elemContainer = $(elem).parents("h2");
        outlinks.append($(elem).remove());
        elemContainer.remove();
      });

      // Outlinks estan en medio del texto con la pinta de "Mirá también: <noticia>"
      $("a").each((i, elem) => {
        if ($(elem).text().toLowerCase().indexOf("mir") === 0) {
          outlinks.append($(elem).remove());
        }
      });

      // Párrafos enteros que sólo son clickbait+link
      const clickbaits = ["leé", "lee", "te puede interesar", "te puede"];
      $("p").each((i, elem) => {
        const p = $(elem);
        if (p.find("a").length === 1) {
          const link = p.find("a").text();
          const clickbait = p.text().replace(link, "").trim().toLowerCase();
          if (clickbaits.indexOf(clickbait) > -1) {
            outlinks.append(p.find("a").remove());
            p.remove();
          }
        }
      });

      $("p a").each((i, elem) => {
        const url = $(elem).attr("href");
        if (url.indexOf("/tema/") !== -1) {
          // Esto es más clickbait basicamente
          $(elem).replaceWith($(elem).text());
        } else {
          // Si no es clickbait, hacer los links relativos
          $(elem).attr("href", url.replace(/http(s)?:\/\/.+\//i, "/"));
        }
      });

      // Reemplazar los mark por strong
      $("mark").each((i, elem) => {
        $(elem).replaceWith(`<strong>${$(elem).text()}</strong>`);
      });

      content.append($(".nota-unica .entry-title, .nota-unica .notas-content"));
      const miraTambien = $("

");
      const addedLinks = [];
      outlinks.find("a").each((i, elem) => {
        const $elem = $(elem);
        const url = $elem.attr("href").replace(/http(s)?:\/\/.+\//i, "/");
        if (
          addedLinks.indexOf(url) === -1 &&
          url.indexOf("/tema/") === -1 &&
          $elem.text().trim().length > 1
        ) {
          const link = $("")
            .text($elem.text().replace(/Mir[áa] (tambi[ée]n: )?/i, ""))
            .attr("href", url);
          miraTambien.append($("

").append(link));
          addedLinks.push(url);
        }
      });

      if (miraTambien.find("li").length) {
        content.append($("
Mirá también
"));
        content.append(miraTambien);
      }

      const publishedDate = content
        .find(".breadcrumb .publishedDate")
        .text()
        .trim();
      const modifiedDate = content
        .find(".breadcrumb .modificatedDate")
        .text()
        .trim();

      content
        .find(".bajada")
        .append($(`<p>${publishedDate} - ${modifiedDate}</p>`));

      // Eliminar cosas de más
      content
        .find(".pull-right, .entry-head, .body-nota > div, #comments, script")
        .remove();

      title = $("title").text();

      const timeClarin = moment().utcOffset(-180);

      let date = `${timeClarin.format("dddd DD MMMM")} de `;
      date += `${timeClarin.format("YYYY")}`;
      date = `${date[0].toUpperCase()}${date.substr(1)}`;
      const time = timeClarin.format("HH:mm");

      const htmlContent = template
        .replace("{{body}}", content.html())
        .replace("{{title}}", title)
        .replace("{{date}}", date)
        .replace("{{time}}", time);

      res.send(htmlContent);
    })
    .catch((e) => {
      console.log({
        type: "error",
        module: "parsing",
        location: url,
        error: e ? e.message : "",
      });
      return Promise.reject();
    });
};

module.exports = (req, res) => {
  let timeout = -1;
  new Promise((resolve, reject) => {
    timeout = setTimeout(() => {
      console.log({
        type: "error",
        module: "main",
        location: url,
        error: "Timeout.",
      });
      reject();
    }, 9500);

    let url = req.query.url || "/";

    res.setHeader("Cache-Control", `max-age=0, s-maxage=${CACHE_DURATION}`);
    res.status(200);

    if (url.indexOf(".js") >= 0 || url.indexOf(".css") >= 0) {
      // No pedir los JS ni CSS externos
      res.setHeader(
        "Cache-Control",
        `max-age=0, s-maxage=${CACHE_DURATION_ERROR}`
      );
      res.send("");
      resolve();
    } else {
      let images = false;

      if (url.indexOf("/img") === 0) {
        // Con imágenes
        url = url.replace(/img\/?/, "");
        images = true;
      }

      if (url.length > 1) {
        // Es un artículo
        parseArticle(res, url, images).then(resolve).catch(reject);
      } else {
        // Es la home
        parseHome(res, images).then(resolve).catch(reject);
      }
    }
  })
    .then(() => {
      clearTimeout(timeout);
    })
    .catch(() => {
      clearTimeout(timeout);
      if (!res.headersSent) {
        res.removeHeader("Cache-Control");
        const timeClarin = moment().utcOffset(-180);

        let date = `${timeClarin.format("dddd DD MMMM")} de `;
        date += `${timeClarin.format("YYYY")}`;
        date = `${date[0].toUpperCase()}${date.substr(1)}`;
        const time = timeClarin.format("HH:mm");

        const htmlContent = template
          .replace("{{body}}", "
Ha ocurrido un error.
")
          .replace("{{title}}", "Error")
          .replace("{{date}}", date)
          .replace("{{time}}", time);

        res.send(htmlContent);
      }
    });
};
<commit_msg>Arreglar parse de mvHero<commit_after>const cheerio = require("cheerio");
const moment = require("moment/min/moment-with-locales");
const axios = require("axios");

const PAPER_URL = "https://www.clarin.com";

const MINUTE = 60; // Seconds
const CACHE_DURATION = 10 * MINUTE;
const CACHE_DURATION_ARTICLE = 30 * MINUTE;
const CACHE_DURATION_ERROR = 60 * MINUTE;

moment.locale("es");

let template = `
<!DOCTYPE html>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{title}}</title>
    <link href="https://fonts.googleapis.com/css?family=Libre+Baskerville:400,400i,700|Lora:700" rel="stylesheet">
    <link rel="stylesheet" href="/css/main.css">
    <link rel="icon" href="/favicon.png" type="image/png">
    <link rel="apple-touch-icon" href="/favicon.png">
    <link rel="shortcut" href="/favicon.ico" type="image/x-icon">

Clean Paper

{{date}} · {{time}} hs

      {{body}}

`;

const fetchContent = (url, res) =>
  axios(url)
    .then(({ data, headers }) => {
      // Solo parsear html
      if (headers["content-type"].indexOf("text/html") !== 0) {
        throw Error();
      }

      return data;
    })
    .catch((e) => {
      res.setHeader(
        "Cache-Control",
        `max-age=0, s-maxage=${CACHE_DURATION_ERROR}`
      );

      console.log({
        type: "error",
        module: "fetching",
        location: url,
        error: e.message,
      });
      return Promise.reject();
    });

const parseHome = (res, withImages) => {
  res.setHeader("Cache-Control", `max-age=0, s-maxage=${CACHE_DURATION}`);

  return fetchContent(PAPER_URL, res)
    .then((body) => {
      if (!res.headersSent) {
        return Promise.resolve(body);
      }
      return Promise.reject();
    })
    .then((body) => {
      const $ = cheerio.load(body);

      // Limpiar el contenido, solo dejar los contenidos validos de content home
      const homeContent = $(".content.home .container.father");
      $("body").empty().append(homeContent);

      // Borrar ads
      $("[data-name=advert]").remove();

      // Borrar artículos de sólo fotos
      $(".fotonoticia").remove();

      if (!withImages) {
        $(".wrap-figure, img").remove();
      } else {
        $(".img-responsive").each((idx, node) => {
          const elem = $(node);
          elem.attr("src", elem.data("srcset"));
        });
        $("img")
          .removeAttr("alt")
          .removeAttr("class")
          .removeAttr("width")
          .removeAttr("height")
          .removeAttr("srcset")
          .attr("height", "140");
        $("source").remove();
      }

      // Borrar Ultimo Momento
      $(".last-moment").remove();

      // Borrar links de contenido relacionado
      $(".content-related").remove();

      // Limpio el markup de los artículos
      $("article").each((i, elem) => {
        let volanta = $(elem).find("p.volanta").first().remove();
        const title = $(elem).find("h1,h2,h3").remove();
        const url = $(elem).find("a").first().attr("href");
        const link = $("").append(title).attr("href", url);

        const image = $(elem)
          .find(withImages ? "picture" : "")
          .remove();

        const bajada = $(elem).find("p.summary").remove();

        // Si no hay volanta, usamos el autor como volanta
        if (!volanta.length) {
          volanta = $(elem).find(".author-name").first().remove();
        }

        // El orden siempre va a ser titulo - imagen - bajada
        $(elem).empty().append(volanta, link, image, bajada);
        $(elem).removeAttr("style");
        $(elem).removeAttr("class");

        // Los articulos de BrandStudio son contenido patrocinado
        if (url.indexOf("brandstudio") !== -1) {
          $(elem).addClass("remove");
        }

        // Sacamos links a Elle
        if (url.indexOf("elle.") !== -1) {
          $(elem).addClass("remove");
        }
      });
      $(".remove").remove();

      const content = $("
");
      content.append($("article"));

      const timeClarin = moment().utcOffset(-180);
      let date = `${timeClarin.format("dddd DD MMMM")} de `;
      date += `${timeClarin.format("YYYY")}`;
      date = `${date[0].toUpperCase()}${date.substr(1)}`;
      const time = timeClarin.format("HH:mm");

      const htmlContent = template
        .replace("{{body}}", content.html())
        .replace("{{title}}", "Clarin Limpio")
        .replace("{{date}}", date)
        .replace("{{time}}", time);

      res.send(htmlContent);
    })
    .catch((e) => {
      console.log({
        type: "error",
        module: "parsing",
        location: "/",
        error: e.message,
      });
      return Promise.reject();
    });
};

const parseArticle = (res, url, withImages) => {
  res.setHeader(
    "Cache-Control",
    `max-age=0, s-maxage=${CACHE_DURATION_ARTICLE}`
  );

  return fetchContent(`${PAPER_URL}/${url}`, res)
    .then((body) => {
      if (!res.headersSent) {
        return Promise.resolve(body);
      }
      return Promise.reject();
    })
    .then((body) => {
      const $ = cheerio.load(body);

      $("header").remove();
      $("footer").remove();
      $(".latest-news").remove();
      $(".most-viewed").remove();

      if (!withImages) {
        // Las imágenes suelen venir adentro de un link en los articulos
        $("article > a").has("img").remove();
        $("img").remove();
        $("#galeria-trigger, .image-trigger").remove();
        // Eliminar todos los embeds que no sean tweets
        $("[class^=embed-]").each((i, elem) => {
          if (!$(elem).find(".twitter-tweet").length) {
            $(elem).remove();
          }
        });
      } else {
        $(".img-responsive").each((idx, node) => {
          const elem = $(node);
          elem.attr("src", elem.data("small"));
        });

        $("article").each((i, elem) => {
          if ($(elem).find("img").length > 0) {
            $(elem).prepend($(elem).find(".txt").first().remove());
          }
        });
      }

      const content = $("
");

      $("[id^=bannerId_]").remove();
      $("[id^=sas_]").remove();
      $('[itemprop="video"]').remove();

      // Parsear mvHero
      const mvHero = $("#mnhead");
      if (mvHero) {
        try {
          const script = mvHero.find("script").html();
          const title = script
            .replace(/\n/g, "")
            .match(/title: +["']([^"']+)["'],/)[1];
          const divTitle = $("

")
            .addClass("entry-title")
            .append($(`<h1>${title}</h1>`))
            .append($(".bajada"));
          $(".nota-unica").prepend(divTitle);
        } catch (e) {
          // Ignore error
        }
      }

      // Outlinks
      const outlinks = $('

');
      outlinks.append($(".box-content-new, .content-new").remove());
      outlinks.find("span").remove();

      // Heurísticas:
      // Clickbait en la bajada
      $(".bajada a").each((i, elem) => {
        const elemContainer = $(elem).parents("h2");
        outlinks.append($(elem).remove());
        elemContainer.remove();
      });

      // Outlinks estan en medio del texto con la pinta de "Mirá también: <noticia>"
      $("a").each((i, elem) => {
        if ($(elem).text().toLowerCase().indexOf("mir") === 0) {
          outlinks.append($(elem).remove());
        }
      });

      // Párrafos enteros que sólo son clickbait+link
      const clickbaits = ["leé", "lee", "te puede interesar", "te puede"];
      $("p").each((i, elem) => {
        const p = $(elem);
        if (p.find("a").length === 1) {
          const link = p.find("a").text();
          const clickbait = p.text().replace(link, "").trim().toLowerCase();
          if (clickbaits.indexOf(clickbait) > -1) {
            outlinks.append(p.find("a").remove());
            p.remove();
          }
        }
      });

      $("p a").each((i, elem) => {
        const url = $(elem).attr("href");
        if (url.indexOf("/tema/") !== -1) {
          // Esto es más clickbait basicamente
          $(elem).replaceWith($(elem).text());
        } else {
          // Si no es clickbait, hacer los links relativos
          $(elem).attr("href", url.replace(/http(s)?:\/\/.+\//i, "/"));
        }
      });

      // Reemplazar los mark por strong
      $("mark").each((i, elem) => {
        $(elem).replaceWith(`<strong>${$(elem).text()}</strong>`);
      });

      content.append($(".nota-unica .entry-title, .nota-unica .notas-content"));
      const miraTambien = $("

");
      const addedLinks = [];
      outlinks.find("a").each((i, elem) => {
        const $elem = $(elem);
        const url = $elem.attr("href").replace(/http(s)?:\/\/.+\//i, "/");
        if (
          addedLinks.indexOf(url) === -1 &&
          url.indexOf("/tema/") === -1 &&
          $elem.text().trim().length > 1
        ) {
          const link = $("")
            .text($elem.text().replace(/Mir[áa] (tambi[ée]n: )?/i, ""))
            .attr("href", url);
          miraTambien.append($("

").append(link));
          addedLinks.push(url);
        }
      });

      if (miraTambien.find("li").length) {
        content.append($("
Mirá también
"));
        content.append(miraTambien);
      }

      const publishedDate = content
        .find(".breadcrumb .publishedDate")
        .text()
        .trim();
      const modifiedDate = content
        .find(".breadcrumb .modificatedDate")
        .text()
        .trim();

      content
        .find(".bajada")
        .append($(`<p>${publishedDate} - ${modifiedDate}</p>`));

      // Eliminar cosas de más
      content
        .find(".pull-right, .entry-head, .body-nota > div, #comments, script")
        .remove();

      title = $("title").text();

      const timeClarin = moment().utcOffset(-180);

      let date = `${timeClarin.format("dddd DD MMMM")} de `;
      date += `${timeClarin.format("YYYY")}`;
      date = `${date[0].toUpperCase()}${date.substr(1)}`;
      const time = timeClarin.format("HH:mm");

      const htmlContent = template
        .replace("{{body}}", content.html())
        .replace("{{title}}", title)
        .replace("{{date}}", date)
        .replace("{{time}}", time);

      res.send(htmlContent);
    })
    .catch((e) => {
      console.log({
        type: "error",
        module: "parsing",
        location: url,
        error: e ? e.message : "",
      });
      return Promise.reject();
    });
};

module.exports = (req, res) => {
  let timeout = -1;
  new Promise((resolve, reject) => {
    timeout = setTimeout(() => {
      console.log({
        type: "error",
        module: "main",
        location: url,
        error: "Timeout.",
      });
      reject();
    }, 9500);

    let url = req.query.url || "/";

    res.setHeader("Cache-Control", `max-age=0, s-maxage=${CACHE_DURATION}`);
    res.status(200);

    if (url.indexOf(".js") >= 0 || url.indexOf(".css") >= 0) {
      // No pedir los JS ni CSS externos
      res.setHeader(
        "Cache-Control",
        `max-age=0, s-maxage=${CACHE_DURATION_ERROR}`
      );
      res.send("");
      resolve();
    } else {
      let images = false;

      if (url.indexOf("/img") === 0) {
        // Con imágenes
        url = url.replace(/img\/?/, "");
        images = true;
      }

      if (url.length > 1) {
        // Es un artículo
        parseArticle(res, url, images).then(resolve).catch(reject);
      } else {
        // Es la home
        parseHome(res, images).then(resolve).catch(reject);
      }
    }
  })
    .then(() => {
      clearTimeout(timeout);
    })
    .catch(() => {
      clearTimeout(timeout);
      if (!res.headersSent) {
        res.removeHeader("Cache-Control");
        const timeClarin = moment().utcOffset(-180);

        let date = `${timeClarin.format("dddd DD MMMM")} de `;
        date += `${timeClarin.format("YYYY")}`;
        date = `${date[0].toUpperCase()}${date.substr(1)}`;
        const time = timeClarin.format("HH:mm");

        const htmlContent = template
          .replace("{{body}}", "
Ha ocurrido un error.
")
          .replace("{{title}}", "Error")
          .replace("{{date}}", date)
          .replace("{{time}}", time);

        res.send(htmlContent);
      }
    });
};
<|endoftext|>


<commit_before>'use strict'

const strToStream = require('string-to-stream')
const spawn = require('child_process').spawn
const through = require('through2')
const fse = require('fs-extra')
const temp = require('temp')
const path = require('path')
const fs = require('fs')

/**
 * Generates a PDF stream from a LaTeX document.
 *
 * @param {String} src - The LaTeX document.
 * @param {Object} options - Optional compilation specifications.
 *
 * @return {DestroyableTransform}
 */
function latex(src, options) {
  const outputStream = through()

  /**
   * Emits the given error to the returned output stream.
   */
  const handleErrors = (err) => {
    outputStream.emit('error', err)
    outputStream.destroy()
  }

  /**
   * Emits errors from logs to output stream, and also gives full log to user if requested.
   */
  const printErrors = (tempPath, userLogPath) => {
    const errorLogPath = path.join(tempPath, 'texput.log')
    const errorLogStream = fs.createReadStream(errorLogPath)

    if (userLogPath) {
      const userLogStream = fs.createWriteStream(userLogPath)
      errorLogStream.pipe(userLogStream)
    }

    const errors = []

    errorLogStream.on('data', (data) => {
      const lines = data.toString().split('\n')

      lines.forEach((line, i) => {
        if (line.startsWith('! Undefined control sequence.')) {
          errors.push(lines[i - 1])
          errors.push(lines[i])
          errors.push(lines[i + 1])
        } else if (line.startsWith('!')) {
          errors.push(line)
        }
      })
    })

    errorLogStream.on('end', () => {
      const errMessage = `LaTeX Syntax Error\n${errors.join('\n')}`
      const error = new Error(errMessage)

      outputStream.emit('error', error)
    })
  }

  temp.mkdir('node-latex', (err, tempPath) => {
    if (err) {
      handleErrors(err)
    }

    let inputStream

    if (!src) {
      handleErrors(new Error('Error: No TeX document provided.'))
    }

    if (typeof src === 'string') {
      inputStream = strToStream(src)
    } else if (src.pipe) {
      inputStream = src
    } else {
      handleErrors(new Error('Error: Invalid TeX document.'))
    }

    options = options || {}

    // The path(s) to your TEXINPUTS.
    const inputs = options.inputs || tempPath

    // The path(s) to your font inputs for fontspec.
    const fonts = options.fonts || tempPath

    // The binary command to run (`pdflatex`, `xetex`, etc).
    const cmd = options.cmd || 'pdflatex'

    // The number of times to run LaTeX.
    const passes = options.passes || 1

    // The path to where the user wants to save the error log file to.
    const userLogPath = options.errorLogs

    // The current amount of times LaTeX has run so far.
    let completedPasses = 0

    if (passes > 1 && typeof src !== 'string') {
      const msg = 'Error: You can\'t process a stream twice. Pass a string to use multiple passes.'
      handleErrors(new Error(msg))

      return
    }

    /**
     * Combines all paths into a single PATH to be added to process.env.
     */
    const joinPaths = inputs =>
      Array.isArray(inputs)
        ? `${inputs.join(':')}:`
        : `${inputs}:`

    const args = options.args || [
      '-halt-on-error'
    ]

    const opts = {
      cwd: tempPath,
      env: Object.assign({}, process.env, {
        TEXINPUTS: joinPaths(inputs),
        TTFONTS: joinPaths(fonts),
        OPENTYPEFONTS: joinPaths(fonts)
      })
    }

    /**
     * Runs a LaTeX child process on the document stream
     * and then decides whether it needs to do it again.
     */
    const runLatex = (inputStream) => {
      const tex = spawn(cmd, args, opts)

      inputStream.pipe(tex.stdin)

      // Prevent Node from crashing on compilation error.
      tex.stdin.on('error', handleErrors)

      tex.on('error', () => {
        handleErrors(new Error(`Error: Unable to run ${cmd} command.`))
      })

      tex.stdout.on('data', (data) => {});

      tex.stderr.on('data', (data) => {});

      tex.on('close', (code) => {});

      tex.on('exit', (code) => {
        if (code !== 0) {
          printErrors(tempPath, userLogPath)
          return
        }

        completedPasses++

        // Schedule another run if necessary.
        completedPasses >= passes
          ? returnDocument()
          : runLatex(strToStream(src))
      })
    }

    /**
     * Returns the PDF stream after the final run.
     */
    const returnDocument = () => {
      const pdfPath = path.join(tempPath, 'texput.pdf')
      const pdfStream = fs.createReadStream(pdfPath)

      pdfStream.pipe(outputStream)
      pdfStream.on('close', () => fse.removeSync(tempPath))
      pdfStream.on('error', handleErrors)
    }

    // Start the first run.
    runLatex(inputStream)
  })

  return outputStream
}

module.exports = latex
<commit_msg>Remove temp directory when LaTeX errors<commit_after>'use strict'

const strToStream = require('string-to-stream')
const spawn = require('child_process').spawn
const through = require('through2')
const fse = require('fs-extra')
const temp = require('temp').track()
const path = require('path')
const fs = require('fs')

/**
 * Generates a PDF stream from a LaTeX document.
 *
 * @param {String} src - The LaTeX document.
 * @param {Object} options - Optional compilation specifications.
 *
 * @return {DestroyableTransform}
 */
function latex(src, options) {
  const outputStream = through()

  /**
   * Emits the given error to the returned output stream.
   */
  const handleErrors = (err) => {
    outputStream.emit('error', err)
    outputStream.destroy()
  }

  /**
   * Emits errors from logs to output stream, and also gives full log to user if requested.
   */
  const printErrors = (tempPath, userLogPath) => {
    const errorLogPath = path.join(tempPath, 'texput.log')
    const errorLogStream = fs.createReadStream(errorLogPath)

    if (userLogPath) {
      const userLogStream = fs.createWriteStream(userLogPath)
      errorLogStream.pipe(userLogStream)
    }

    const errors = []

    errorLogStream.on('data', (data) => {
      const lines = data.toString().split('\n')

      lines.forEach((line, i) => {
        if (line.startsWith('! Undefined control sequence.')) {
          errors.push(lines[i - 1])
          errors.push(lines[i])
          errors.push(lines[i + 1])
        } else if (line.startsWith('!')) {
          errors.push(line)
        }
      })
    })

    errorLogStream.on('end', () => {
      const errMessage = `LaTeX Syntax Error\n${errors.join('\n')}`
      const error = new Error(errMessage)

      outputStream.emit('error', error)
    })
  }

  temp.mkdir('node-latex', (err, tempPath) => {
    if (err) {
      handleErrors(err)
    }

    let inputStream

    if (!src) {
      handleErrors(new Error('Error: No TeX document provided.'))
    }

    if (typeof src === 'string') {
      inputStream = strToStream(src)
    } else if (src.pipe) {
      inputStream = src
    } else {
      handleErrors(new Error('Error: Invalid TeX document.'))
    }

    options = options || {}

    // The path(s) to your TEXINPUTS.
    const inputs = options.inputs || tempPath

    // The path(s) to your font inputs for fontspec.
    const fonts = options.fonts || tempPath

    // The binary command to run (`pdflatex`, `xetex`, etc).
    const cmd = options.cmd || 'pdflatex'

    // The number of times to run LaTeX.
    const passes = options.passes || 1

    // The path to where the user wants to save the error log file to.
    const userLogPath = options.errorLogs

    // The current amount of times LaTeX has run so far.
    let completedPasses = 0

    if (passes > 1 && typeof src !== 'string') {
      const msg = 'Error: You can\'t process a stream twice. Pass a string to use multiple passes.'
      handleErrors(new Error(msg))

      return
    }

    /**
     * Combines all paths into a single PATH to be added to process.env.
     */
    const joinPaths = inputs =>
      Array.isArray(inputs)
        ? `${inputs.join(':')}:`
        : `${inputs}:`

    const args = options.args || [
      '-halt-on-error'
    ]

    const opts = {
      cwd: tempPath,
      env: Object.assign({}, process.env, {
        TEXINPUTS: joinPaths(inputs),
        TTFONTS: joinPaths(fonts),
        OPENTYPEFONTS: joinPaths(fonts)
      })
    }

    /**
     * Runs a LaTeX child process on the document stream
     * and then decides whether it needs to do it again.
     */
    const runLatex = (inputStream) => {
      const tex = spawn(cmd, args, opts)

      inputStream.pipe(tex.stdin)

      // Prevent Node from crashing on compilation error.
      tex.stdin.on('error', handleErrors)

      tex.on('error', () => {
        handleErrors(new Error(`Error: Unable to run ${cmd} command.`))
      })

      tex.stdout.on('data', (data) => {});

      tex.stderr.on('data', (data) => {});

      tex.on('close', (code) => {});

      tex.on('exit', (code) => {
        if (code !== 0) {
          printErrors(tempPath, userLogPath)
          return
        }

        completedPasses++

        // Schedule another run if necessary.
        completedPasses >= passes
          ? returnDocument()
          : runLatex(strToStream(src))
      })
    }

    /**
     * Returns the PDF stream after the final run.
     */
    const returnDocument = () => {
      const pdfPath = path.join(tempPath, 'texput.pdf')
      const pdfStream = fs.createReadStream(pdfPath)

      pdfStream.pipe(outputStream)
      pdfStream.on('close', () => fse.removeSync(tempPath))
      pdfStream.on('error', handleErrors)
    }

    // Start the first run.
    runLatex(inputStream)
  })

  return outputStream
}

module.exports = latex
<|endoftext|>


<commit_before>var commands = []
var state = {}
var checker = require('../../Utils/access_checker')
var logger = require('../../Utils/error_loggers')
var genlog = require('../../Utils/generic_logger')
var config = require('../../config.js')
var bugsnag = require('bugsnag')

bugsnag.register(config.discord.bugsnag)

try {
  state = JSON.parse(require('fs').readFileSync('./dump.json', 'utf8'))
  console.log('Queue restored!')
} catch (e) {
  console.log('Failed to restore queue! ' + e.message)
}

commands.newCardInit = {
  internal: true,
  fn: function (msg) {
    state[msg.id] = {
      type: 'newCard',
      reports: 0,
      embed: msg.embeds[0],
      UvId: msg.embeds[0].footer.text.split('ID: ')[1]
    }
    msg.addReaction({
      id: '302137374920671233',
      name: 'report'
    })
    msg.addReaction({
      id: '302138464986595339',
      name: 'upvote'
    })
  }
}

commands.registerVote = {
  internal: true,
  fn: function (msg, reaction, bot, uv, user) {
    if (msg === null) {
      console.warn('Warning! Vote registering failed due to the message not being cached!')
      return
    }
    if (state[msg.id] === undefined) {
      console.warn('Warning! Vote registering failed due to the report not being known to memory!')
      return
    }
    switch (state[msg.id].type) {
      case 'newCard': {
        if (reaction.id === '302137374920671233') {
          checker.getLevel(user.memberOf('268811439588900865'), function (l) {
            if (l > 0) {
              state[msg.id].reports++
              genlog.log(bot, user, {
                message: 'Reported a card as inappropriate in the feed',
                affected: state[msg.id].UvId,
                result: (state[msg.id].reports === config.discord.reportThreshold) ? 'Report has been sent to admins': undefined
              })
              if (state[msg.id].reports === config.discord.reportThreshold) {
                bot.Channels.get(config.discord.feedChannel).sendMessage(`Feedback with ID ${state[msg.id].UvId} (${msg.embeds[0].title}) has been sent off for admin review.`)
                state[msg.id].type = 'adminReviewDelete'
                switchIDs(state[msg.id], bot)
                delete state[msg.id]
              }
            }
          })
        } else if (reaction.id === '302138464986595339') {
          getMail(uv, user.id).then(f => {
            uv.loginAs(f).then(c => {
              c.post(`forums/${config.uservoice.forumId}/suggestions/${state[msg.id].UvId}/votes.json`, {
                to: 1
              }).then((s) => {
                if (user !== null) {
                  genlog.log(bot, user, {
                    message: 'Feed-voted',
                    affected: state[msg.id].UvId
                  })
                  bot.Channels.get(config.discord.feedChannel).sendMessage(`${user.mention}, your vote has been registered.`).then(c => {
                    setTimeout(() => c.delete(), 5000)
                  })
                }
              }).catch(e => {
                if (e.statusCode === 404) {
                  logger.log(bot, {
                    cause: 'feed_vote',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                } else {
                  logger.log(bot, {
                    cause: 'feed_vote_apply',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                }
              })
            }).catch(e => {
              logger.log(bot, {
                cause: 'login_as',
                message: (e.message !== undefined) ? e.message : JSON.stringify(e)
              }).catch(e => {
                 if (e === 'Not found') {
                  bot.Channels.get(config.discord.feedChannel).sendMessage(`${user.mention}, your details are not found.`).then(c => {
                    setTimeout(() => c.delete(), 5000)
                  })
                } else {
                  logger.log(bot, {
                    cause: 'email_search',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                }
              })
            })
          })
        }
        break
      }
      case 'adminReviewDelete': {
        if (reaction.id === '302137375113609219') {
          genlog.log(bot, user, {
            message: 'Dismissed a report',
            affected: state[msg.id].UvId,
          })
          bot.Channels.find(c => c.name === 'admin-queue').sendMessage(`The report for ${state[msg.id].embed.title} has been dismissed, no action has been taken.`).then(o => {
            setTimeout(() => bot.Messages.deleteMessages([o.id, msg.id], bot.Channels.find(c => c.name === 'admin-queue').id), 5000)
          })
          delete state[msg.id]
        } else if (reaction.id === '302137375092375553') {
          genlog.log(bot, user, {
            message: 'Approved a report',
            affected: state[msg.id].UvId,
            result: `Card with ID ${state[msg.id].UvId} has been deleted`
          })

          bot.Channels.find(c => c.name === 'admin-queue').sendMessage(`The report for ${state[msg.id].embed.title} has been approved, the card has been deleted from Uservoice.`).then(o => {
            setTimeout(() => bot.Messages.deleteMessages([o.id, msg.id], bot.Channels.find(c => c.name === 'admin-queue').id), 5000)
          })
          deleteFromUV(state[msg.id].UvId, uv, bot)
          delete state[msg.id]
        }
        break
      }
    }
  }
}

function switchIDs (og, bot) {
  bot.Channels.find(c => c.name === 'admin-queue').sendMessage('The following card was reported as inappropriate, please confirm this report.\n**Confirming this report will DESTROY the card, please be certain.**', false, og.embed).then(b => {
    b.addReaction({
      name: 'approve',
      id: '302137375092375553'
    })
    b.addReaction({
      name: 'deny',
      id: '302137375113609219'
    })
    state[b.id] = og
  })
}

function deleteFromUV (UVID, uvClient, bot) {
  uvClient.loginAsOwner().then(i => {
    i.delete(`forums/${config.uservoice.forumId}/suggestions/${UVID}.json`).catch((e) => {
      logger.log(bot, {
        cause: 'card_destroy',
        message: (e.message !== undefined) ? e.message : JSON.stringify(e)
      })
    })
  }).catch((e) => {
    logger.log(bot, {
      cause: 'card_destroy',
      message: (e.message !== undefined) ? e.message : JSON.stringify(e)
    })
  })
}

function getMail (uv, user) {
  return new Promise(function (resolve, reject) {
    if (config.debug === true) return resolve('hello@dougley.com') // no dox pls
    uv.loginAsOwner().then(i => {
      i.get('users/search.json', {
        guid: user
      }).then((data) => {
        if (data.users === undefined || data.users.length !== 1) {
          return reject('Not found')
        } else {
          return resolve(data.users[0].email)
        }
      }).catch(reject)
    }).catch(reject)
  })
}

function saveTheWorld () {
  console.log('About to exit! Attempting to dump current report memory to file...')
  try {
    require('fs').writeFileSync('./dump.json', JSON.stringify(state))
    console.log('Queue saved.')
  } catch (e) {
    console.log('Error when writing json file! QUEUE HAS NOT BEEN SAVED!')
  }
  process.exit()
}

process.on('exit', () => {
  saveTheWorld()
})

exports.Commands = commands
<commit_msg>Delete command<commit_after>var commands = []
var state = {}
var checker = require('../../Utils/access_checker')
var logger = require('../../Utils/error_loggers')
var genlog = require('../../Utils/generic_logger')
var config = require('../../config.js')
var bugsnag = require('bugsnag')

var UVRegex = /http[s]?:\/\/[\w.]*\/forums\/([0-9]{6,})-[\w-]+\/suggestions\/([0-9]{8,})-[\w-]*/

bugsnag.register(config.discord.bugsnag)

try {
  state = JSON.parse(require('fs').readFileSync('./dump.json', 'utf8'))
  console.log('Queue restored!')
} catch (e) {
  console.log('Failed to restore queue! ' + e.message)
}

commands.newCardInit = {
  internal: true,
  fn: function (msg) {
    state[msg.id] = {
      type: 'newCard',
      reports: 0,
      embed: msg.embeds[0],
      UvId: msg.embeds[0].footer.text.split('ID: ')[1]
    }
    msg.addReaction({
      id: '302137374920671233',
      name: 'report'
    })
    msg.addReaction({
      id: '302138464986595339',
      name: 'upvote'
    })
  }
}

commands.delete = {
  modOnly: true,
  adminOnly: false,
  fn: function(bot, msg, suffix, uv, cBack) {
    msg.channel.sendTyping()
    let parts = suffix.split(' ')[0].match(UVRegex)
    let part = suffix.split(' ')
    part.shift()
    let content = part.join(' ')
    if (content.length === 0) {
      msg.reply('you need to provide a reason.')
      return
    }
    let id
    if (parts === null) {
      id = suffix.split(' ')[0]
    } else {
      id = parts[2]
    }
    uv.loginAsOwner().then(c => {
      c.get(`forums/${config.uservoice.forumId}/suggestions/${id}.json`).then((data) => {
        msg.reply(`you're about to mark ${id} for **DELETION** because \`${content}\`\n__Are you sure this is correct?__ (yes/no)`).then(() => {
          wait(bot, msg).then((r) => {
            if (r === null) {
              msg.reply('you took too long to anwser, the operation has been cancelled.')
            }
            if (r === false) {
              msg.reply('thanks for reconsidering, the operation has been cancelled.')
            }
            if (r === true) {
              msg.reply('your report has been sent to the admins, thanks!')
              bot.Channels.find(f => f.name === 'admin-queue').sendMessage(`The following card has been marked for ***deletion*** by ${msg.author.username}#${msg.author.discriminator} for the following reason:\n${content}\n\nPlease review this report.`, false, {
                color: 0x3498db,
                author: {
                  name: data.suggestion.creator.name,
                  icon_url: data.suggestion.creator.avatar_url,
                  url: data.suggestion.creator.url
                },
                title: data.suggestion.title,
                description: (data.suggestion.text.length < 1900) ? data.suggestion.text : '*Content too long*',
                url: data.suggestion.url,
                footer: {
                  text: data.suggestion.category.name
                }
              }).then(b => {
                b.addReaction({
                  name: 'approve',
                  id: '302137375092375553'
                })
                b.addReaction({
                  name: 'deny',
                  id: '302137375113609219'
                })
                state[b.id] = {
                  type: 'adminReviewDelete',
                  author: msg.author,
                  UvId: id,
                  embed: b.embeds[0]
                }
              })
            }
          })
        })
      }).catch((e) => {
        if (e.statusCode === 404) {
          msg.reply('unable to find a suggestion using your query.')
        } else {
          logger.log(bot, {
            cause: 'delete_search',
            message: (e.message !== undefined) ? e.message : JSON.stringify(e)
          })
          msg.reply('an error occured, please try again later.')
        }
      })
    })
  }
}

commands.registerVote = {
  internal: true,
  fn: function (msg, reaction, bot, uv, user) {
    if (msg === null) {
      console.warn('Warning! Vote registering failed due to the message not being cached!')
      return
    }
    if (state[msg.id] === undefined) {
      console.warn('Warning! Vote registering failed due to the report not being known to memory!')
      return
    }
    switch (state[msg.id].type) {
      case 'newCard': {
        if (reaction.id === '302137374920671233') {
          checker.getLevel(user.memberOf('268811439588900865'), function (l) {
            if (l > 0) {
              state[msg.id].reports++
              genlog.log(bot, user, {
                message: 'Reported a card as inappropriate in the feed',
                affected: state[msg.id].UvId,
                result: (state[msg.id].reports === config.discord.reportThreshold) ? 'Report has been sent to admins': undefined
              })
              if (state[msg.id].reports === config.discord.reportThreshold) {
                bot.Channels.get(config.discord.feedChannel).sendMessage(`Feedback with ID ${state[msg.id].UvId} (${msg.embeds[0].title}) has been sent off for admin review.`)
                state[msg.id].type = 'adminReviewDelete'
                switchIDs(state[msg.id], bot)
                delete state[msg.id]
              }
            }
          })
        } else if (reaction.id === '302138464986595339') {
          getMail(uv, user.id).then(f => {
            uv.loginAs(f).then(c => {
              c.post(`forums/${config.uservoice.forumId}/suggestions/${state[msg.id].UvId}/votes.json`, {
                to: 1
              }).then((s) => {
                if (user !== null) {
                  genlog.log(bot, user, {
                    message: 'Feed-voted',
                    affected: state[msg.id].UvId
                  })
                  bot.Channels.get(config.discord.feedChannel).sendMessage(`${user.mention}, your vote has been registered.`).then(c => {
                    setTimeout(() => c.delete(), 5000)
                  })
                }
              }).catch(e => {
                if (e.statusCode === 404) {
                  logger.log(bot, {
                    cause: 'feed_vote',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                } else {
                  logger.log(bot, {
                    cause: 'feed_vote_apply',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                }
              })
            }).catch(e => {
              logger.log(bot, {
                cause: 'login_as',
                message: (e.message !== undefined) ? e.message : JSON.stringify(e)
              }).catch(e => {
                 if (e === 'Not found') {
                  bot.Channels.get(config.discord.feedChannel).sendMessage(`${user.mention}, your details are not found.`).then(c => {
                    setTimeout(() => c.delete(), 5000)
                  })
                } else {
                  logger.log(bot, {
                    cause: 'email_search',
                    message: (e.message !== undefined) ? e.message : JSON.stringify(e)
                  })
                }
              })
            })
          })
        }
        break
      }
      case 'adminReviewDelete': {
        if (reaction.id === '302137375113609219') {
          genlog.log(bot, user, {
            message: 'Dismissed a report',
            affected: state[msg.id].UvId,
          })
          bot.Channels.find(c => c.name === 'admin-queue').sendMessage(`The report for ${state[msg.id].embed.title} has been dismissed, no action has been taken.`).then(o => {
            setTimeout(() => bot.Messages.deleteMessages([o.id, msg.id], bot.Channels.find(c => c.name === 'admin-queue').id), 5000)
          })
          delete state[msg.id]
        } else if (reaction.id === '302137375092375553') {
          genlog.log(bot, user, {
            message: 'Approved a report',
            affected: state[msg.id].UvId,
            result: `Card with ID ${state[msg.id].UvId} has been deleted`
          })

          bot.Channels.find(c => c.name === 'admin-queue').sendMessage(`The report for ${state[msg.id].embed.title} has been approved, the card has been deleted from Uservoice.`).then(o => {
            setTimeout(() => bot.Messages.deleteMessages([o.id, msg.id], bot.Channels.find(c => c.name === 'admin-queue').id), 5000)
          })
          deleteFromUV(state[msg.id].UvId, uv, bot)
          delete state[msg.id]
        }
        break
      }
    }
  }
}

function switchIDs (og, bot) {
  bot.Channels.find(c => c.name === 'admin-queue').sendMessage('The following card was reported as inappropriate, please confirm this report.\n**Confirming this report will DESTROY the card, please be certain.**', false, og.embed).then(b => {
    b.addReaction({
      name: 'approve',
      id: '302137375092375553'
    })
    b.addReaction({
      name: 'deny',
      id: '302137375113609219'
    })
    state[b.id] = og
  })
}

function deleteFromUV (UVID, uvClient, bot) {
  uvClient.loginAsOwner().then(i => {
    i.delete(`forums/${config.uservoice.forumId}/suggestions/${UVID}.json`).catch((e) => {
      logger.log(bot, {
        cause: 'card_destroy',
        message: (e.message !== undefined) ? e.message : JSON.stringify(e)
      })
    })
  }).catch((e) => {
    logger.log(bot, {
      cause: 'card_destroy',
      message: (e.message !== undefined) ? e.message : JSON.stringify(e)
    })
  })
}

function getMail (uv, user) {
  return new Promise(function (resolve, reject) {
    if (config.debug === true) return resolve('hello@dougley.com') // no dox pls
    uv.loginAsOwner().then(i => {
      i.get('users/search.json', {
        guid: user
      }).then((data) => {
        if (data.users === undefined || data.users.length !== 1) {
          return reject('Not found')
        } else {
          return resolve(data.users[0].email)
        }
      }).catch(reject)
    }).catch(reject)
  })
}

function wait(bot, msg) {
  let yn = /^y(es)?$|^n(o)?$/
  return new Promise((resolve, reject) => {
    bot.Dispatcher.on('MESSAGE_CREATE', function doStuff(c) {
      var time = setTimeout(() => {
        resolve(null)
        bot.Dispatcher.removeListener('MESSAGE_CREATE', doStuff)
      }, config.timeouts.duplicateConfirm) // We won't wait forever for the person to anwser
      if (c.message.channel.id !== msg.channel.id) return
      if (c.message.author.id !== msg.author.id) return
      if (c.message.content.match(yn) === null) return
      else {
        resolve((c.message.content.match(/^y(es)?/) !== null) ? true : false)
        bot.Dispatcher.removeListener('MESSAGE_CREATE', doStuff)
        clearTimeout(time)
      }
    })
  })
}

function saveTheWorld () {
  console.log('About to exit! Attempting to dump current report memory to file...')
  try {
    require('fs').writeFileSync('./dump.json', JSON.stringify(state))
    console.log('Queue saved.')
  } catch (e) {
    console.log('Error when writing json file! QUEUE HAS NOT BEEN SAVED!')
  }
  process.exit()
}

process.on('exit', () => {
  saveTheWorld()
})

exports.Commands = commands
<|endoftext|>


<commit_before>import isEqual from 'lodash/isEqual'

const select = (state, selector) => selector.reduce((prev, current) => prev[current], state)
export default class ReduxWatcher {
	constructor(store) {
		const watchList = this.__watchList = {}
		this.__prevState = store.getState()
		store.subscribe(() => {
			const currentState = store.getState()
			const prevState = this.__prevState
			Object.keys(watchList).forEach(key => {
				const listeners = watchList[key]
				if (!listeners) {
					return
				}
				const selector = JSON.parse(key)
				const prevValue = select(prevState, selector)
				const currentValue = select(currentState, selector)

				if (!isEqual(prevValue, currentValue)) {
					listeners.forEach(listener => listener({
						store,
						selector,
						prevState,
						currentState,
						prevValue,
						currentValue
					}))
				}
			})
			this.__prevState = currentState
		})
		this.watch = this.watch.bind(this)
	}
	watch(selector, listener) {
		const watchList = this.__watchList
		const selectorStr = JSON.stringify(selector)
		watchList[selectorStr] = watchList[selectorStr] || []
		watchList[selectorStr].push(listener)
	}
	off(selector, listener) {
		const selectorStr = JSON.stringify(selector)
		if (!this.__watchList[selectorStr]) {
			throw new Error(`No listener for ${selectorStr}`)
		}
		const listeners = this.__watchList[selectorStr]
		const listenerIndex = listeners.indexOf(listener)
		if (listenerIndex >= 0) {
			listeners.splice(listeners.indexOf(listener), 1)
		} else {
			throw new Error(`No such listener for ${selectorStr}`)
		}
		if (listeners.length === 0) {
			delete this.__watchList[selectorStr]
		}
	}
}<commit_msg>export `select` method<commit_after>import isEqual from 'lodash/isEqual'

const select = (state, selector) => selector.reduce((prev, current) => prev[current], state)
export default class ReduxWatcher {
	constructor(store) {
		const watchList = this.__watchList = {}
		this.__prevState = store.getState()
		store.subscribe(() => {
			const currentState = store.getState()
			const prevState = this.__prevState
			Object.keys(watchList).forEach(key => {
				const listeners = watchList[key]
				if (!listeners) {
					return
				}
				const selector = JSON.parse(key)
				const prevValue = select(prevState, selector)
				const currentValue = select(currentState, selector)

				if (!isEqual(prevValue, currentValue)) {
					listeners.forEach(listener => listener({
						store,
						selector,
						prevState,
						currentState,
						prevValue,
						currentValue
					}))
				}
			})
			this.__prevState = currentState
		})
		this.watch = this.watch.bind(this)
	}
	watch(selector, listener) {
		const watchList = this.__watchList
		const selectorStr = JSON.stringify(selector)
		watchList[selectorStr] = watchList[selectorStr] || []
		watchList[selectorStr].push(listener)
	}
	off(selector, listener) {
		const selectorStr = JSON.stringify(selector)
		if (!this.__watchList[selectorStr]) {
			throw new Error(`No listener for ${selectorStr}`)
		}
		const listeners = this.__watchList[selectorStr]
		const listenerIndex = listeners.indexOf(listener)
		if (listenerIndex >= 0) {
			listeners.splice(listeners.indexOf(listener), 1)
		} else {
			throw new Error(`No such listener for ${selectorStr}`)
		}
		if (listeners.length === 0) {
			delete this.__watchList[selectorStr]
		}
	}
}

ReduxWatcher.select = select<|endoftext|>


<commit_before>import 'babel/polyfill';
import { extend, items } from './utils';

const properties = {
    individual: { required: true, type: 'function' },
    recombine: { required: true, type: 'function' },
    mutate: { required: true, type: 'function' },
    fitness: { required: true, type: 'function' },
    deconstruct: { required: true, type: 'function' },
    reconstruct: { required: true, type: 'function' },
    perfectFitness: { required: true }
};

export default class GeneticAlgorithm {
    constructor(options = {}) {
        this.populationSize = 10;
        this.maxGenerations = 1000;
        extend(this, options);

        for (let [key, value] of items(properties)) {
            if (value.required && !this.hasOwnProperty(key)) {
                throw new Error(`Property "${key}" is required.`);
            } else if (value.type && typeof this[key] !== value.type) {
                throw new Error(`Property "${key}" should be of type `
                    + `"${value.type}".`);
            }
        }
    }
}
<commit_msg>defined generate population method<commit_after>import 'babel/polyfill';
import { extend, items } from './utils';

const properties = {
    individual: { required: true, type: 'function' },
    recombine: { required: true, type: 'function' },
    mutate: { required: true, type: 'function' },
    fitness: { required: true, type: 'function' },
    deconstruct: { required: true, type: 'function' },
    reconstruct: { required: true, type: 'function' },
    perfectFitness: { required: true }
};

export default class GeneticAlgorithm {
    constructor(options = {}) {
        this.populationSize = 10;
        this.maxGenerations = 1000;
        extend(this, options);

        for (let [key, value] of items(properties)) {
            if (value.required && !this.hasOwnProperty(key)) {
                throw new Error(`Property "${key}" is required.`);
            } else if (value.type && typeof this[key] !== value.type) {
                throw new Error(`Property "${key}" should be of type `
                    + `"${value.type}".`);
            }
        }
    }

    generatePopulation() {
        var population = [];
        for (let i = 0; i < this.populationSize; i++) {
            population.push(this.individual());
        }
        return population;
    }
}
<|endoftext|>


<commit_before>/*!
 * base-list <https://github.com/jonschlinkert/base-list>
 *
 * Copyright (c) 2015, Jon Schlinkert.
 * Licensed under the MIT License.
 */

'use strict';

var utils = require('./utils');

/**
 * Build grouped lists of tasks.
 *
 * @param {String} `prop` e.g. `generators`
 * @param {String} `options`
 */

module.exports = function(prop, options) {
  var Questions = utils.Questions;

  return function(app) {

    var treeOpts = {
      name: prop,
      method: 'hierarchy',
      tree: buildTree,
      getLabel: function(app) {
        return app.name || app.options.name;
      }
    };

    app.use(utils.tree(treeOpts));
    app.define('taskList', function(cb) {
      var questions = new Questions(this.options);
      var tree = this.hierarchy();
      var choices = toChoices(tree);

      var results = {};
      results[prop] = {};

      if (!choices.length) {
        // politely inform...
        console.log(utils.colors.cyan('no tasks found.'));
        return cb(null, results);
      }

      var question = {};
      question[prop] = {
        message: 'Pick the ' + options.method + ' and tasks to run',
        type: 'checkbox',
        choices: choices
      };

      questions.ask(question, function(err, answers) {
        if (err) return cb(err);

        answers[prop].forEach(function(answer) {
          var segs = answer.split(':');
          if (segs.length === 1) return;
          utils.union(results[prop], segs[0], (segs[1] || 'default').split(','));
        });
        return cb(null, results);
      });
    });

    /**
     * Return a tree of "apps" (generators, updaters, etc) and
     * their tasks
     */

    function buildTree(app, options) {
      var opts = utils.extend({}, options);
      var name = options.getLabel(app, opts);
      var node = new AppNode(name);

      // add tasks to the node
      if (app.tasks) {
        var hasDefault = app.tasks['default'];
        if (hasDefault) {
          node.hasDefault = true;
          node.metadata.value = name + ':default';
        }

        for (var task in app.tasks) {
          if (task === 'default') continue;
          node.addTask(task);
        }
      }

      // get the children to lookup
      var children = app[prop];
      if (typeof children === 'object') {
        // build a tree for each child node
        for (var key in children) {
          var child = children[key];
          if (typeof child[opts.method] === 'function') {
            node.addApp(child[opts.method](opts));
          } else {
            node.addApp(buildTree(child, opts));
          }
        }
      }
      return node;
    }

    /**
     * Transform tree nodes into a list of choices.
     * Prefix choices with special characters based on their position in the tree.
     * Color lines based on their type (app or task)
     *
     * @param  {Object} `tree` Tree object generated from buildTree function.
     * @param  {String} `prefix` prepend a prefix to the label
     * @param  {Boolean} `last` true if last item in an array.
     * @return {Array} List of choices to pass to questions
     */

    function toChoices(tree) {
      return renderApp(tree, ' ', 0);
    }

    function renderTasks(tasks, prefix, more, lastApp) {
      var list = [];
      var len = tasks.length, i = 0;
      while(len--) {
        list = list.concat(renderTask(tasks[i++], (prefix + (lastApp ? (more === false ? ' ' : '') : '|')), len === 0));
      }
      return list;
    }

    function renderTask(task, prefix, last) {
      var item = {};
      var label = prefix + '';
      if (last === true) label += '└─';
      else if (typeof last !== 'undefined') label += '├─';
      label += utils.colors.green(task.label);

      item.name = label;
      item.value = task.metadata.value;
      item.short = task.metadata.short;
      return item;
    }

    function renderApps(apps, prefix, depth, last) {
      var list = [];
      var len = apps.length, i = 0;
      while (len--) {
        list = list.concat(renderApp(apps[i++], (last === true ? prefix.replace('|', ' ') : prefix) + ' ', depth, len === 0));
        '┬';
      }
      return list;
    }

    function renderApp(app, prefix, depth, last) {
      var item = {};
      var label = prefix + '';
      if (last === true) label += '└─';
      else if (typeof last !== 'undefined') label += '├─';
      label += utils.colors.gray(app.label + (app.hasDefault ? ' (default)' : ''));

      item.name = label;
      item.value = app.metadata.value;
      item.short = app.metadata.short;

      var list = [item];
      var more = !!(app.nodes && app.nodes.length);
      var pad = '';
      if (more && depth && last === true) {
        for (var i = 0; i < depth*2; i++) {
          pad = ' ' + pad;
        }
      }
      if (app.tasks && app.tasks.length) {
        list = list.concat(renderTasks(app.tasks, (typeof last === 'undefined' ? ' ' : prefix + (more ? pad + '|' : ' ') + ' '), more, last));
      }

      if (app.nodes && app.nodes.length) {
        list = list.concat(renderApps(app.nodes, (typeof last === 'undefined' ? '' : prefix + (more ? '|' : '')), depth + 1, last));
      }
      return list;
    }

    function AppNode(name) {
      if (!(this instanceof AppNode)) {
        return new AppNode(name);
      }
      this.type = 'app';
      this.label = name;
      this.metadata = {
        value: name,
        short: name
      };
      this.tasks = [];
      this.nodes = [];
    }

    AppNode.prototype.addTask = function(name) {
      this.tasks.push(new TaskNode(name, this));
      return this;
    };

    AppNode.prototype.addApp = function(node) {
      this.nodes.push(node);
      return this;
    };

    function TaskNode(name, parent) {
      if (!(this instanceof TaskNode)) {
        return new TaskNode(name, parent);
      }
      this.type = 'task';
      this.label = name;
      this.metadata = {
        value: parent.label + ':' + name,
        short: parent.label + ':' + name
      };
    }
  }
};
<commit_msg>adding dependencies and cleaning up formatting<commit_after>/*!
 * base-list <https://github.com/jonschlinkert/base-list>
 *
 * Copyright (c) 2015, Jon Schlinkert.
 * Licensed under the MIT License.
 */

'use strict';

var utils = require('./utils');

/**
 * Build grouped lists of tasks.
 *
 * @param {String} `prop` e.g. `generators`
 * @param {String} `options`
 */

module.exports = function(prop, options) {
  var Questions = utils.Questions;

  return function(app) {

    var treeOpts = {
      name: prop,
      method: 'hierarchy',
      tree: buildTree,
      getLabel: function(app) {
        return app.name || app.options.name;
      }
    };

    app.use(utils.tree(treeOpts));
    app.define('taskList', function(cb) {
      var questions = new Questions(this.options);
      var tree = this.hierarchy();
      var choices = toChoices(tree);

      var results = {};
      results[prop] = {};

      if (!choices.length) {
        // politely inform...
        console.log(utils.colors.cyan('no tasks found.'));
        return cb(null, results);
      }

      var question = {};
      question[prop] = {
        message: 'Pick the ' + options.method + ' and tasks to run',
        type: 'checkbox',
        choices: choices
      };

      questions.ask(question, function(err, answers) {
        if (err) return cb(err);

        answers[prop].forEach(function(answer) {
          var segs = answer.split(':');
          if (segs.length === 1) return;
          utils.union(results[prop], segs[0], (segs[1] || 'default').split(','));
        });
        return cb(null, results);
      });
    });

    /**
     * Return a tree of "apps" (generators, updaters, etc) and
     * their tasks
     */

    function buildTree(app, options) {
      var opts = utils.extend({}, options);
      var name = options.getLabel(app, opts);
      var node = new AppNode(name);

      // add tasks to the node
      if (app.tasks) {
        var hasDefault = app.tasks['default'];
        if (hasDefault) {
          node.hasDefault = true;
          node.metadata.value = name + ':default';
        }

        for (var task in app.tasks) {
          if (task === 'default') continue;
          node.addTask(app.tasks[task]);
        }
      }

      // get the children to lookup
      var children = app[prop];
      if (typeof children === 'object') {
        // build a tree for each child node
        for (var key in children) {
          var child = children[key];
          if (typeof child[opts.method] === 'function') {
            node.addApp(child[opts.method](opts));
          } else {
            node.addApp(buildTree(child, opts));
          }
        }
      }
      return node;
    }

    /**
     * Transform tree nodes into a list of choices.
     * Prefix choices with special characters based on their position in the tree.
     * Color lines based on their type (app or task)
     *
     * @param  {Object} `tree` Tree object generated from buildTree function.
     * @param  {String} `prefix` prepend a prefix to the label
     * @param  {Boolean} `last` true if last item in an array.
     * @return {Array} List of choices to pass to questions
     */

    function toChoices(tree) {
      return renderApp(tree, ' ', 0);
    }

    function renderTasks(tasks, prefix, more, lastApp) {
      var list = [];
      var len = tasks.length, i = 0;
      while(len--) {
        list = list.concat(renderTask(tasks[i++], (prefix + (lastApp ? (more === false ? ' ' : '') : '| ')), len === 0));
      }
      return list;
    }

    function renderTask(task, prefix, last) {
      var item = {};
      var label = prefix + '';
      if (last === true) label += '└─';
      else if (typeof last !== 'undefined') label += '├─';
      // label += utils.colors.green('⚙ ' + task.label);
      label += ' ' + utils.colors.green(task.label);

      if (task.metadata.dependencies && task.metadata.dependencies.length) {
        label += utils.colors.gray(' [' + task.metadata.dependencies.join(', ') + ']');
      }
      item.name = label;
      item.value = task.metadata.value;
      item.short = task.metadata.short;
      return item;
    }

    function renderApps(apps, prefix, depth, last) {
      var list = [];
      var len = apps.length, i = 0;
      while (len--) {
        list = list.concat(renderApp(apps[i++], (last === true ? prefix.replace('|', ' ') : prefix) + ' ', depth, len === 0));
      }
      return list;
    }

    function renderApp(app, prefix, depth, last) {
      var item = {};
      var hasChildren = !!(app.nodes && app.nodes.length);
      var label = prefix + '';
      if (last === true) {
        label += '└─' + (hasChildren ? '┬' : '') + ' ';
      } else if (typeof last !== 'undefined') {
        label += '├─' + (hasChildren ? '┬' : '') + ' ';
      }

      // label += '📱 ' + utils.colors.cyan(app.label) + utils.colors.green(app.hasDefault ? ' (⚙ default)' : '');
      label += utils.colors.cyan(app.label) + utils.colors.green(app.hasDefault ? ' (default)' : '');

      item.name = label;
      item.value = app.metadata.value;
      item.short = app.metadata.short;

      var list = [item];
      var more = !!(app.nodes && app.nodes.length);
      var pad = '';
      if (more && depth && last === true) {
        for (var i = 0; i < depth*2; i++) {
          pad = ' ' + pad;
        }
      }
      if (app.tasks && app.tasks.length) {
        list = list.concat(renderTasks(app.tasks, (typeof last === 'undefined' ? ' ' : prefix + (more ? pad + '|' : ' ') + ' '), more, last));
      }

      if (app.nodes && app.nodes.length) {
        list = list.concat(renderApps(app.nodes, (typeof last === 'undefined' ? '' : prefix + (more ? '|' : '')), depth + 1, last));
      }
      return list;
    }

    function AppNode(name) {
      if (!(this instanceof AppNode)) {
        return new AppNode(name);
      }
      this.type = 'app';
      this.label = name;
      this.metadata = {
        value: name,
        short: name
      };
      this.tasks = [];
      this.nodes = [];
    }

    AppNode.prototype.addTask = function(task) {
      this.tasks.push(new TaskNode(task, this));
      return this;
    };

    AppNode.prototype.addApp = function(node) {
      this.nodes.push(node);
      return this;
    };

    function TaskNode(task, parent) {
      if (!(this instanceof TaskNode)) {
        return new TaskNode(task, parent);
      }
      var name = task.name;
      this.type = 'task';
      this.label = name;
      this.metadata = {
        value: parent.label + ':' + name,
        short: parent.label + ':' + name,
        dependencies: task.deps
      };
    }
  }
};
<|endoftext|>