DScript / PostAge

// ==UserScript==
// @name         PostAge
// @namespace    DScript
// @version      0.2.9
// @description  Unify online publication dates; draw attention to old articles
// @author       DScript
// @match        https://www.svt.se/*
// @match        https://www.dn.se/*
// @match        https://www.aftonbladet.se/*
// @match        https://www.expressen.se/*
// @match        https://www.svd.se/*
// @match        https://www.metro.se/*
// @match        https://www.di.se/*
// @match        https://nyheter24.se/*
// @match        https://newsvoice.se/*
// @match        https://www.theguardian.com/*
// @match        http://www.dailymail.co.uk/*
// @match        https://www.huffingtonpost.com/*
// @match        https://*.wordpress.com/*
// @match        http://scienceblogs.com/*
// @match        http://www.gp.se/*
// @match        http://www.hn.se/*
// @match        http://www.ttela.se/*
// @match        http://www.bohuslaningen.se/*
// @match        http://www.stromstadstidning.se/*
// @match        http://www.hallandsposten.se/*
// @match        https://www.sydsvenskan.se/*
// @match        https://www.hd.se/*
// @match        https://www.etc.se/*
// @match        http://www.bbc.com/*
// @match        http://www.bbc.co.uk/*
// @match        https://www.washingtonpost.com/*
// @match        https://www.nytimes.com/*
// @match        http://*.cnn.com/*
// @match        http://www.foxnews.com/*
// @match        https://www.nbcnews.com/*
// @match        https://www.wsj.com/*
// @match        http://www.latimes.com/*
// @match        https://www.today.com/*
// @match        http://abcnews.go.com/*
// @match        http://www.dt.se/*
// @grant        none
// @require      http://code.jquery.com/jquery-2.2.3.min.js
// @updateURL    https://openuserjs.org/meta/DScript/PostAge.meta.js
// ==/UserScript==

/*
  TODO:
  - Stampen: scrolling into new articles
  - Yahoo news doesn't update on navigation
  - Sydsvenskan: not ideal when scrolling into new articles. also some cases of navigation not detected
  - SR: currently commented out. needs to update when navigating
    // @match        http://sverigesradio.se/*
*/

(function() {
    'use strict';

    function debounce(fn, delay) {
        var timer = null;
        return function () {
            var context = this, args = arguments;
            clearTimeout(timer);
            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        };
    }

    // shorthand for handlesUrl that simply checks if the url contains a substring
    function urlContains() {
        var validUrls = Array.prototype.slice.call(arguments, 0);
        return (url) => validUrls.some(u => url.toLowerCase().indexOf(u) >= 0);
    }

    var postAge = {
        debug: false,
        colors: {
            recent: { color: '#3c763d', 'background-color': '#dff0d8', 'border-color': '#d6e9c6' },
            moderate: { color: '#8a6d3b', 'background-color': '#fcf8e3', 'border-color': '#faebcc' },
            old: { color: '#a94442', 'background-color': '#f2dede', 'border-color': '#ebccd1' }
        },
        parsers: [],
        log: function(l) {
            if(!this.debug) return;
            console.log.apply(null, arguments);
        },
        addParser: function(name,parser) {
            if(!parser) return;
            parser.name = name;
            this.parsers.push(parser);
        },
        findParser: function(url) {
            return this.parsers.find(function(p) {
                try {
                   postAge.log(p);
                   var h = p.handlesUrl(url);
                   postAge.log('Handled by ' + p.name + '? ' + (h ? 'YES' : 'NO'));
                   return h;
                } catch(ex) {
                   postAge.log('ERROR testing ' + p.name, + ex.message);
                   return false;
                }
            });
        },
        formatDate: function(dt) {
            return [dt.getFullYear(), ('0' + (dt.getMonth()+1)).slice(-2), ('0' + dt.getDate()).slice(-2)].join('-');
        },
        showDates: function(dates) {
            if(typeof dates !== 'object') { $('.postAgePopover').hide(); return; };
            dates = dates.filter(d => d.date instanceof Date && isFinite(d.date));
            if (dates.length == 0)  { $('.postAgePopover').hide(); return; };

            var popover = $('.postAgePopover');
            if(popover.length === 0)
                popover = $('<div/>', { 'class': 'postAgePopover' })
                    .css({ position: 'fixed', top: 50, left: 10, padding: 15, 'border-radius': 15, 'border-style': 'solid', 'border-width': 1, 'z-index': 1000000071 })
                    .appendTo(document.body);

            popover.empty();

            var minDate = new Date();
            dates.forEach(function(dt) {
                var item = $('<div/>').appendTo(popover);
                $('<span/>', { 'class': 'title', text: dt.title })
                    .css({ 'font-family': 'verdana', 'font-size': 11, 'font-weight': 'bold', display: 'inline-block', width: 100 })
                    .appendTo(item);

                $('<span/>', { 'class': 'date', text: postAge.formatDate(dt.date) })
                    .css({ 'font-family': 'verdana', 'font-size': 11 })
                    .appendTo(item);

                if(dt.date < minDate) minDate = dt.date;
            });

            var age = (new Date() - minDate) / (1000*60*60);
            var color = this.colors.old;
            if(age < (24*182)) color = this.colors.moderate;
            if(age < (24*7)) color = this.colors.recent;
            popover.css(color);

            postAge.log('Age: ' + age + ' (' + (typeof age) + ')');

            if(age > 24*365) {
                age = (age/24/365).toFixed(1) + 'y';
            } else if(age > 24*30) {
                age = Math.floor(age/24/30) + 'm';
                postAge.log(age);
            } else if(age > 24) {
                age = Math.floor(age/24) + 'd';
                postAge.log(age);
            } else {
                age = Math.floor(age) + 'h';
                postAge.log(age);
            }

            var h2 = $('<h2/>', { text: age })
                .css({ 'font-family': 'georgia', 'font-size': 30, 'font-weight': 'bold', 'text-align': 'center', margin: 5, color: color.color })
                .insertBefore(popover.find('div:first'));

            $('<button/>', { html: '&times', 'type': 'button' })
                .click(function() { popover.hide(); }).css(color)
                .css({ 'float': 'right', 'border-style': 'solid', 'border-width': 1, 'border-radius': '50%', 'border-color': color.color })
                .insertBefore(h2);
        },
        exec: function() {
            var parser = this.findParser(location.href);
            if(typeof parser !== 'object') return;
            try {
               var out = parser.run();
               this.log('Data from ' + parser.name + ':');
               this.log(out);
               this.showDates(out);
            } catch(ex) {
                postAge.log('ERROR executing ' + parser.name, ex);
            }
        },
        monitoring: false,
        monitorDom: function($node, key) {
            if($node.length==0) return;
            postAge.log('monitor: ' + $node.length);
            var mo = window.MutationObserver || window.WebKitMutationObserver;
            var ctx = this;
            var observer = new mo(debounce(function(mutations, observer) {
                ctx.log('domchange', key||'');
                ctx.exec();
            }, 500));
            observer.observe($node[0], { childList: true, subtree: true });
            postAge.monitoring = true;
            postAge.log(observer);
        }
    };

    postAge.addParser('SVT Nyheter', {
        handlesUrl: urlContains('www.svt.se'),
        run: function() {
            if(!postAge.monitoring) {
                postAge.monitorDom($('.nyh_body'));
            }

            var fixTime = (time) => {
                var dt = time.attr('datetime').replace(/(\d+)\.(\d+)$/, '$1:$2');
                var months = {januari:'jan',februari:'feb',mars:'mar',april:'apr',maj:'may',juni:'jun',juli:'jul',augusti:'aug',oktober:'oct',november:'nov',december:'dec'};
                Object.keys(months).forEach(k => dt = dt.replace(k,months[k]));
                return new Date(dt);
            };

            return $('.nyh_article__date').map(function() {
                return {
                    title: $('span',this).text().replace(':',''),
                    date: fixTime($('time',this))
                };
            }).toArray();
        }
    });

    postAge.addParser('DN', {
        handlesUrl: urlContains('www.dn.se'),
        run: function() {
            var parser = this;
            return $('.article__byline:first meta').map(function() {
                return {
                    title: parser.resolveTitle($(this).attr('itemprop')),
                    date: new Date($(this).attr('content'))
                };
            }).toArray();
        },
        resolveTitle: function(itemProp) {
            switch(itemProp) {
                case 'dateModified': return 'Edited';
                case 'datePublished': return 'Published';
                default: return '';
            }
        }
    });

    postAge.addParser('Aftonbladet', {
        handlesUrl:  urlContains('www.aftonbladet.se'),
        run: function() {
            var dates = this.getFromAbSe();
            if(dates.length == 0)
                dates = this.getFromTimestamp();
            return dates;
        },
        getFromAbSe: function() {
            var dates = [];

            if(typeof ABse !== 'object' || !('page' in ABse))
                return dates;

            if('articlePublishedDateTime' in ABse.page)
                dates.push({ title: 'Published', date: new Date(ABse.page.articlePublishedDateTime) });
            if('articleLastModifiedDateTime' in ABse.page)
                dates.push({ title: 'Edited', date: new Date(ABse.page.articleLastModifiedDateTime) });
            return dates;
        },
        getFromTimestamp: function() {
            return [{ title: 'Published', date: new Date($('.abArticle .abTimestamp:first time').attr('datetime')) }];
        }
    });

    postAge.addParser('Expressen', {
        handlesUrl: function(url) {
            return typeof Ariel === 'object'
               && 'pagedata' in Ariel
               && 'trackingInfo' in Ariel.pagedata
               && 'burt' in Ariel.pagedata.trackingInfo
               && 'desktop' in Ariel.pagedata.trackingInfo.burt
               && 'annotations' in Ariel.pagedata.trackingInfo.burt.desktop;
        },
        run: function() {
            var ann = Ariel.pagedata.trackingInfo.burt.desktop.annotations;
            var dt = ann.filter(function(a) { return a[1] == 'published_date'; })[0];
            if(!!dt) return [{ title: 'Published', date: new Date(dt[2]) }];
            return [];
        }
    });

    postAge.addParser('SvD', {
        handlesUrl: urlContains('www.svd.se'),
        run: () => [{ title: 'Published', date: new Date($('meta[name="publishdate"]').attr('content')) }]
    });

    postAge.addParser('OpenGraph websites', {
        handlesUrl: (url) => $('meta[property="article:published_time"]').length > 0,
        run: () => [
            { title: 'Published', date: new Date($('meta[property="article:published_time"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[property="article:modified_time"]').attr('content')) }
        ]
    });

    postAge.addParser('Stampen', {
        handlesUrl: urlContains('www.gp.se','www.hn.se','www.ttela.se','www.stromstadstidning.se','www.hallandsposten.se','www.bohuslaningen.se'),
        run: function() {
            if(!postAge.monitoring) {
                postAge.monitorDom($('.nyh_body'));
            }

            var fixTime = (time) => {
                var dt = time.attr('datetime');
                var tmatch = $('.article__meta__published time').text().match(/\d+:\d+/);
                return new Date([dt, tmatch.length > 0 ? tmatch[0] : '00:00'].join(' '));
            };

            return [
                { title: 'Published', date: fixTime($('.article__meta__published time')) }
            ];
        }
    });

    postAge.addParser('Sydsvenskan / HD', {
        handlesUrl: urlContains('sydsvenskan.se','www.hd.se'),
        run: () => 'hdsconfig' in window && 'published' in window.hdsconfig ? [{ title: 'Published', date: new Date(window.hdsconfig.published) }] : null
    });

    postAge.addParser('Dagens ETC', {
        handlesUrl: urlContains('www.etc.se'),
        run: () => [{ title: 'Published', date: new Date($('meta[name="dcterms.date"]').attr('content'))}]
    });

    postAge.addParser('BBC', {
        handlesUrl: (url) => urlContains('www.bbc.co')(url) && $('script[type="application/ld+json"]').length > 0,
        run: function() {
            var data = JSON.parse($('script[type="application/ld+json"]').text().trim());
            return [{ title: 'Published', date: new Date(data.datePublished) }];
        }
    });

    postAge.addParser('Washington Post', {
        handlesUrl: urlContains('www.washingtonpost.com'),
        run: function() {
            var date = new Date($('.pb-timestamp[itemprop="datePublished"]').attr('content').replace(/\-500$/,'-0500'));
            return [{ title: 'Published', date: date }];
        }
    });

    postAge.addParser('NY Times', {
        handlesUrl: urlContains('www.nytimes.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[property="article:published"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[property="article:modified"]').attr('content')) }
        ]
    });

    postAge.addParser('Yahoo News', {
        handlesUrl: urlContains('.yahoo.com'),
        run: () => [{ title: 'Published', date: new Date($('.auth-attr time').attr('datetime')) }]
    });

    postAge.addParser('CNN', {
        handlesUrl: urlContains('.cnn.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[property="og:pubdate"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[name="lastmod"]').attr('content')) }
        ]
    });

    postAge.addParser('Fox News', {
        handlesUrl: urlContains('www.foxnews.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[name="dcterms.created"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[name="dcterms.modified"]').attr('content')) }
        ]
    });

    postAge.addParser('NBC News', {
        handlesUrl: urlContains('www.nbcnews.com'),
        run: () => [ { title: 'Published', date: new Date($('.timestamp_article[itemprop="dateModified"]').attr('datetime')) }]
    });

    postAge.addParser('Wall Street Journal', {
        handlesUrl: urlContains('.wsj.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[name="article.published"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[name="article.updated"]').attr('content')) }
        ]
    });

    postAge.addParser('LA Times', {
        handlesUrl: urlContains('www.latimes.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[itemprop="datePublished"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[itemprop="dateModified"]').attr('content')) }
        ]
    });

    postAge.addParser('Today.com', {
        handlesUrl: urlContains('www.today.com'),
        run: () => [{ title: 'Published', date: new Date(analyticsDataLayer.publishDate) }]
    });

    postAge.addParser('Dalarnas Tidningar', {
        handlesUrl: () => 'PageObject' in window && 'metadata' in window.PageObject && 'pub_date' in window.PageObject.metadata,
        run: () => [{ title: 'Published', date: new Date(window.PageObject.metadata.pub_date) }]
    });

    postAge.addParser('ABC News', {
        handlesUrl: () => urlContains('abcnews.go.com'),
        run: () => [
            { title: 'Published', date: new Date($('meta[name="DC.date.issued"]').attr('content')) },
            { title: 'Edited', date: new Date($('meta[name="Last-Modified"]').attr('content')) }
        ]
    });

   /* postAge.addParser('Sveriges Radio', {
        handlesUrl: (url) => url.toLowerCase().indexOf('sverigesradio.se') >= 0,
        run: function() {
            if(!postAge.monitoring) {
                postAge.monitorDom($('body'), 'body');
                postAge.monitorDom($('.sr-page__wrapper'), 'sr-page__wrapper');
            }

            postAge.log('Exec');

            var meta = $('meta[name="displaydate"]');
            var dates = [];
            if(meta.length > 0) {
               var dstr = meta.attr('content');
               dates.push({ title: 'Published', date: new Date(dstr.slice(0,4), dstr.slice(4,6), dstr.slice(6,8), 0, 0, 0, 0)});
            } else {
                // fallback
                var dateMatch = $('.article-details__published').text().trim().match(/Publicerat (.+) kl (\d+)\.(\d+)/);
                if(dateMatch) {
                    var months = {januari:'jan',februari:'feb',mars:'mar',april:'apr',maj:'may',juni:'jun',juli:'jul',augusti:'aug',oktober:'oct',november:'nov',december:'dec'};
                    Object.keys(months).forEach(k => dateMatch[1] = dateMatch[1].replace(k,months[k]));
                    postAge.log(match);
                    dates.push({ title: 'Published', date: new Date(match[1] + ' ' + match[2] + ':' + match[3]) });
                }
            }
            return dates;
        }
    });*/

    $(function(){
       postAge.exec();
    });

})();