<commit_before>var mongoose = require('mongoose');
var FeedParser = require('feedparser');
var request = require('request');
var Stream = require('stream');
var chardet = require('chardet');
var iconv = require('iconv-lite');

var feedSchema = mongoose.Schema({
    _id:              { type: String, required: true, unique: true },
    title:            String,
    description:      String,
    link:             String,
    xmlUrl:           String,
    date:             { type: Date },
    pubDate:          { type: Date },
    author:           String,
    language:         String,
    image:            { title: String, url: String },
    favicon:          String,
    copyright:        String,
    generator:        String,
    categories:       [String],
    lastModified:     String
});

var Feed = mongoose.model('Feed', feedSchema);

var articleSchema = mongoose.Schema({
    title:            String,
    description:      String,
    link:             String,
    origLink:         String,
    date:             { type: Date },
    pubDate:          { type: Date },
    author:           String,
    guid:             String,
    comments:         String,
    image:            { title: String, url: String },
    categories:       [String],
    source:           { title: String, url: String },
    enclosures:       [{ url: String, type: String, length: String }],
    feed:             { type: String, ref: 'Feed' },
    savedAt:          { type: Date }
});

var Article = mongoose.model('Article', articleSchema);

var timeoutInMs = 10000;
var defaultMongoDbPort = 27017;
var updateIntervalHandle = null;

function connect(uri, database, port) {
    port = port || defaultMongoDbPort;
    mongoose.connect(uri, database, port);

    mongoose.connection.on('error', function(e){
        logError("Could not connect to MongoDB: "+e.message);
        process.exit(1);
    });

    mongoose.connection.once('open', function callback () {
        logDebug("Connection to MongoDB was successful.");
    });
}

function addFeed(url) {
    Feed.count({ _id: url }, function(error, count) {
        if(error != null) {
            logError('Failed to check if feed exists in MongoDB: '+error);
        }
        else {
            if(count == 0) {
               requestAndParseFeed(url);
            }
            else if(count == 1) {
                logWarning('Feed '+url+' already exists in the MongoDB. '
                + 'Skipping adding new feed to the storage.');
            }
            else {
                logError('Multiple documents with same url exist in '
                + 'MongoDB. This should not happen!');
            }
        }
    });
}

function removeFeed(url) {
    Feed.findByIdAndRemove(url, function(error, feedDocument) {
        if(error != null) {
            logError('Failed to find feed to be removed: '+error);
        }
        else if(feedDocument != null) {
            setTimeout(function() { removeArticlesByFeedUrl(url) }, 5000);
        }
        else {
            logWarn('Feed not found with url '+url+'. Skipping removing.');
        }
    });
}

function removeArticlesByFeedUrl(url) {
    Article.remove({ feed: url }, function (error) {
        if(error != null) {
            logError('Failed to find articles to be removed: '+error);
        }
    });
}

function removeArticlesOlderThan(days) {
    var oldArticleDate = new Date();
    oldArticleDate.setDate(oldArticleDate.getDate()-days);

    Article.remove({ date: { $lt: oldArticleDate } }, function (error) {
        if(error != null) {
            logError('Failed to remove articles older than '+days+': '+error);
        }
    });
}

function requestAndParseFeed(url) {
    var requestObject = createDefaultRequest(url);

    Feed.findOne({ _id: url }, 'lastModified', function(error,
        partialFeedDocument) {
        if(error != null) {
            logError('Failed to get feeds Last-Modified from MongoDB: '+
                error);
        }
        else if(partialFeedDocument != null &&
            partialFeedDocument.lastModified != null) {
            requestObject.headers = { 'If-Modified-Since':
                partialFeedDocument.lastModified }
        }

        request(requestObject, function(error, response, body) {
            handleResponse(url, error, response, body)
        });
    });
}

function createDefaultRequest(url) {
    return { url: url, timeout: timeoutInMs, encoding: null };
}

function handleResponse(url, error, response, body) {
    if(error != null) {
        logError('Failed to handle HTTP response: '+error);
    }
    else if (response.statusCode == 200) {
        var lastModified = getLastModifiedFromResponseHeaders(response.headers);
        body = getBufferAsUtf8Buffer(body);

        //Feedparser no longer supports string, so convert body to stream again.
        //This is a little dirty, but needed because before parsing the body
        //it is a good idea to check http status code and Last-Modified
        var bodyAsStream = Stream.PassThrough();
        bodyAsStream.write(body);
        bodyAsStream.end();

        bodyAsStream.pipe(new FeedParser())
        .on('error', function (error) {
            handleParseError(error, url);
        })
        .on('meta', function (meta) {
            saveOrUpdateFeedMeta(meta, url, lastModified);
        })
        .on('readable', function() {
            var stream = this;
            var article;

            while (article = stream.read()) {
                saveOrUpdateArticle(article, url);
            }

        });
    }
    else if (response.statusCode == 304) {

    }
    else {
        logWarning('HTTP status code received that can not be handled by '
            + 'this module: '+response.statusCode);
    }
}

function getBufferAsUtf8Buffer(buffer) {
    var encoding = chardet.detect(buffer);

    if(encoding != 'UTF-8') {
        decodedString = iconv.decode(buffer, encoding);
        buffer = iconv.encode(decodedString, 'utf8');
    }

    return buffer;
}

function handleParseError(error, url) {
    logError('Could not parse feed at '+url+'. '+error);
}

function getLastModifiedFromResponseHeaders(headers) {
    var lastModified = null;

    for(var headerName in headers){
        if(headerName.toLowerCase() == 'last-modified') {
            lastModified = headers[headerName];
        }
    }

    return lastModified;
}

function saveOrUpdateFeedMeta(meta, url, lastModified) {
    Feed.findOne({ _id: url }, function(error, feedDocument) {
        if(error != null) {
            logError('Failed to get feed from MongoDB: '+error);
        }
        else {
            if(feedDocument == null) {
                saveFeedMeta(meta, url, lastModified);
            }
            else {
                updateFeedMeta(feedDocument, meta, url, lastModified);
            }
        }
    });
}

function saveFeedMeta(meta, url, lastModified) {
    var feedDocument = createFeedDocument(meta, url, lastModified);

    feedDocument.save(function (error, feedDocument) {
        if(error != null) {
            logError('Failed to save feed meta to MongoDB: '+error);
        }
        else {
            logDebug('Added new feed "'+feedDocument._id+'" to MongoDB.');
        }
    });
}

function updateFeedMeta(feedDocument, meta, lastModified) {
    if(feedRequiresUpdate(feedDocument, meta, lastModified)) {
        feedDocument.title = meta.title;
        feedDocument.description = meta.description;
        feedDocument.link = meta.link;
        feedDocument.xmlUrl = meta.xmlUrl;
        feedDocument.date = meta.date;
        feedDocument.pubDate = meta.pubDate;
        feedDocument.author = meta.author;
        feedDocument.language = meta.language;
        feedDocument.image = meta.image;
        feedDocument.favicon = meta.favicon;
        feedDocument.copyright = meta.copyright;
        feedDocument.generator = meta.generator;
        feedDocument.categories = meta.categories;
        feedDocument.lastModified = lastModified;

        feedDocument.save(function (error, feedDocument) {
            if(error != null) {
                logError('Failed to update feed meta to MongoDB: '+error);
            }
            else {
                logDebug('Updated feed "'+feedDocument._id+'" in MongoDB.');
            }
        });
    }
}

function feedRequiresUpdate(feedDocument, meta, lastModified) {
    var requiresUpdate = false;

    if(feedDocument.title != meta.title &&
        feedDocument.description != meta.description &&
        feedDocument.link != meta.link &&
        feedDocument.xmlUrl != meta.xmlUrl &&
        feedDocument.author != meta.author &&
        feedDocument.language != meta.language &&
        feedDocument.image.title != meta.image.title &&
        feedDocument.image.url != meta.image.url &&
        feedDocument.favicon != meta.favicon &&
        feedDocument.copyright != meta.copyright &&
        feedDocument.generator != meta.generator &&
        JSON.stringify(feedDocument.categories) !=
            JSON.stringify(meta.categories) &&
        feedDocument.lastModified != lastModified) {
        requiresUpdate = true;
    }

    return requiresUpdate;
}

function createFeedDocument(meta, url, lastModified) {
    return new Feed({ _id: url, title: meta.title,
        description: meta.description, link: meta.link, xmlUrl: meta.xmlUrl,
        date: meta.date, pubDate: meta.pubDate, author: meta.author,
        language: meta.language, image: meta.image, favicon: meta.favicon,
        copyright: meta.copyright, generator: meta.generator,
        categories: meta.categories, lastModified: lastModified });
}

function saveOrUpdateArticle(article, url) {
    Article.findOne({ guid: article.guid, feed: url }, function(error,
        articleDocument) {
        if(error != null) {
            logError('Failed to get article from MongoDB: '+error);
        }
        else {
            if(articleDocument == null) {
                saveArticle(article, url);
            }
            else {
                updateArticle(articleDocument, article);
            }
        }
    });
}

function saveArticle(article, url) {
    var articleDocument = createArticleDocument(article, url);

    articleDocument.save(function (error, articleDocument) {
        if(error != null) {
            logError('Failed to save article to MongoDB: '+error);
        }
        else {
            logDebug('Added new article "'+articleDocument.guid
            + '" to MongoDB.');
        }
    });
}

function updateArticle(articleDocument, article) {
    if(articleRequiresUpdate(articleDocument, article)) {
        articleDocument.title = article.title;
        articleDocument.description = article.description;
        articleDocument.link = article.link;
        articleDocument.xmlUrl = article.xmlUrl;
        articleDocument.date = article.date;
        articleDocument.pubDate = article.pubDate;
        articleDocument.author = article.author;
        articleDocument.language = article.language;
        articleDocument.image = article.image;
        articleDocument.favicon = article.favicon;
        articleDocument.copyright = article.copyright;
        articleDocument.generator = article.generator;
        articleDocument.categories = article.categories;

        articleDocument.save(function (error, feedDocument) {
            if(error != null) {
                logError('Failed to update article to MongoDB: '+error);
            }
            else {
                logDebug('Updated article "'+articleDocument.guid
                +'" in MongoDB.');
            }
        });
    }
}

function articleRequiresUpdate(articleDocument, article) {
    var requiresUpdate = false;

    if(articleDocument.title != article.title &&
        articleDocument.description != article.description &&
        articleDocument.link != article.link &&
        articleDocument.origLink != article.origLink &&
        articleDocument.author != article.author &&
        articleDocument.guid != article.guid &&
        articleDocument.comments != article.comments &&
        articleDocument.image.title != article.image.title &&
        articleDocument.image.url != article.image.url &&
        JSON.stringify(articleDocument.categories) !=
            JSON.stringify(article.categories) &&
        articleDocument.source.title != article.source.title &&
        articleDocument.source.url != article.source.url &&
        articleDocument.enclosures.url != article.enclosures.url &&
        articleDocument.enclosures.type != article.enclosures.type &&
        articleDocument.enclosures.length != article.enclosures.length) {
        requiresUpdate = true;
    }

    return requiresUpdate;

}

function createArticleDocument(article, url) {
    return new Article({ title: article.title,
        description: article.description, link: article.link,
        origLink: article.origLink, date: article.date,
        pubDate: article.pubDate, author: article.author, guid: article.guid,
        comments: article.comments, image: article.image,
        categories: article.categories, source: article.source,
        enclosures: article.enclosures, feed: url, savedAt: new Date() });
}

function updateDatabase() {
    Feed.find(function(error, feedDocuments) {
        if(error != null) {
            logError('Failed to get feeds from MongoDB: '+error);
        }
        else {
            feedDocuments.forEach(function(feedDocument) {
                requestAndParseFeed(feedDocument._id);
            });
        }
    });
}

function updateDatabaseAtInterval(milliseconds) {
    if(updateIntervalHandle == null) {
        updateIntervalHandle = setInterval(updateDatabase, milliseconds);
    }
    else {
        logWarning('Database update interval already set. Skipping.');
    }
}

function stopUpdateDataBaseAtInterval() {
    if(updateIntervalHandle != null) {
        clearInterval(updateIntervalHandle);
        updateIntervalHandle = null;
    }
}

function getArticlesByKeyword(keyword, options, callback) {
    if(typeof callback == 'undefined' && typeof options == 'function') {
        callback = options;
        options = {};
    }

    var query = createQueryForArticles(keyword, options);
    executeArticleQuery(query, callback);
}

function getArticlesByKeywordArray(keywords, options, callback) {
    var keyword = '('+keywords.join('|')+')';
    getArticlesByKeyword(keyword, options, callback);
}

function createQueryForArticles(keyword, options) {
    var allowedCharsBeforeAndAfter = '\\s|\\.|\,|\!|\\?|:|-|_|\'|"|@|#|\$|'
        +'%|&|\\(|\\)|\\[|\\]|{|}|\\+';
    var searchTerm = new RegExp('.*(^|'+allowedCharsBeforeAndAfter+')+'
        +keyword+'('+allowedCharsBeforeAndAfter+'|$)+.*', 'i');
    var criteria = { $or: [ { title: searchTerm }, { description:
            searchTerm }, { author: searchTerm } ] };

    if(typeof options.from != 'undefined') {
        criteria.savedAt = criteria.savedAt || {};
        criteria.savedAt.$gte = options.from;
    }

    if(typeof options.to != 'undefined') {
        criteria.savedAt = criteria.savedAt || {};
        criteria.savedAt.$lte = options.to;
    }

    var query = Article.find(criteria);

    if(typeof options.limit != 'undefined') {
        query.limit(options.limit);
    }

    query.sort({savedAt: 'desc'});
    query.populate('feed', 'title');

    return query;
}

function executeArticleQuery(query, callback) {
    query.execFind(function (error, articleDocuments) {
        if(error != null) {
            logError('Failed to get articles from MongoDB: '+error);
        }
        else {
            callback(articleDocuments);
        }
    });
}

function logDebug(message) {
    console.log(getTimeStampForLog()+' [DEBUG] '+message);
}

function logWarning(message) {
    console.warn(getTimeStampForLog()+' [WARN] '+message);
}

function logError(message) {
    console.error(getTimeStampForLog()+' [ERROR] '+message);
}

function getTimeStampForLog() {
    return new Date().toUTCString();
}

exports.connect = connect;
exports.addFeed = addFeed;
exports.updateDatabase = updateDatabase;
exports.updateDatabaseAtInterval = updateDatabaseAtInterval;
exports.stopUpdateDataBaseAtInterval = stopUpdateDataBaseAtInterval;
exports.getArticlesByKeyword = getArticlesByKeyword;
exports.getArticlesByKeywordArray = getArticlesByKeywordArray;
exports.removeFeed = removeFeed;
exports.removeArticlesOlderThan = removeArticlesOlderThan;
<commit_msg>Modified the code for latest version of Mongoose.<commit_after>var mongoose = require('mongoose');
var FeedParser = require('feedparser');
var request = require('request');
var Stream = require('stream');
var chardet = require('chardet');
var iconv = require('iconv-lite');

var feedSchema = mongoose.Schema({
    _id:              { type: String, required: true, unique: true },
    title:            String,
    description:      String,
    link:             String,
    xmlUrl:           String,
    date:             { type: Date },
    pubDate:          { type: Date },
    author:           String,
    language:         String,
    image:            { title: String, url: String },
    favicon:          String,
    copyright:        String,
    generator:        String,
    categories:       [String],
    lastModified:     String
});

var Feed = mongoose.model('Feed', feedSchema);

var articleSchema = mongoose.Schema({
    title:            String,
    description:      String,
    link:             String,
    origLink:         String,
    date:             { type: Date },
    pubDate:          { type: Date },
    author:           String,
    guid:             String,
    comments:         String,
    image:            { title: String, url: String },
    categories:       [String],
    source:           { title: String, url: String },
    enclosures:       [{ url: String, type: String, length: String }],
    feed:             { type: String, ref: 'Feed' },
    savedAt:          { type: Date }
});

var Article = mongoose.model('Article', articleSchema);

var timeoutInMs = 10000;
var defaultMongoDbPort = 27017;
var updateIntervalHandle = null;

function connect(uri, database, port) {
    port = port || defaultMongoDbPort;
    mongoose.connect(uri, database, port);

    mongoose.connection.on('error', function(e){
        logError("Could not connect to MongoDB: "+e.message);
        process.exit(1);
    });

    mongoose.connection.once('open', function callback () {
        logDebug("Connection to MongoDB was successful.");
    });
}

function addFeed(url) {
    Feed.count({ _id: url }, function(error, count) {
        if(error != null) {
            logError('Failed to check if feed exists in MongoDB: '+error);
        }
        else {
            if(count == 0) {
               requestAndParseFeed(url);
            }
            else if(count == 1) {
                logWarning('Feed '+url+' already exists in the MongoDB. '
                + 'Skipping adding new feed to the storage.');
            }
            else {
                logError('Multiple documents with same url exist in '
                + 'MongoDB. This should not happen!');
            }
        }
    });
}

function removeFeed(url) {
    Feed.findByIdAndRemove(url, function(error, feedDocument) {
        if(error != null) {
            logError('Failed to find feed to be removed: '+error);
        }
        else if(feedDocument != null) {
            setTimeout(function() { removeArticlesByFeedUrl(url) }, 5000);
        }
        else {
            logWarn('Feed not found with url '+url+'. Skipping removing.');
        }
    });
}

function removeArticlesByFeedUrl(url) {
    Article.remove({ feed: url }, function (error) {
        if(error != null) {
            logError('Failed to find articles to be removed: '+error);
        }
    });
}

function removeArticlesOlderThan(days) {
    var oldArticleDate = new Date();
    oldArticleDate.setDate(oldArticleDate.getDate()-days);

    Article.remove({ date: { $lt: oldArticleDate } }, function (error) {
        if(error != null) {
            logError('Failed to remove articles older than '+days+': '+error);
        }
    });
}

function requestAndParseFeed(url) {
    var requestObject = createDefaultRequest(url);

    Feed.findOne({ _id: url }, 'lastModified', function(error,
        partialFeedDocument) {
        if(error != null) {
            logError('Failed to get feeds Last-Modified from MongoDB: '+
                error);
        }
        else if(partialFeedDocument != null &&
            partialFeedDocument.lastModified != null) {
            requestObject.headers = { 'If-Modified-Since':
                partialFeedDocument.lastModified }
        }

        request(requestObject, function(error, response, body) {
            handleResponse(url, error, response, body)
        });
    });
}

function createDefaultRequest(url) {
    return { url: url, timeout: timeoutInMs, encoding: null };
}

function handleResponse(url, error, response, body) {
    if(error != null) {
        logError('Failed to handle HTTP response: '+error);
    }
    else if (response.statusCode == 200) {
        var lastModified = getLastModifiedFromResponseHeaders(response.headers);
        body = getBufferAsUtf8Buffer(body);

        //Feedparser no longer supports string, so convert body to stream again.
        //This is a little dirty, but needed because before parsing the body
        //it is a good idea to check http status code and Last-Modified
        var bodyAsStream = Stream.PassThrough();
        bodyAsStream.write(body);
        bodyAsStream.end();

        bodyAsStream.pipe(new FeedParser())
        .on('error', function (error) {
            handleParseError(error, url);
        })
        .on('meta', function (meta) {
            saveOrUpdateFeedMeta(meta, url, lastModified);
        })
        .on('readable', function() {
            var stream = this;
            var article;

            while (article = stream.read()) {
                saveOrUpdateArticle(article, url);
            }

        });
    }
    else if (response.statusCode == 304) {

    }
    else {
        logWarning('HTTP status code received that can not be handled by '
            + 'this module: '+response.statusCode);
    }
}

function getBufferAsUtf8Buffer(buffer) {
    var encoding = chardet.detect(buffer);

    if(encoding != 'UTF-8') {
        decodedString = iconv.decode(buffer, encoding);
        buffer = iconv.encode(decodedString, 'utf8');
    }

    return buffer;
}

function handleParseError(error, url) {
    logError('Could not parse feed at '+url+'. '+error);
}

function getLastModifiedFromResponseHeaders(headers) {
    var lastModified = null;

    for(var headerName in headers){
        if(headerName.toLowerCase() == 'last-modified') {
            lastModified = headers[headerName];
        }
    }

    return lastModified;
}

function saveOrUpdateFeedMeta(meta, url, lastModified) {
    Feed.findOne({ _id: url }, function(error, feedDocument) {
        if(error != null) {
            logError('Failed to get feed from MongoDB: '+error);
        }
        else {
            if(feedDocument == null) {
                saveFeedMeta(meta, url, lastModified);
            }
            else {
                updateFeedMeta(feedDocument, meta, url, lastModified);
            }
        }
    });
}

function saveFeedMeta(meta, url, lastModified) {
    var feedDocument = createFeedDocument(meta, url, lastModified);

    feedDocument.save(function (error, feedDocument) {
        if(error != null) {
            logError('Failed to save feed meta to MongoDB: '+error);
        }
        else {
            logDebug('Added new feed "'+feedDocument._id+'" to MongoDB.');
        }
    });
}

function updateFeedMeta(feedDocument, meta, lastModified) {
    if(feedRequiresUpdate(feedDocument, meta, lastModified)) {
        feedDocument.title = meta.title;
        feedDocument.description = meta.description;
        feedDocument.link = meta.link;
        feedDocument.xmlUrl = meta.xmlUrl;
        feedDocument.date = meta.date;
        feedDocument.pubDate = meta.pubDate;
        feedDocument.author = meta.author;
        feedDocument.language = meta.language;
        feedDocument.image = meta.image;
        feedDocument.favicon = meta.favicon;
        feedDocument.copyright = meta.copyright;
        feedDocument.generator = meta.generator;
        feedDocument.categories = meta.categories;
        feedDocument.lastModified = lastModified;

        feedDocument.save(function (error, feedDocument) {
            if(error != null) {
                logError('Failed to update feed meta to MongoDB: '+error);
            }
            else {
                logDebug('Updated feed "'+feedDocument._id+'" in MongoDB.');
            }
        });
    }
}

function feedRequiresUpdate(feedDocument, meta, lastModified) {
    var requiresUpdate = false;

    if(feedDocument.title != meta.title &&
        feedDocument.description != meta.description &&
        feedDocument.link != meta.link &&
        feedDocument.xmlUrl != meta.xmlUrl &&
        feedDocument.author != meta.author &&
        feedDocument.language != meta.language &&
        feedDocument.image.title != meta.image.title &&
        feedDocument.image.url != meta.image.url &&
        feedDocument.favicon != meta.favicon &&
        feedDocument.copyright != meta.copyright &&
        feedDocument.generator != meta.generator &&
        JSON.stringify(feedDocument.categories) !=
            JSON.stringify(meta.categories) &&
        feedDocument.lastModified != lastModified) {
        requiresUpdate = true;
    }

    return requiresUpdate;
}

function createFeedDocument(meta, url, lastModified) {
    return new Feed({ _id: url, title: meta.title,
        description: meta.description, link: meta.link, xmlUrl: meta.xmlUrl,
        date: meta.date, pubDate: meta.pubDate, author: meta.author,
        language: meta.language, image: meta.image, favicon: meta.favicon,
        copyright: meta.copyright, generator: meta.generator,
        categories: meta.categories, lastModified: lastModified });
}

function saveOrUpdateArticle(article, url) {
    Article.findOne({ guid: article.guid, feed: url }, function(error,
        articleDocument) {
        if(error != null) {
            logError('Failed to get article from MongoDB: '+error);
        }
        else {
            if(articleDocument == null) {
                saveArticle(article, url);
            }
            else {
                updateArticle(articleDocument, article);
            }
        }
    });
}

function saveArticle(article, url) {
    var articleDocument = createArticleDocument(article, url);

    articleDocument.save(function (error, articleDocument) {
        if(error != null) {
            logError('Failed to save article to MongoDB: '+error);
        }
        else {
            logDebug('Added new article "'+articleDocument.guid
            + '" to MongoDB.');
        }
    });
}

function updateArticle(articleDocument, article) {
    if(articleRequiresUpdate(articleDocument, article)) {
        articleDocument.title = article.title;
        articleDocument.description = article.description;
        articleDocument.link = article.link;
        articleDocument.xmlUrl = article.xmlUrl;
        articleDocument.date = article.date;
        articleDocument.pubDate = article.pubDate;
        articleDocument.author = article.author;
        articleDocument.language = article.language;
        articleDocument.image = article.image;
        articleDocument.favicon = article.favicon;
        articleDocument.copyright = article.copyright;
        articleDocument.generator = article.generator;
        articleDocument.categories = article.categories;

        articleDocument.save(function (error, feedDocument) {
            if(error != null) {
                logError('Failed to update article to MongoDB: '+error);
            }
            else {
                logDebug('Updated article "'+articleDocument.guid
                +'" in MongoDB.');
            }
        });
    }
}

function articleRequiresUpdate(articleDocument, article) {
    var requiresUpdate = false;

    if(articleDocument.title != article.title &&
        articleDocument.description != article.description &&
        articleDocument.link != article.link &&
        articleDocument.origLink != article.origLink &&
        articleDocument.author != article.author &&
        articleDocument.guid != article.guid &&
        articleDocument.comments != article.comments &&
        articleDocument.image.title != article.image.title &&
        articleDocument.image.url != article.image.url &&
        JSON.stringify(articleDocument.categories) !=
            JSON.stringify(article.categories) &&
        articleDocument.source.title != article.source.title &&
        articleDocument.source.url != article.source.url &&
        articleDocument.enclosures.url != article.enclosures.url &&
        articleDocument.enclosures.type != article.enclosures.type &&
        articleDocument.enclosures.length != article.enclosures.length) {
        requiresUpdate = true;
    }

    return requiresUpdate;

}

function createArticleDocument(article, url) {
    return new Article({ title: article.title,
        description: article.description, link: article.link,
        origLink: article.origLink, date: article.date,
        pubDate: article.pubDate, author: article.author, guid: article.guid,
        comments: article.comments, image: article.image,
        categories: article.categories, source: article.source,
        enclosures: article.enclosures, feed: url, savedAt: new Date() });
}

function updateDatabase() {
    Feed.find(function(error, feedDocuments) {
        if(error != null) {
            logError('Failed to get feeds from MongoDB: '+error);
        }
        else {
            feedDocuments.forEach(function(feedDocument) {
                requestAndParseFeed(feedDocument._id);
            });
        }
    });
}

function updateDatabaseAtInterval(milliseconds) {
    if(updateIntervalHandle == null) {
        updateIntervalHandle = setInterval(updateDatabase, milliseconds);
    }
    else {
        logWarning('Database update interval already set. Skipping.');
    }
}

function stopUpdateDataBaseAtInterval() {
    if(updateIntervalHandle != null) {
        clearInterval(updateIntervalHandle);
        updateIntervalHandle = null;
    }
}

function getArticlesByKeyword(keyword, options, callback) {
    if(typeof callback == 'undefined' && typeof options == 'function') {
        callback = options;
        options = {};
    }

    var query = createQueryForArticles(keyword, options);
    executeArticleQuery(query, callback);
}

function getArticlesByKeywordArray(keywords, options, callback) {
    var keyword = '('+keywords.join('|')+')';
    getArticlesByKeyword(keyword, options, callback);
}

function createQueryForArticles(keyword, options) {
    var allowedCharsBeforeAndAfter = '\\s|\\.|\,|\!|\\?|:|-|_|\'|"|@|#|\$|'
        +'%|&|\\(|\\)|\\[|\\]|{|}|\\+';
    var searchTerm = new RegExp('.*(^|'+allowedCharsBeforeAndAfter+')+'
        +keyword+'('+allowedCharsBeforeAndAfter+'|$)+.*', 'i');
    var criteria = { $or: [ { title: searchTerm }, { description:
            searchTerm }, { author: searchTerm } ] };

    if(typeof options.from != 'undefined') {
        criteria.savedAt = criteria.savedAt || {};
        criteria.savedAt.$gte = options.from;
    }

    if(typeof options.to != 'undefined') {
        criteria.savedAt = criteria.savedAt || {};
        criteria.savedAt.$lte = options.to;
    }

    var query = Article.find(criteria);

    if(typeof options.limit != 'undefined') {
        query.limit(options.limit);
    }

    query.sort({savedAt: 'desc'});
    query.populate('feed', 'title');

    return query;
}

function executeArticleQuery(query, callback) {
    query.exec(function (error, articleDocuments) {
        if(error != null) {
            logError('Failed to get articles from MongoDB: '+error);
        }
        else {
            callback(articleDocuments);
        }
    });
}

function logDebug(message) {
    console.log(getTimeStampForLog()+' [DEBUG] '+message);
}

function logWarning(message) {
    console.warn(getTimeStampForLog()+' [WARN] '+message);
}

function logError(message) {
    console.error(getTimeStampForLog()+' [ERROR] '+message);
}

function getTimeStampForLog() {
    return new Date().toUTCString();
}

exports.connect = connect;
exports.addFeed = addFeed;
exports.updateDatabase = updateDatabase;
exports.updateDatabaseAtInterval = updateDatabaseAtInterval;
exports.stopUpdateDataBaseAtInterval = stopUpdateDataBaseAtInterval;
exports.getArticlesByKeyword = getArticlesByKeyword;
exports.getArticlesByKeywordArray = getArticlesByKeywordArray;
exports.removeFeed = removeFeed;
exports.removeArticlesOlderThan = removeArticlesOlderThan;
<|endoftext|>


<commit_before>const parse = require('./parse')
const error = require('./error')
const help = require('./help')

module.exports = function sergeant (name, description, define) {
  if (define == null) {
    define = description

    description = null
  }

  const definitions = {}
  const commands = {}

  option('help', {
    type: Boolean,
    aliases: ['h'],
    description: 'get help'
  })

  let i = 0
  const action = define({option, parameter, command})

  return (argv) => {
    const filtered = argv.filter((arg) => arg !== '-' && !arg.startsWith('-'))

    if (filtered[0] != null && commands[filtered[0]] != null) {
      commands[filtered[0]].action(filtered.slice(1))
    } else {
      const args = parse(argv, definitions)

      try {
        if (args != null) {
          if (args.help === true || action == null) {
            help(name, description, definitions, commands)
          } else if (action != null) {
            const result = action(args)

            if (typeof result === 'object' && result instanceof Promise) {
              result.catch(error)
            }
          }
        }
      } catch (e) {
        error(e)
      }
    }
  }

  function command (subname, description, define) {
    if (define == null) {
      define = description

      description = null
    }

    commands[subname] = {
      action: sergeant(name + ' ' + subname, description, define),
      description
    }
  }

  function option (key, definition) {
    definitions[key] = definition
  }

  function parameter (key, definition) {
    definitions[i++] = Object.assign(definition, {key})
  }
}
<commit_msg>Fix options<commit_after>const parse = require('./parse')
const error = require('./error')
const help = require('./help')

module.exports = function sergeant (name, description, define) {
  if (define == null) {
    define = description

    description = null
  }

  const definitions = {}
  const commands = {}

  option('help', {
    type: Boolean,
    aliases: ['h'],
    description: 'get help'
  })

  let i = 0
  const action = define({option, parameter, command})

  return (argv) => {
    const filtered = argv.filter((arg) => arg !== '-' && !arg.startsWith('-'))

    if (filtered[0] != null && commands[filtered[0]] != null) {
      commands[filtered[0]].action(argv.slice(1))
    } else {
      const args = parse(argv, definitions)

      try {
        if (args != null) {
          if (args.help === true || action == null) {
            help(name, description, definitions, commands)
          } else if (action != null) {
            const result = action(args)

            if (typeof result === 'object' && result instanceof Promise) {
              result.catch(error)
            }
          }
        }
      } catch (e) {
        error(e)
      }
    }
  }

  function command (subname, description, define) {
    if (define == null) {
      define = description

      description = null
    }

    commands[subname] = {
      action: sergeant(name + ' ' + subname, description, define),
      description
    }
  }

  function option (key, definition) {
    definitions[key] = definition
  }

  function parameter (key, definition) {
    definitions[i++] = Object.assign(definition, {key})
  }
}
<|endoftext|>


<commit_before>var app = require('app');
var BrowserWindow = require('browser-window');

var globalWindow = null;

app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  globalWindow = new BrowserWindow({
    'width': 800,
    'height': 600,
    'web-preferences': {
        'web-security': false,
        'webgl': false
    }
  });

  globalWindow.loadUrl('file://index.html');

  globalWindow.on('closed', function() {
    globalWindow = null;
  });
});
<commit_msg>fix index.html path<commit_after>var app = require('app');
var BrowserWindow = require('browser-window');

var globalWindow = null;

app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  globalWindow = new BrowserWindow({
    'width': 800,
    'height': 600,
    'web-preferences': {
        'web-security': false,
        'webgl': false
    }
  });

  globalWindow.loadUrl('file://' + __dirname + '/index.html');

  globalWindow.on('closed', function() {
    globalWindow = null;
  });
});
<|endoftext|>


<commit_before>(function() {
    'use strict';

    var width = 960,
    height = 500;

    var path = d3.geo.path();
    var pathVar;
    var svg = d3.select('.chart').append('svg')
        .attr('width', width)
        .attr('height', height);

    d3.json('data/us.json', function(error, us) {

        if (error) throw error;
        var data = topojson.feature(us, us.objects.states).features;

        d3.tsv('data/us-state-names.tsv',function(stateMapping){

            var states = {};
            stateMapping.forEach(function(state){
                states[state.id] = state.name;
            });

            svg.selectAll('.state')
                .data(data)
                .enter().append('path')
                    .attr('class','state')
                    .attr('d', path)
                    .attr('class','abc')
                    .on("mouseover", function() {
                        expandAreaPath(this);
                    })
                    .on("mouseout", function() {
                        compressAreaPath(this);
                    })
                    .on('click',function(datum){
                        console.log(states[datum.id]);
                    });

            // svg.selectAll('.state-name')
            //     .data(data)
            //     .enter().append('text')
            //         .attr('class','state-name')
            //         .text(function(datum){
            //             return states[datum.id];
            //         })
            //         .attr('x',function(datum){
            //             return path.centroid(datum)[0];
            //         })
            //         .attr('y',function(datum){
            //             return path.centroid(datum)[1];
            //         })
            //         .attr('text-anchor','middle');

        });

    });

    function expandAreaPath(pathElem) {
        d3.select(pathElem).style('fill','red');
        d3.select(pathElem).style({'stroke':'black','stroke-width':'1px'})
    }

    function compressAreaPath(pathElem){
        d3.select(pathElem).style('fill','#ccc');
        d3.select(pathElem).style({'stroke':'#fff','stroke-width':'.5px'})
    }

})();<commit_msg>added tooltip<commit_after>(function() {
    'use strict';

    var width = 960,
    height = 500;

    var path = d3.geo.path();
    var pathVar;
    var svg = d3.select('.chart').append('svg')
        .attr('width', width)
        .attr('height', height);

    d3.json('data/us.json', function(error, us) {

        if (error) throw error;
        var data = topojson.feature(us, us.objects.states).features;

        d3.tsv('data/us-state-names.tsv',function(stateMapping){

            var states = {};
            stateMapping.forEach(function(state){
                states[state.id] = state.name;
            });

            svg.selectAll('.state')
                .data(data)
                .enter().append('path')
                    .attr('class','state')
                    .attr('d', path)
                    .attr('class','abc')
                    .on("mouseover", function() {
                        expandAreaPath(this);
                    })
                    .on("mouseout", function() {
                        compressAreaPath(this);
                    })
                    .on('click',function(datum){
                        console.log(states[datum.id]);
                    })
                    .append('title')
                    .text(function(datum){
                        return states[datum.id];
                    });

            // svg.selectAll('.state-name')
            //     .data(data)
            //     .enter().append('text')
            //         .attr('class','state-name')
            //         .text(function(datum){
            //             return states[datum.id];
            //         })
            //         .attr('x',function(datum){
            //             return path.centroid(datum)[0];
            //         })
            //         .attr('y',function(datum){
            //             return path.centroid(datum)[1];
            //         })
            //         .attr('text-anchor','middle');

        });

    });

    function expandAreaPath(pathElem) {
        d3.select(pathElem).style('fill','red');
        d3.select(pathElem).style({'stroke':'black','stroke-width':'1px'})
    }

    function compressAreaPath(pathElem){
        d3.select(pathElem).style('fill','#ccc');
        d3.select(pathElem).style({'stroke':'#fff','stroke-width':'.5px'})
    }

})();<|endoftext|>


<commit_before>/*The MIT License (MIT)

Copyright (c) 2014 Andrew MacKenzie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.*/

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $ */

define(function (require, exports, module) {
    "use strict";

    var AppInit                 = brackets.getModule("utils/AppInit"),
        CodeHintManager         = brackets.getModule("editor/CodeHintManager"),
        ExtensionUtils          = brackets.getModule("utils/ExtensionUtils"),
        TokenUtils              = brackets.getModule("utils/TokenUtils"),
        PreferencesManager      = brackets.getModule("preferences/PreferencesManager"),
        prefs                   = PreferencesManager.getExtensionPrefs("php-sig.php-smarthints");

    var Strings = require('strings');

    var phpBuiltins             = require("phpdata/php-predefined"),
        functionGroups          = require("text!phpdata/php-function-groups.json"),
        predefinedFunctions     = [];

    var toolbarIcon             = $(''),
        filters                 = [],
        projectUI               = require("project-ui");

    function getTokenToCursor(token) {
        var tokenStart = token.token.start,
            tokenCursor = token.pos.ch,
            tokenString = token.token.string;
        return tokenString.substr(0, (tokenCursor - tokenStart));
    }

    /**
     * @constructor
     */
    function PHPHints() {
        this.activeToken            = "";
        this.lastToken              = "";
        this.cachedPhpVariables     = [];
        this.cachedPhpConstants     = [];
        this.cachedPhpKeywords      = [];
        this.cachedPhpFunctions     = [];
        this.cachedLocalVariables   = [];
        this.tokenVariable          = /[$][\a-zA-Z_][a-zA-Z0-9_]*/g;
    }

    PHPHints.prototype.hasHints = function (editor, implicitChar) {
        this.editor = editor;
        var i               = 0,
            cursor          = editor.getCursorPos(),
            tokenToCursor   = "";

        this.activeToken = TokenUtils.getInitialContext(editor._codeMirror, cursor);

        // if implicitChar or 1 letter token is $, we *always* have hints so return immediately
        if (implicitChar === "$"  || this.activeToken.token.string.charAt(0) === "$") {
            return true;
        }

        tokenToCursor = getTokenToCursor(this.activeToken);

        // start at 2nd char unless explicit request then start immediately
        if (this.activeToken.token.string.length > 1 || implicitChar === null) {
            // do keywords first as they are common and small
            for (i = 0; i < this.cachedPhpKeywords.length; i++) {
                if (this.cachedPhpKeywords[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    return true;
                }
            }
            // do constants 2nd as they are also small
            for (i = 0; i < this.cachedPhpConstants.length; i++) {
                if (this.cachedPhpConstants[i].indexOf(tokenToCursor) === 0) {
                    return true;
                }
            }
            // do functions last as the array is quite large
            for (i = 0; i < this.cachedPhpFunctions.length; i++) {
                if (this.cachedPhpFunctions[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    return true;
                }
            }
        }
        // nope, no hints
        return false;
    };

    PHPHints.prototype.getHints = function (implicitChar) {
        var i =                 0,
            hintList =          [],
            localVarList =      [],
            phpVarList =        [],
            phpFuncList =       [],
            phpConstList =      [],
            phpKeywordList =    [],
            $fHint,
            cursor = this.editor.getCursorPos(),
            tokenToCursor = "";

        this.activeToken = TokenUtils.getInitialContext(this.editor._codeMirror, cursor);
        tokenToCursor = getTokenToCursor(this.activeToken);
        // if it's a $variable, then build the local variable list
        if (implicitChar === "$"  || this.activeToken.token.string.charAt(0) === "$") {
            if ((this.lastToken === "") || (this.activeToken.token.start !== this.lastToken.token.start) || (this.activeToken.pos.line !== this.lastToken.pos.line)) {
                this.cachedLocalVariables.length = 0;
                var varList = this.editor.document.getText().match(this.tokenVariable);
                if (varList) {
                    for (i = 0; i < varList.length; i++) {
                        var word = varList[i];
                        if (this.cachedLocalVariables.indexOf(word) === -1) {
                            this.cachedLocalVariables.push(word);
                        }
                    }
                }
            }
            this.lastToken = this.activeToken;

            if (this.cachedLocalVariables === null) {
                return null;
            }
            this.cachedLocalVariables.sort();
            // add unique local $variables
            for (i = 0; i < this.cachedLocalVariables.length; i++) {
                if (this.cachedLocalVariables[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-localvar")
                        .text(this.cachedLocalVariables[i]);
                    localVarList.push($fHint);
                }
            }
            // load the predefined $variables next
            for (i = 0; i < this.cachedPhpVariables.length; i++) {
                if (this.cachedPhpVariables[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpvar")
                        .text(this.cachedPhpVariables[i]);
                    phpVarList.push($fHint);
                }
            }
            // list is presented with local first then predefined
            hintList = localVarList.concat(phpVarList);
        } else {
            // not a $variable, could be a reserved word of some type
            // load keywords that match
            for (i = 0; i < this.cachedPhpKeywords.length; i++) {
                if (this.cachedPhpKeywords[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpkeyword")
                        .text(this.cachedPhpKeywords[i]);
                    phpKeywordList.push($fHint);
                }
            }
            // load constants that match
            for (i = 0; i < this.cachedPhpConstants.length; i++) {
                if (this.cachedPhpConstants[i].indexOf(tokenToCursor) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpconstant")
                        .text(this.cachedPhpConstants[i]);
                    phpConstList.push($fHint);
                }
            }
            // load functions that match
            for (i = 0; i < this.cachedPhpFunctions.length; i++) {
                if (this.cachedPhpFunctions[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpfunction")
                        .text(this.cachedPhpFunctions[i]);
                    phpFuncList.push($fHint);
                }
            }
            // munge all the lists together and sort
            hintList = phpKeywordList.concat(phpConstList, phpFuncList).sort();
        }

        return {
            hints: hintList,
            match: false,
            selectInitial: true,
            handleWideResults: false
        };
    };

    PHPHints.prototype.insertHint = function ($hint) {
        var cursor              = this.editor.getCursorPos(),
            currentToken        = this.editor._codeMirror.getTokenAt(cursor),
            replaceStart        = {line: cursor.line, ch: currentToken.start},
            replaceEnd          = {line: cursor.line, ch: cursor.ch};

        this.editor.document.replaceRange($hint.text(), replaceStart, replaceEnd);
        return false;
    };

    var phpHints = new PHPHints();

    function buildFunctionsList(selectedFunctions) {
        var i = 0,
            filter,
            fnArray             = [],
            fg                  = JSON.parse(functionGroups),
            fgKey,
            theFunctionList     = [];

        filters.length = 0;

        Object.keys(fg).forEach(function (key) {
            fgKey = fg[key];

            if (selectedFunctions.length > 0) {
                if (selectedFunctions.indexOf(key) > -1) {
                    fnArray = fgKey.fnNames;
                    for (i = 0; i < fnArray.length; i++) {
                        theFunctionList.push(fnArray[i]);
                    }
                    filter = { "filter": key, "filterName": fgKey.name, "checked": "checked" };
                } else {
                    filter = { "filter": key, "filterName": fgKey.name, "checked": "" };
                }
            } else {
                fnArray = fgKey.fnNames;
                for (i = 0; i < fnArray.length; i++) {
                    theFunctionList.push(fnArray[i]);
                }
                filter = { "filter": key, "filterName": fgKey.name, "checked": "checked" };
            }
            filters.push(filter);
        });
        return theFunctionList.concat(predefinedFunctions);
    }

    function createHintArray(rawList) {
        var sortedRawList   = rawList.sort(),
            i               = 0,
            currentWord     = "",
            finalList       = [];

        for (i = 0; i < sortedRawList.length; i++) {
            currentWord = sortedRawList[i];
            if (finalList.indexOf(currentWord) === -1) {
                finalList.push(currentWord);
            }
        }
        return finalList;
    }

    function handleFunctionPrefs(selectedFunctions) {
        var fnList = [];
        fnList = buildFunctionsList(selectedFunctions);
        phpHints.cachedPhpFunctions.length = 0;
        phpHints.cachedPhpFunctions = createHintArray(fnList);
    }

    function handlePhpProjectPrefs(isPhpProject) {
        if (isPhpProject) {
            toolbarIcon.addClass('active');
        } else {
            toolbarIcon.removeClass('active');
        }
    }

    AppInit.appReady(function () {
        predefinedFunctions = phpBuiltins.predefinedFunctions;
        // PHP SmartHints prefs
        prefs.definePreference("filteredFunctionList", "array", [])
            .on("change", function () {
                handleFunctionPrefs(prefs.get("filteredFunctionList", {location: { scope: "project"}}));
            });
        prefs.definePreference("isPhpProject", "boolean", false)
            .on("change", function () {
                handlePhpProjectPrefs(prefs.get("isPhpProject", {location: { scope: "project"}}));
            });
        handleFunctionPrefs(prefs.get("filteredFunctionList", {location: { scope: "project"}}));
        handlePhpProjectPrefs(prefs.get("isPhpProject", {location: { scope: "project"}}));

        // register the provider.  Priority = 10 to be the provider of choice for php
        CodeHintManager.registerHintProvider(phpHints, ["php"], 10);

        phpHints.cachedPhpKeywords = createHintArray(phpBuiltins.keywords);
        phpHints.cachedPhpConstants = createHintArray(phpBuiltins.predefinedConstants);
        phpHints.cachedPhpVariables = createHintArray(phpBuiltins.predefinedVariables);

        ExtensionUtils.loadStyleSheet(module, "css/main.css");
        toolbarIcon.appendTo('#main-toolbar .buttons')
            .on("click", function () {
                projectUI.showProjectDialog(filters);
            });
    });
});
<commit_msg>potential fix for #28<commit_after>/*The MIT License (MIT)

Copyright (c) 2014 Andrew MacKenzie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.*/

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $ */

define(function (require, exports, module) {
    "use strict";

    var AppInit                 = brackets.getModule("utils/AppInit"),
        CodeHintManager         = brackets.getModule("editor/CodeHintManager"),
        ExtensionUtils          = brackets.getModule("utils/ExtensionUtils"),
        TokenUtils              = brackets.getModule("utils/TokenUtils"),
        PreferencesManager      = brackets.getModule("preferences/PreferencesManager"),
        prefs                   = PreferencesManager.getExtensionPrefs("php-sig.php-smarthints");

    var Strings = require('strings');

    var phpBuiltins             = require("phpdata/php-predefined"),
        functionGroups          = require("text!phpdata/php-function-groups.json"),
        predefinedFunctions     = [];

    var toolbarIcon             = $(''),
        filters                 = [],
        projectUI               = require("project-ui");

    function getTokenToCursor(token) {
        var tokenStart = token.token.start,
            tokenCursor = token.pos.ch,
            tokenString = token.token.string;
        return tokenString.substr(0, (tokenCursor - tokenStart));
    }

    /**
     * @constructor
     */
    function PHPHints() {
        this.activeToken            = "";
        this.lastToken              = "";
        this.cachedPhpVariables     = [];
        this.cachedPhpConstants     = [];
        this.cachedPhpKeywords      = [];
        this.cachedPhpFunctions     = [];
        this.cachedLocalVariables   = [];
        this.tokenVariable          = /[$][\a-zA-Z_][a-zA-Z0-9_]*/g;
    }

    PHPHints.prototype.hasHints = function (editor, implicitChar) {
        this.editor = editor;
        var i               = 0,
            cursor          = editor.getCursorPos(),
            tokenToCursor   = "";

        this.activeToken = TokenUtils.getInitialContext(editor._codeMirror, cursor);

        // if implicitChar or 1 letter token is $, we *always* have hints so return immediately
        if (implicitChar === "$"  || this.activeToken.token.string.charAt(0) === "$") {
            return true;
        }

        tokenToCursor = getTokenToCursor(this.activeToken);

        // start at 2nd char unless explicit request then start immediately
        if (this.activeToken.token.string.length > 1 || implicitChar === null) {
            // do keywords first as they are common and small
            for (i = 0; i < this.cachedPhpKeywords.length; i++) {
                if (this.cachedPhpKeywords[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    return true;
                }
            }
            // do constants 2nd as they are also small
            for (i = 0; i < this.cachedPhpConstants.length; i++) {
                if (this.cachedPhpConstants[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    return true;
                }
            }
            // do functions last as the array is quite large
            for (i = 0; i < this.cachedPhpFunctions.length; i++) {
                if (this.cachedPhpFunctions[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    return true;
                }
            }
        }
        // nope, no hints
        return false;
    };

    PHPHints.prototype.getHints = function (implicitChar) {
        var i =                 0,
            hintList =          [],
            localVarList =      [],
            phpVarList =        [],
            phpFuncList =       [],
            phpConstList =      [],
            phpKeywordList =    [],
            $fHint,
            cursor = this.editor.getCursorPos(),
            tokenToCursor = "";

        this.activeToken = TokenUtils.getInitialContext(this.editor._codeMirror, cursor);
        tokenToCursor = getTokenToCursor(this.activeToken);
        // if it's a $variable, then build the local variable list
        if (implicitChar === "$"  || this.activeToken.token.string.charAt(0) === "$") {
            if ((this.lastToken === "") || (this.activeToken.token.start !== this.lastToken.token.start) || (this.activeToken.pos.line !== this.lastToken.pos.line)) {
                this.cachedLocalVariables.length = 0;
                var varList = this.editor.document.getText().match(this.tokenVariable);
                if (varList) {
                    for (i = 0; i < varList.length; i++) {
                        var word = varList[i];
                        if (this.cachedLocalVariables.indexOf(word) === -1) {
                            this.cachedLocalVariables.push(word);
                        }
                    }
                }
            }
            this.lastToken = this.activeToken;

            if (this.cachedLocalVariables === null) {
                return null;
            }
            this.cachedLocalVariables.sort();
            // add unique local $variables
            for (i = 0; i < this.cachedLocalVariables.length; i++) {
                if (this.cachedLocalVariables[i].indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-localvar")
                        .text(this.cachedLocalVariables[i]);
                    localVarList.push($fHint);
                }
            }
            // load the predefined $variables next
            for (i = 0; i < this.cachedPhpVariables.length; i++) {
                if (this.cachedPhpVariables[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpvar")
                        .text(this.cachedPhpVariables[i]);
                    phpVarList.push($fHint);
                }
            }
            // list is presented with local first then predefined
            hintList = localVarList.concat(phpVarList);
        } else {
            // not a $variable, could be a reserved word of some type
            // load keywords that match
            for (i = 0; i < this.cachedPhpKeywords.length; i++) {
                if (this.cachedPhpKeywords[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpkeyword")
                        .text(this.cachedPhpKeywords[i]);
                    phpKeywordList.push($fHint);
                }
            }
            // load constants that match
            for (i = 0; i < this.cachedPhpConstants.length; i++) {
                if (this.cachedPhpConstants[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpconstant")
                        .text(this.cachedPhpConstants[i]);
                    phpConstList.push($fHint);
                }
            }
            // load functions that match
            for (i = 0; i < this.cachedPhpFunctions.length; i++) {
                if (this.cachedPhpFunctions[i].toLowerCase().indexOf(tokenToCursor.toLowerCase()) === 0) {
                    $fHint = $("")
                        .addClass("PHPSmartHints-completion")
                        .addClass("PHPSmartHints-completion-phpfunction")
                        .text(this.cachedPhpFunctions[i]);
                    phpFuncList.push($fHint);
                }
            }
            // munge all the lists together and sort
            hintList = phpKeywordList.concat(phpConstList, phpFuncList).sort();
        }

        return {
            hints: hintList,
            match: false,
            selectInitial: true,
            handleWideResults: false
        };
    };

    PHPHints.prototype.insertHint = function ($hint) {
        var cursor              = this.editor.getCursorPos(),
            currentToken        = this.editor._codeMirror.getTokenAt(cursor),
            replaceStart        = {line: cursor.line, ch: currentToken.start},
            replaceEnd          = {line: cursor.line, ch: cursor.ch};

        this.editor.document.replaceRange($hint.text(), replaceStart, replaceEnd);
        return false;
    };

    var phpHints = new PHPHints();

    function buildFunctionsList(selectedFunctions) {
        var i = 0,
            filter,
            fnArray             = [],
            fg                  = JSON.parse(functionGroups),
            fgKey,
            theFunctionList     = [];

        filters.length = 0;

        Object.keys(fg).forEach(function (key) {
            fgKey = fg[key];

            if (selectedFunctions.length > 0) {
                if (selectedFunctions.indexOf(key) > -1) {
                    fnArray = fgKey.fnNames;
                    for (i = 0; i < fnArray.length; i++) {
                        theFunctionList.push(fnArray[i]);
                    }
                    filter = { "filter": key, "filterName": fgKey.name, "checked": "checked" };
                } else {
                    filter = { "filter": key, "filterName": fgKey.name, "checked": "" };
                }
            } else {
                fnArray = fgKey.fnNames;
                for (i = 0; i < fnArray.length; i++) {
                    theFunctionList.push(fnArray[i]);
                }
                filter = { "filter": key, "filterName": fgKey.name, "checked": "checked" };
            }
            filters.push(filter);
        });
        return theFunctionList.concat(predefinedFunctions);
    }

    function createHintArray(rawList) {
        var sortedRawList   = rawList.sort(),
            i               = 0,
            currentWord     = "",
            finalList       = [];

        for (i = 0; i < sortedRawList.length; i++) {
            currentWord = sortedRawList[i];
            if (finalList.indexOf(currentWord) === -1) {
                finalList.push(currentWord);
            }
        }
        return finalList;
    }

    function handleFunctionPrefs(selectedFunctions) {
        var fnList = [];
        fnList = buildFunctionsList(selectedFunctions);
        phpHints.cachedPhpFunctions.length = 0;
        phpHints.cachedPhpFunctions = createHintArray(fnList);
    }

    function handlePhpProjectPrefs(isPhpProject) {
        if (isPhpProject) {
            toolbarIcon.addClass('active');
        } else {
            toolbarIcon.removeClass('active');
        }
    }

    AppInit.appReady(function () {
        predefinedFunctions = phpBuiltins.predefinedFunctions;
        // PHP SmartHints prefs
        prefs.definePreference("filteredFunctionList", "array", [])
            .on("change", function () {
                handleFunctionPrefs(prefs.get("filteredFunctionList", {location: { scope: "project"}}));
            });
        prefs.definePreference("isPhpProject", "boolean", false)
            .on("change", function () {
                handlePhpProjectPrefs(prefs.get("isPhpProject", {location: { scope: "project"}}));
            });
        handleFunctionPrefs(prefs.get("filteredFunctionList", {location: { scope: "project"}}));
        handlePhpProjectPrefs(prefs.get("isPhpProject", {location: { scope: "project"}}));

        // register the provider.  Priority = 10 to be the provider of choice for php
        CodeHintManager.registerHintProvider(phpHints, ["php"], 10);

        phpHints.cachedPhpKeywords = createHintArray(phpBuiltins.keywords);
        phpHints.cachedPhpConstants = createHintArray(phpBuiltins.predefinedConstants);
        phpHints.cachedPhpVariables = createHintArray(phpBuiltins.predefinedVariables);

        ExtensionUtils.loadStyleSheet(module, "css/main.css");
        toolbarIcon.appendTo('#main-toolbar .buttons')
            .on("click", function () {
                projectUI.showProjectDialog(filters);
            });
    });
});
<|endoftext|>


<commit_before>// Version
var VERSION = '0.1-dev';

var system = require('system');
var args = system.args;
var server = require('webserver').create();
var page = require('webpage').create();
var fs = require('fs');

var usage =
  'Usage: phantomjs main.js [options]\n' +
  'Options:\n' +
  '  -h,--help            Print this usage message and exit\n' +
  '  -v,--version         Print the version number and exit\n' +
  '  -p,--port <port>     IP port on which to start the server\n' +
  '  -r,--requests <num>  Process this many requests and then exit.  -1 means \n' +
  '                       never stop.\n' +
  '  -b,--bench <page>    Use alternate bench page (default is index.html)\n' +
  '  -d,--debug           Enable verbose debug messages\n';

var port = 16000;
var requests_to_serve = -1;
var bench_page = 'index.html';
var debug = false;

// Parse command-line options.  This keeps track of which one we are on
var arg_num = 1;

// Helper function for option parsing.  Allow option/arg in any of
// these forms:
//    1: -p 1234
//    2: --po 1234
//    3: --po=1234
// Returns:
//    1 or 2: true
//    3:      '1234'
//    else:   false
function option_match(name, takes_optarg, arg) {
  var ieq = arg.indexOf('=');

  var arg_key;
  if (arg.substr(0, 2) == '--' && (takes_optarg && ieq != -1)) {  // form #3
    arg_key = arg.substring(2, ieq);
    if (name.substr(0, arg_key.length) == arg_key) {
      return arg.substr(ieq + 1);
    }
    return false;
  }

  if (arg.substr(0, 2) == '--') {
    arg_key = arg.substr(2);
  }
  else if (arg.substr(0, 1) == '-') {
    arg_key = arg.substr(1);
  }
  else {
    return false;
  }
  return name.substr(0, arg_key.length) == arg_key;
}

// This helper handles one option that takes an option-argument
function option_arg_parse(name) {
  var arg = args[arg_num];
  match = option_match(name, true, arg);
  if (!match) return false;

  if (typeof match != 'string') {
    if (arg_num + 1 < args.length) {
      arg_num++;
      match = args[arg_num];
    }
    else {
      phantom.exit(1);
    }
  }

  if (name == 'port') {
      port = match - 0;
  }
  else if (name == 'requests') { requests_to_serve = match - 0; }
  else if (name == 'bench') { bench_page = match; }

  arg_num++;
  return true;
}

var to_exit = false;
while (arg_num < args.length) {
  var arg = args[arg_num];

  if (option_match('help', false, arg)) {
    console.log(usage);
    phantom.exit(0);
    break;
  }

  if (option_match('version', false, arg)) {
    console.log('svgtex version ' + VERSION);
    phantom.exit(0);
    break;
  }

  if (option_match('debug', false, arg)) {
    debug = true;
    arg_num++;
    continue;
  }

  if (option_arg_parse('port')) {
    continue;
  }
  if (option_arg_parse('requests')) {
    continue;
  }
  if (option_arg_parse('bench')) {
    continue;
  }

  console.error("Unrecognized argument: '" + arg + "'. Use '--help' for usage info.");
  phantom.exit(1);
  break;
}

/*
  console.log(
    'port = ' + port + ", " +
    'requests_to_serve = ' + requests_to_serve + ", " +
    'bench_page = ' + bench_page + ", " +
    'debug = ' + debug + "\n"
  );
*/

// activeRequests holds information about any active MathJax requests.  It is
// a hash, with a sequential number as the key.  request_num gets incremented
// for *every* HTTP request, but only requests that get passed to MathJax have an
// entry in activeRequests.  Each element of activeRequests
// is an array of [<response object>, <start time>].
var request_num = 0;
var activeRequests = {};

// This will hold the test HTML form, which is read once, the first time it is
// requested, from test.html.
var test_form_filename = 'test.html';
var test_form = null;

var service = null;

// thanks to:
// stackoverflow.com/questions/5515869/string-length-in-bytes-in-javascript
function utf8_strlen(str) {
  var m = encodeURIComponent(str).match(/%[89ABab]/g);
  return str.length + (m ? m.length : 0);
}

// This is the callback that gets invoked after the math has been converted.
// The argument, data, is an array that holds the three arguments from the
// process() function in engine.js:  the request number, the original
// text, and either the converted svg, or an array of one element
// that holds an error message.
page.onCallback = function(data) {
  var num = data[0],
      src = data[1],
      svg_or_error = data[2],
      record = activeRequests[num],
      resp = record[0],
      start_time = record[1],
      duration = (new Date()).getTime() - start_time,
      duration_msg = ', took ' + duration + 'ms.';

  if ((typeof svg_or_error) === 'string') {
    resp.statusCode = 200;
    resp.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
    resp.setHeader("Content-Length", utf8_strlen(svg_or_error));
    resp.write(svg_or_error);
    console.log(num + ': ' + src.substr(0, 30) + '.. ' +
        src.length + 'B query, OK ' + svg_or_error.length + 'B result' +
        duration_msg);
  }
  else {
    resp.statusCode = 400;    // bad request
    resp.write(svg_or_error[0]);
    console.log(num, src.substr(0, 30) + '.. ' +
        src.length + 'B query, error: ' + svg_or_error[0] + duration_msg);
  }
  resp.close();

  delete(activeRequests[num]);

  if (!(--requests_to_serve)) {
    phantom.exit();
  }
}

// Parse the request and return an object with the parsed values.
// It will either have an error indication, e.g.
//   { num: 5, status_code: 400, error: "message" }
// Or indicate that the test form should be returned, e.g.
//   { num: 5, test_form: 1 }
// or a valid request, e.g.
//   { num: 5, type: 'tex', q: 'n^2', width: '500' }

function parse_request(req) {
  // Set any defaults here:
  var query = {
    num: request_num++,
    type: 'tex',
    width: null
  };

  if (debug) {
    if (req.method == 'POST') {
      console.log("  req.postRaw = '" + req.postRaw + "'");
    }
    else {
      console.log("  req.url = '" + req.url + "'");
    }
  }

  var qs;   // will store the content of the (tex or mml) math
  if (req.method == 'GET') {
    var url = req.url;

    if (url == '' || url == '/') {
      // User has requested the test form
      if (test_form == null && fs.isReadable(test_form_filename)) {
        test_form = fs.read(test_form_filename);  // set the global variable
      }
      if (test_form != null) {
        query.test_form = 1;
      }
      else {
        query.status_code = 500;  // Internal server error
        query.error = "Can't find test form";
      }
      return query;
    }

    var iq = url.indexOf("?");
    if (iq == -1) {  // no query string
      query.status_code = 400;  // bad request
      query.error = "Missing query string";
      return query;
    }

    qs = url.substr(iq+1);
  }

  else if (req.method == 'POST') {
    if (typeof req.postRaw !== 'string') {   // which can happen
      query.status_code = 400;  // bad request
      query.error = "Missing post content";
      return query;
    }
    qs = req.postRaw;
  }

  else {  // method is not GET or POST
    query.status_code = 400;  // bad request
    query.error = "Method " + req.method + " not supported";
    return query;
  }

  var param_strings = qs.split(/&/);
  var num_param_strings = param_strings.length;

  for (var i = 0; i < num_param_strings; ++i) {
    var ps = param_strings[i];
    var ie = ps.indexOf('=');
    if (ie == -1) {
      query.status_code = 400;  // bad request
      query.error = "Can't decipher request parameter";
      return query;
    }
    var key = ps.substr(0, ie);
    var val = decodeURIComponent(ps.substr(ie+1).replace(/\+/g, ' '));
    if (key == 'type') {
      query.type = val;
    }
    else if (key == 'q') {
      query.q = val;
    }
    else if (key == 'width') {
      query.width = parseInt(val) || null;
    }
    else {
      query.status_code = 400;  // bad request
      query.error = "Unrecognized parameter name: " + key;
      return query;
    }
  }
  if (!query.q) {   // no source math
    query.status_code = 400;  // bad request
    query.error = "No source math detected in input";
    return query;
  }

  return query;
}

function listenLoop() {
  // Set up the listener that will respond to every new request
  service = server.listen('0.0.0.0:' + port, function(req, resp) {
    var query = parse_request(req);
    var request_num = query.num;
    console.log(request_num + ': ' + "received: " + req.method + " " +
        req.url.substr(0, 30) + " ..");

    if (query.test_form) {
      console.log(request_num + ": returning test form");
      resp.write(test_form);
      resp.close();
    }
    else {
      if (query.error) {
        console.log(request_num + ": error: " + query.error);
        resp.statusCode = query.status_code;
        resp.write(query.error);
        resp.close();
      }

      else {
        // The following evaluates the function argument in the page's context,
        // with query -> _query. That, in turn, calls the process() function in
        // engine.js, which causes MathJax to render the math.  The callback is
        // PhantomJS's callPhantom() function, which in turn calls page.onCallback(),
        // above.  This just queues up the call, and will return at once.
        activeRequests[request_num] = [resp, (new Date()).getTime()];
        page.evaluate(function(_query) {
          window.engine.process(_query, window.callPhantom);
        }, query);
      }
    }
  });

  if (!service) {
    console.log("server failed to start on port " + port);
    phantom.exit(1);
  }
  else {
    console.log("Server started on port " + port);
    console.log("Point your brownser at http://localhost:" + port + " for a test form.");
  }
}

console.log("Loading bench page " + bench_page);
page.open(bench_page, listenLoop);

/* These includeJs calls would allow us to specify the MathJax location as a
   command-line parameter, but then you'd have to take the <script> tags out of
   index.html, and we'd lose the ability to debug by loading that in a browser
   directly.
page.open('index.html', function (status) {
  page.includeJs('mathjax/MathJax.js?config=TeX-AMS-MML_SVG', function() {
    page.includeJs('engine.js', listenLoop);
  });
});
*/

<commit_msg>adds support for JSON posts<commit_after>// Version
var VERSION = '0.1-dev';

var system = require('system');
var args = system.args;
var server = require('webserver').create();
var page = require('webpage').create();
var fs = require('fs');

var usage =
  'Usage: phantomjs main.js [options]\n' +
  'Options:\n' +
  '  -h,--help            Print this usage message and exit\n' +
  '  -v,--version         Print the version number and exit\n' +
  '  -p,--port <port>     IP port on which to start the server\n' +
  '  -r,--requests <num>  Process this many requests and then exit.  -1 means \n' +
  '                       never stop.\n' +
  '  -b,--bench <page>    Use alternate bench page (default is index.html)\n' +
  '  -d,--debug           Enable verbose debug messages\n';

var port = 16000;
var requests_to_serve = -1;
var bench_page = 'index.html';
var debug = false;

// Parse command-line options.  This keeps track of which one we are on
var arg_num = 1;

// Helper function for option parsing.  Allow option/arg in any of
// these forms:
//    1: -p 1234
//    2: --po 1234
//    3: --po=1234
// Returns:
//    1 or 2: true
//    3:      '1234'
//    else:   false
function option_match(name, takes_optarg, arg) {
  var ieq = arg.indexOf('=');

  var arg_key;
  if (arg.substr(0, 2) == '--' && (takes_optarg && ieq != -1)) {  // form #3
    arg_key = arg.substring(2, ieq);
    if (name.substr(0, arg_key.length) == arg_key) {
      return arg.substr(ieq + 1);
    }
    return false;
  }

  if (arg.substr(0, 2) == '--') {
    arg_key = arg.substr(2);
  }
  else if (arg.substr(0, 1) == '-') {
    arg_key = arg.substr(1);
  }
  else {
    return false;
  }
  return name.substr(0, arg_key.length) == arg_key;
}

// This helper handles one option that takes an option-argument
function option_arg_parse(name) {
  var arg = args[arg_num];
  match = option_match(name, true, arg);
  if (!match) return false;

  if (typeof match != 'string') {
    if (arg_num + 1 < args.length) {
      arg_num++;
      match = args[arg_num];
    }
    else {
      phantom.exit(1);
    }
  }

  if (name == 'port') {
      port = match - 0;
  }
  else if (name == 'requests') { requests_to_serve = match - 0; }
  else if (name == 'bench') { bench_page = match; }

  arg_num++;
  return true;
}

var to_exit = false;
while (arg_num < args.length) {
  var arg = args[arg_num];

  if (option_match('help', false, arg)) {
    console.log(usage);
    phantom.exit(0);
    break;
  }

  if (option_match('version', false, arg)) {
    console.log('svgtex version ' + VERSION);
    phantom.exit(0);
    break;
  }

  if (option_match('debug', false, arg)) {
    debug = true;
    arg_num++;
    continue;
  }

  if (option_arg_parse('port')) {
    continue;
  }
  if (option_arg_parse('requests')) {
    continue;
  }
  if (option_arg_parse('bench')) {
    continue;
  }

  console.error("Unrecognized argument: '" + arg + "'. Use '--help' for usage info.");
  phantom.exit(1);
  break;
}

/*
  console.log(
    'port = ' + port + ", " +
    'requests_to_serve = ' + requests_to_serve + ", " +
    'bench_page = ' + bench_page + ", " +
    'debug = ' + debug + "\n"
  );
*/

// activeRequests holds information about any active MathJax requests.  It is
// a hash, with a sequential number as the key.  request_num gets incremented
// for *every* HTTP request, but only requests that get passed to MathJax have an
// entry in activeRequests.  Each element of activeRequests
// is an array of [<response object>, <start time>].
var request_num = 0;
var activeRequests = {};

// This will hold the test HTML form, which is read once, the first time it is
// requested, from test.html.
var test_form_filename = 'test.html';
var test_form = null;

var service = null;

// thanks to:
// stackoverflow.com/questions/5515869/string-length-in-bytes-in-javascript
function utf8_strlen(str) {
  var m = encodeURIComponent(str).match(/%[89ABab]/g);
  return str.length + (m ? m.length : 0);
}

// This is the callback that gets invoked after the math has been converted.
// The argument, data, is an array that holds the three arguments from the
// process() function in engine.js:  the request number, the original
// text, and either the converted svg, or an array of one element
// that holds an error message.
page.onCallback = function(data) {
  var num = data[0],
      src = data[1],
      svg_or_error = data[2],
      record = activeRequests[num],
      resp = record[0],
      start_time = record[1],
      duration = (new Date()).getTime() - start_time,
      duration_msg = ', took ' + duration + 'ms.';

  if ((typeof svg_or_error) === 'string') {
    resp.statusCode = 200;
    resp.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
    resp.setHeader("Content-Length", utf8_strlen(svg_or_error));
    resp.write(svg_or_error);
    console.log(num + ': ' + src.substr(0, 30) + '.. ' +
        src.length + 'B query, OK ' + svg_or_error.length + 'B result' +
        duration_msg);
  }
  else {
    resp.statusCode = 400;    // bad request
    resp.write(svg_or_error[0]);
    console.log(num, src.substr(0, 30) + '.. ' +
        src.length + 'B query, error: ' + svg_or_error[0] + duration_msg);
  }
  resp.close();

  delete(activeRequests[num]);

  if (!(--requests_to_serve)) {
    phantom.exit();
  }
};

function isJson(data) {
    var str = JSON.stringify(data);
    try {
        JSON.parse(str);
    } catch (e) {
        return false;
    }
    return true;
}

// Parse the request and return an object with the parsed values.
// It will either have an error indication, e.g.
//   { num: 5, status_code: 400, error: "message" }
// Or indicate that the test form should be returned, e.g.
//   { num: 5, test_form: 1 }
// or a valid request, e.g.
//   { num: 5, type: 'tex', q: 'n^2', width: '500' }

function parse_request(req) {
  // Set any defaults here:
  var query = {
    num: request_num++,
    type: 'tex',
    width: null
  };

  if (debug) {
    if (req.method == 'POST') {
      console.log("  req.postRaw = '" + req.postRaw + "'");
    }
    else {
      console.log("  req.url = '" + req.url + "'");
    }
  }

  var qs;   // will store the content of the (tex or mml) math
  if (req.method == 'GET') {
    var url = req.url;

    if (url == '' || url == '/') {
      // User has requested the test form
      if (test_form == null && fs.isReadable(test_form_filename)) {
        test_form = fs.read(test_form_filename);  // set the global variable
      }
      if (test_form != null) {
        query.test_form = 1;
      }
      else {
        query.status_code = 500;  // Internal server error
        query.error = "Can't find test form";
      }
      return query;
    }

    var iq = url.indexOf("?");
    if (iq == -1) {  // no query string
      query.status_code = 400;  // bad request
      query.error = "Missing query string";
      return query;
    }

    qs = url.substr(iq+1);
  }

  else if (req.method == 'POST') {
    if (isJson(req.post)) {
      var data = JSON.parse(req.post);
      query.type = data.type;
      query.width = data.width;
      query.status_code = 200;
      query.q = data.expression;
      return query;
    }
    if (typeof req.postRaw !== 'string') {   // which can happen
      query.status_code = 400;  // bad request
      query.error = "Missing post content";
      return query;
    }
    qs = req.postRaw;
  }

  else {  // method is not GET or POST
    query.status_code = 400;  // bad request
    query.error = "Method " + req.method + " not supported";
    return query;
  }

  var param_strings = qs.split(/&/);
  var num_param_strings = param_strings.length;

  for (var i = 0; i < num_param_strings; ++i) {
    var ps = param_strings[i];
    var ie = ps.indexOf('=');
    if (ie == -1) {
      query.status_code = 400;  // bad request
      query.error = "Can't decipher request parameter";
      return query;
    }
    var key = ps.substr(0, ie);
    var val = decodeURIComponent(ps.substr(ie+1).replace(/\+/g, ' '));
    if (key == 'type') {
      query.type = val;
    }
    else if (key == 'q') {
      query.q = val;
    }
    else if (key == 'width') {
      query.width = parseInt(val) || null;
    }
    else {
      query.status_code = 400;  // bad request
      query.error = "Unrecognized parameter name: " + key;
      return query;
    }
  }
  if (!query.q) {   // no source math
    query.status_code = 400;  // bad request
    query.error = "No source math detected in input";
    return query;
  }

  return query;
}

function listenLoop() {
  // Set up the listener that will respond to every new request
  service = server.listen('0.0.0.0:' + port, function(req, resp) {
    var query = parse_request(req);
    var request_num = query.num;
    console.log(request_num + ': ' + "received: " + req.method + " " +
        req.url.substr(0, 30) + " ..");

    if (query.test_form) {
      console.log(request_num + ": returning test form");
      resp.write(test_form);
      resp.close();
    }
    else {
      if (query.error) {
        console.log(request_num + ": error: " + query.error);
        resp.statusCode = query.status_code;
        resp.write(query.error);
        resp.close();
      }

      else {
        // The following evaluates the function argument in the page's context,
        // with query -> _query. That, in turn, calls the process() function in
        // engine.js, which causes MathJax to render the math.  The callback is
        // PhantomJS's callPhantom() function, which in turn calls page.onCallback(),
        // above.  This just queues up the call, and will return at once.
        activeRequests[request_num] = [resp, (new Date()).getTime()];
        page.evaluate(function(_query) {
          window.engine.process(_query, window.callPhantom);
        }, query);
      }
    }
  });

  if (!service) {
    console.log("server failed to start on port " + port);
    phantom.exit(1);
  }
  else {
    console.log("Server started on port " + port);
    console.log("Point your brownser at http://localhost:" + port + " for a test form.");
  }
}

console.log("Loading bench page " + bench_page);
page.open(bench_page, listenLoop);

/* These includeJs calls would allow us to specify the MathJax location as a
   command-line parameter, but then you'd have to take the <script> tags out of
   index.html, and we'd lose the ability to debug by loading that in a browser
   directly.
page.open('index.html', function (status) {
  page.includeJs('mathjax/MathJax.js?config=TeX-AMS-MML_SVG', function() {
    page.includeJs('engine.js', listenLoop);
  });
});
*/

<|endoftext|>