<commit_before>const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const moment = require('moment');

const messages = {
    stripeNotConnected: 'Missing Stripe connection.',
    memberAlreadyExists: 'Member already exists.'
};

/**
 * @typedef {object} ILabsService
 * @prop {(key: string) => boolean} isSet
 */

/**
 * @typedef {object} IEmailService
 * @prop {(data: {email: string, requestedType: string}) => Promise<any>} sendEmailWithMagicLink
 */

/**
 * @typedef {object} IStripeService
 * @prop {boolean} configured
 */

module.exports = class MemberBREADService {
    /**
     * @param {object} deps
     * @param {import('../repositories/member')} deps.memberRepository
     * @param {import('@tryghost/members-offers/lib/application/OffersAPI')} deps.offersAPI
     * @param {ILabsService} deps.labsService
     * @param {IEmailService} deps.emailService
     * @param {IStripeService} deps.stripeService
     */
    constructor({memberRepository, labsService, emailService, stripeService, offersAPI}) {
        this.offersAPI = offersAPI;
        /** @private */
        this.memberRepository = memberRepository;
        /** @private */
        this.labsService = labsService;
        /** @private */
        this.emailService = emailService;
        /** @private */
        this.stripeService = stripeService;
    }

    /**
     * @private
     */
    attachSubscriptionsToMember(member) {
        if (!member.products || !Array.isArray(member.products)) {
            return member;
        }

        const subscriptionProducts = (member.subscriptions || []).map(sub => sub.price.product.product_id);
        for (const product of member.products) {
            if (!subscriptionProducts.includes(product.id)) {
                const productAddEvent = member.productEvents.find(event => event.product_id === product.id);
                let startDate;
                if (!productAddEvent || productAddEvent.action !== 'added') {
                    startDate = moment();
                } else {
                    startDate = moment(productAddEvent.created_at);
                }
                member.subscriptions.push({
                    id: '',
                    tier: product,
                    customer: {
                        id: '',
                        name: member.name,
                        email: member.email
                    },
                    plan: {
                        id: '',
                        nickname: 'Complimentary',
                        interval: 'year',
                        currency: 'USD',
                        amount: 0
                    },
                    status: 'active',
                    start_date: startDate,
                    default_payment_card_last4: '****',
                    cancel_at_period_end: false,
                    cancellation_reason: null,
                    current_period_end: moment(startDate).add(1, 'year'),
                    price: {
                        id: '',
                        price_id: '',
                        nickname: 'Complimentary',
                        amount: 0,
                        interval: 'year',
                        type: 'recurring',
                        currency: 'USD',
                        product: {
                            id: '',
                            product_id: product.id
                        }
                    }
                });
            }
        }

        for (const subscription of member.subscriptions) {
            if (!subscription.tier) {
                subscription.tier = member.products.find(product => product.id === subscription.price.product.product_id);
            }
        }
    }

    /**
     * @private
     */
    attachOffersToSubscriptions(member, subscriptionOffers) {
        member.subscriptions = member.subscriptions.map((subscription) => {
            const offer = subscriptionOffers[subscription.id];
            if (offer) {
                subscription.offer = offer;
            } else {
                subscription.offer = null;
            }
            return subscription;
        });
    }

    async read(data, options = {}) {
        const defaultWithRelated = [
            'labels',
            'stripeSubscriptions',
            'stripeSubscriptions.customer',
            'stripeSubscriptions.stripePrice',
            'stripeSubscriptions.stripePrice.stripeProduct',
            'products',
            'offerRedemptions'
        ];

        const withRelated = new Set((options.withRelated || []).concat(defaultWithRelated));

        if (!withRelated.has('productEvents')) {
            withRelated.add('productEvents');
        }

        if (withRelated.has('email_recipients')) {
            withRelated.add('email_recipients.email');
        }

        const model = await this.memberRepository.get(data, {
            ...options,
            withRelated: Array.from(withRelated)
        });

        if (!model) {
            return null;
        }

        const subscriptionOffers = await model.related('offerRedemptions').toJSON().reduce(async (promiseObj, offerRedemption) => {
            const obj = await promiseObj;
            const offer = await this.offersAPI.getOffer({id: offerRedemption.offer_id});
            return {
                ...obj,
                [offerRedemption.subscription_id]: offer
            };
        }, Promise.resolve({}));

        model.related('stripeSubscriptions').forEach((subscriptionModel) => {
            const offer = subscriptionOffers[subscriptionModel.id];
            if (!offer) {
                return;
            }

            if (offer.cadence !== subscriptionModel.related('stripePrice').get('interval')) {
                return;
            }

            if (offer.tier.id !== subscriptionModel.related('stripePrice').related('stripeProduct').get('product_id')) {
                return;
            }

            subscriptionOffers[subscriptionModel.get('subscription_id')] = offer;
        });

        const member = model.toJSON(options);

        member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
        this.attachSubscriptionsToMember(member);

        this.attachOffersToSubscriptions(member, subscriptionOffers);

        return member;
    }

    async add(data, options) {
        if (!this.labsService.isSet('multipleProducts')) {
            delete data.products;
        }

        if (!this.stripeService.configured && (data.comped || data.stripe_customer_id)) {
            const property = data.comped ? 'comped' : 'stripe_customer_id';
            throw new errors.ValidationError({
                message: tpl(messages.stripeNotConnected),
                context: 'Attempting to import members with Stripe data when there is no Stripe account connected.',
                help: 'You need to connect to Stripe to import Stripe customers. ',
                property
            });
        }

        let model;

        try {
            model = await this.memberRepository.create(data, options);
        } catch (error) {
            if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
                throw new errors.ValidationError({
                    message: tpl(messages.memberAlreadyExists),
                    context: 'Attempting to add member with existing email address'
                });
            }
            throw error;
        }

        try {
            if (data.stripe_customer_id) {
                await this.memberRepository.linkStripeCustomer({
                    customer_id: data.stripe_customer_id,
                    member_id: model.id
                }, options);
            }
        } catch (error) {
            const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
            if (isStripeLinkingError) {
                if (error.message.indexOf('customer') && error.code === 'resource_missing') {
                    error.message = `Member not imported. ${error.message}`;
                    error.context = 'Missing Stripe Customer';
                    error.help = 'Make sure you\'re connected to the correct Stripe Account';
                }

                await this.memberRepository.destroy({
                    id: model.id
                }, options);
            }
            throw error;
        }

        if (!this.labsService.isSet('multipleProducts')) {
            if (data.comped) {
                await this.memberRepository.setComplimentarySubscription(model, options);
            }
        }

        if (options.send_email) {
            await this.emailService.sendEmailWithMagicLink({
                email: model.get('email'), requestedType: options.email_type
            });
        }

        return this.read({id: model.id}, options);
    }

    async edit(data, options) {
        if (!this.labsService.isSet('multipleProducts')) {
            delete data.products;
        }

        let model;

        try {
            model = await this.memberRepository.update(data, options);
        } catch (error) {
            if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
                throw new errors.ValidationError({
                    message: tpl(messages.memberAlreadyExists),
                    context: 'Attempting to edit member with existing email address'
                });
            }

            throw error;
        }

        const hasCompedSubscription = !!model.related('stripeSubscriptions').find((sub) => {
            return sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active';
        });

        if (!this.labsService.isSet('multipleProducts')) {
            if (typeof data.comped === 'boolean') {
                if (data.comped && !hasCompedSubscription) {
                    await this.memberRepository.setComplimentarySubscription(model);
                } else if (!data.comped && hasCompedSubscription) {
                    await this.memberRepository.cancelComplimentarySubscription(model);
                }
            }
        }

        return this.read({id: model.id}, options);
    }

    async browse(options) {
        const defaultWithRelated = [
            'labels',
            'stripeSubscriptions',
            'stripeSubscriptions.customer',
            'stripeSubscriptions.stripePrice',
            'stripeSubscriptions.stripePrice.stripeProduct',
            'products'
        ];

        const originalWithRelated = options.withRelated || [];

        const withRelated = new Set((originalWithRelated).concat(defaultWithRelated));

        if (!withRelated.has('productEvents')) {
            withRelated.add('productEvents');
        }

        if (withRelated.has('email_recipients')) {
            withRelated.add('email_recipients.email');
        }

        const page = await this.memberRepository.list({
            ...options,
            withRelated: Array.from(withRelated)
        });

        if (!page) {
            return null;
        }

        const members = page.data.map(model => model.toJSON(options));

        const data = members.map((member) => {
            member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
            this.attachSubscriptionsToMember(member);
            if (!originalWithRelated.includes('products')) {
                delete member.products;
            }
            return member;
        });

        return {
            data,
            meta: page.meta
        };
    }
};
<commit_msg>Added the product name property in member reads<commit_after>const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const moment = require('moment');

const messages = {
    stripeNotConnected: 'Missing Stripe connection.',
    memberAlreadyExists: 'Member already exists.'
};

/**
 * @typedef {object} ILabsService
 * @prop {(key: string) => boolean} isSet
 */

/**
 * @typedef {object} IEmailService
 * @prop {(data: {email: string, requestedType: string}) => Promise<any>} sendEmailWithMagicLink
 */

/**
 * @typedef {object} IStripeService
 * @prop {boolean} configured
 */

module.exports = class MemberBREADService {
    /**
     * @param {object} deps
     * @param {import('../repositories/member')} deps.memberRepository
     * @param {import('@tryghost/members-offers/lib/application/OffersAPI')} deps.offersAPI
     * @param {ILabsService} deps.labsService
     * @param {IEmailService} deps.emailService
     * @param {IStripeService} deps.stripeService
     */
    constructor({memberRepository, labsService, emailService, stripeService, offersAPI}) {
        this.offersAPI = offersAPI;
        /** @private */
        this.memberRepository = memberRepository;
        /** @private */
        this.labsService = labsService;
        /** @private */
        this.emailService = emailService;
        /** @private */
        this.stripeService = stripeService;
    }

    /**
     * @private
     */
    attachSubscriptionsToMember(member) {
        if (!member.products || !Array.isArray(member.products)) {
            return member;
        }

        const subscriptionProducts = (member.subscriptions || []).map(sub => sub.price.product.product_id);
        for (const product of member.products) {
            if (!subscriptionProducts.includes(product.id)) {
                const productAddEvent = member.productEvents.find(event => event.product_id === product.id);
                let startDate;
                if (!productAddEvent || productAddEvent.action !== 'added') {
                    startDate = moment();
                } else {
                    startDate = moment(productAddEvent.created_at);
                }
                member.subscriptions.push({
                    id: '',
                    tier: product,
                    customer: {
                        id: '',
                        name: member.name,
                        email: member.email
                    },
                    plan: {
                        id: '',
                        nickname: 'Complimentary',
                        interval: 'year',
                        currency: 'USD',
                        amount: 0
                    },
                    status: 'active',
                    start_date: startDate,
                    default_payment_card_last4: '****',
                    cancel_at_period_end: false,
                    cancellation_reason: null,
                    current_period_end: moment(startDate).add(1, 'year'),
                    price: {
                        id: '',
                        price_id: '',
                        nickname: 'Complimentary',
                        amount: 0,
                        interval: 'year',
                        type: 'recurring',
                        currency: 'USD',
                        product: {
                            id: '',
                            product_id: product.id
                        }
                    }
                });
            }
        }

        for (const subscription of member.subscriptions) {
            if (!subscription.tier) {
                subscription.tier = member.products.find(product => product.id === subscription.price.product.product_id);
            }
        }
    }

    /**
     * @private
     */
    attachOffersToSubscriptions(member, subscriptionOffers) {
        member.subscriptions = member.subscriptions.map((subscription) => {
            const offer = subscriptionOffers[subscription.id];
            if (offer) {
                subscription.offer = offer;
            } else {
                subscription.offer = null;
            }
            return subscription;
        });
    }

    async read(data, options = {}) {
        const defaultWithRelated = [
            'labels',
            'stripeSubscriptions',
            'stripeSubscriptions.customer',
            'stripeSubscriptions.stripePrice',
            'stripeSubscriptions.stripePrice.stripeProduct',
            'stripeSubscriptions.stripePrice.stripeProduct.product',
            'products',
            'offerRedemptions'
        ];

        const withRelated = new Set((options.withRelated || []).concat(defaultWithRelated));

        if (!withRelated.has('productEvents')) {
            withRelated.add('productEvents');
        }

        if (withRelated.has('email_recipients')) {
            withRelated.add('email_recipients.email');
        }

        const model = await this.memberRepository.get(data, {
            ...options,
            withRelated: Array.from(withRelated)
        });

        if (!model) {
            return null;
        }

        const subscriptionOffers = await model.related('offerRedemptions').toJSON().reduce(async (promiseObj, offerRedemption) => {
            const obj = await promiseObj;
            const offer = await this.offersAPI.getOffer({id: offerRedemption.offer_id});
            return {
                ...obj,
                [offerRedemption.subscription_id]: offer
            };
        }, Promise.resolve({}));

        model.related('stripeSubscriptions').forEach((subscriptionModel) => {
            const offer = subscriptionOffers[subscriptionModel.id];
            if (!offer) {
                return;
            }

            if (offer.cadence !== subscriptionModel.related('stripePrice').get('interval')) {
                return;
            }

            if (offer.tier.id !== subscriptionModel.related('stripePrice').related('stripeProduct').get('product_id')) {
                return;
            }

            subscriptionOffers[subscriptionModel.get('subscription_id')] = offer;
        });

        const member = model.toJSON(options);

        member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
        this.attachSubscriptionsToMember(member);

        this.attachOffersToSubscriptions(member, subscriptionOffers);

        return member;
    }

    async add(data, options) {
        if (!this.labsService.isSet('multipleProducts')) {
            delete data.products;
        }

        if (!this.stripeService.configured && (data.comped || data.stripe_customer_id)) {
            const property = data.comped ? 'comped' : 'stripe_customer_id';
            throw new errors.ValidationError({
                message: tpl(messages.stripeNotConnected),
                context: 'Attempting to import members with Stripe data when there is no Stripe account connected.',
                help: 'You need to connect to Stripe to import Stripe customers. ',
                property
            });
        }

        let model;

        try {
            model = await this.memberRepository.create(data, options);
        } catch (error) {
            if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
                throw new errors.ValidationError({
                    message: tpl(messages.memberAlreadyExists),
                    context: 'Attempting to add member with existing email address'
                });
            }
            throw error;
        }

        try {
            if (data.stripe_customer_id) {
                await this.memberRepository.linkStripeCustomer({
                    customer_id: data.stripe_customer_id,
                    member_id: model.id
                }, options);
            }
        } catch (error) {
            const isStripeLinkingError = error.message && (error.message.match(/customer|plan|subscription/g));
            if (isStripeLinkingError) {
                if (error.message.indexOf('customer') && error.code === 'resource_missing') {
                    error.message = `Member not imported. ${error.message}`;
                    error.context = 'Missing Stripe Customer';
                    error.help = 'Make sure you\'re connected to the correct Stripe Account';
                }

                await this.memberRepository.destroy({
                    id: model.id
                }, options);
            }
            throw error;
        }

        if (!this.labsService.isSet('multipleProducts')) {
            if (data.comped) {
                await this.memberRepository.setComplimentarySubscription(model, options);
            }
        }

        if (options.send_email) {
            await this.emailService.sendEmailWithMagicLink({
                email: model.get('email'), requestedType: options.email_type
            });
        }

        return this.read({id: model.id}, options);
    }

    async edit(data, options) {
        if (!this.labsService.isSet('multipleProducts')) {
            delete data.products;
        }

        let model;

        try {
            model = await this.memberRepository.update(data, options);
        } catch (error) {
            if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
                throw new errors.ValidationError({
                    message: tpl(messages.memberAlreadyExists),
                    context: 'Attempting to edit member with existing email address'
                });
            }

            throw error;
        }

        const hasCompedSubscription = !!model.related('stripeSubscriptions').find((sub) => {
            return sub.get('plan_nickname') === 'Complimentary' && sub.get('status') === 'active';
        });

        if (!this.labsService.isSet('multipleProducts')) {
            if (typeof data.comped === 'boolean') {
                if (data.comped && !hasCompedSubscription) {
                    await this.memberRepository.setComplimentarySubscription(model);
                } else if (!data.comped && hasCompedSubscription) {
                    await this.memberRepository.cancelComplimentarySubscription(model);
                }
            }
        }

        return this.read({id: model.id}, options);
    }

    async browse(options) {
        const defaultWithRelated = [
            'labels',
            'stripeSubscriptions',
            'stripeSubscriptions.customer',
            'stripeSubscriptions.stripePrice',
            'stripeSubscriptions.stripePrice.stripeProduct',
            'products'
        ];

        const originalWithRelated = options.withRelated || [];

        const withRelated = new Set((originalWithRelated).concat(defaultWithRelated));

        if (!withRelated.has('productEvents')) {
            withRelated.add('productEvents');
        }

        if (withRelated.has('email_recipients')) {
            withRelated.add('email_recipients.email');
        }

        const page = await this.memberRepository.list({
            ...options,
            withRelated: Array.from(withRelated)
        });

        if (!page) {
            return null;
        }

        const members = page.data.map(model => model.toJSON(options));

        const data = members.map((member) => {
            member.subscriptions = member.subscriptions.filter(sub => !!sub.price);
            this.attachSubscriptionsToMember(member);
            if (!originalWithRelated.includes('products')) {
                delete member.products;
            }
            return member;
        });

        return {
            data,
            meta: page.meta
        };
    }
};
<|endoftext|>


<commit_before>/* Modernizr 2.5.2 (Custom Build) | MIT & BSD
 * Build: http://www.modernizr.com/download/#-fontface-geolocation-shiv-mq-teststyles
 */
;window.Modernizr=function(a,b,c){function v(a){i.cssText=a}function w(a,b){return v(prefixes.join(a+";")+(b||""))}function x(a,b){return typeof a===b}function y(a,b){return!!~(""+a).indexOf(b)}function z(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:x(f,"function")?f.bind(d||b):f}return!1}var d="2.5.2",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l={},m={},n={},o=[],p=o.slice,q,r=function(a,c,d,e){var h,i,j,k=b.createElement("div"),l=b.body,m=l?l:b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),k.appendChild(j);return h=["","<style>",a,"</style>"].join(""),k.id=g,m.innerHTML+=h,m.appendChild(k),l||f.appendChild(m),i=c(k,a),l?k.parentNode.removeChild(k):m.parentNode.removeChild(m),!!i},s=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return r("@media "+b+" { #"+g+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},t={}.hasOwnProperty,u;!x(t,"undefined")&&!x(t.call,"undefined")?u=function(a,b){return t.call(a,b)}:u=function(a,b){return b in a&&x(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=p.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(p.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(p.call(arguments)))};return e});var A=function(a,c){var d=a.join(""),f=c.length;r(d,function(a,c){var d=b.styleSheets[b.styleSheets.length-1],g=d?d.cssRules&&d.cssRules[0]?d.cssRules[0].cssText:d.cssText||"":"",h=a.childNodes,i={};while(f--)i[h[f].id]=h[f];e.fontface=/src/i.test(g)&&g.indexOf(c.split(" ")[0])===0},f,c)}(['@font-face {font-family:"font";src:url("https://")}'],["fontface"]);l.geolocation=function(){return!!navigator.geolocation},l.fontface=function(){return e.fontface};for(var B in l)u(l,B)&&(q=B.toLowerCase(),e[q]=l[B](),o.push((e[q]?"":"no-")+q));return v(""),h=j=null,function(a,b){function g(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function h(){var a=k.elements;return typeof a=="string"?a.split(" "):a}function i(a){function m(){var a=j.cloneNode(!1);return k.shivMethods?(i(a),a):a}function n(a){var b=(c[a]||(c[a]=e(a))).cloneNode(!1);return k.shivMethods&&!d.test(a)?j.appendChild(b):b}var b,c={},e=a.createElement,f=a.createDocumentFragment,g=h(),j=f(),l=g.length;while(l--)b=g[l],c[b]=e(b),j.createElement(b);a.createElement=n,a.createDocumentFragment=m}function j(a){var b;return a.documentShived?a:(k.shivCSS&&!e&&(b=!!g(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),k.shivMethods&&!f&&(b=!i(a)),b&&(a.documentShived=b),a)}var c=a.html5||{},d=/^<|^(?:button|iframe|input|script|textarea)$/i,e,f;(function(){var c,d=b.createElement("a"),g=a.getComputedStyle,h=b.documentElement,i=b.body||(c=h.insertBefore(b.createElement("body"),h.firstChild));i.insertBefore(d,i.firstChild),d.hidden=!0,d.innerHTML="<xyz></xyz>",e=(d.currentStyle||g(d,null)).display=="none",f=d.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}(),i.removeChild(d),c&&h.removeChild(c)})();var k={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:j};a.html5=k,j(b)}(this,b),e._version=d,e.mq=s,e.testStyles=r,e}(this,this.document);
<commit_msg>Use modernizr build from h5bp. They know this better than me.<commit_after>/* Modernizr 2.5.2 (Custom Build) | MIT & BSD
 * Build: http://www.modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load
 */
;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a)if(j[a[d]]!==c)return b=="pfx"?a[d]:!0;return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.substr(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function L(){e.input=function(c){for(var d=0,e=c.length;d<e;d++)u[c[d]]=c[d]in k;return u.list&&(u.list=!!b.createElement("datalist")&&!!a.HTMLDataListElement),u}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),e.inputtypes=function(a){for(var d=0,e,f,h,i=a.length;d<i;d++)k.setAttribute("type",f=a[d]),e=k.type!=="text",e&&(k.value=l,k.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(f)&&k.style.WebkitAppearance!==c?(g.appendChild(k),h=b.defaultView,e=h.getComputedStyle&&h.getComputedStyle(k,null).WebkitAppearance!=="textfield"&&k.offsetHeight!==0,g.removeChild(k)):/^(search|tel)$/.test(f)||(/^(url|email)$/.test(f)?e=k.checkValidity&&k.checkValidity()===!1:/^color$/.test(f)?(g.appendChild(k),g.offsetWidth,e=k.value!=l,g.removeChild(k)):e=k.value!=l)),t[a[d]]=!!e;return t}("search tel url email datetime date month week time datetime-local number range color".split(" "))}var d="2.5.2",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k=b.createElement("input"),l=":)",m={}.toString,n=" -webkit- -moz- -o- -ms- ".split(" "),o="Webkit Moz O ms",p=o.split(" "),q=o.toLowerCase().split(" "),r={svg:"http://www.w3.org/2000/svg"},s={},t={},u={},v=[],w=v.slice,x,y=function(a,c,d,e){var f,i,j,k=b.createElement("div"),l=b.body,m=l?l:b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),k.appendChild(j);return f=["","<style>",a,"</style>"].join(""),k.id=h,m.innerHTML+=f,m.appendChild(k),l||g.appendChild(m),i=c(k,a),l?k.parentNode.removeChild(k):m.parentNode.removeChild(m),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e});var K=function(c,d){var f=c.join(""),g=d.length;y(f,function(c,d){var f=b.styleSheets[b.styleSheets.length-1],h=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"",i=c.childNodes,j={};while(g--)j[i[g].id]=i[g];e.touch="ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch||(j.touch&&j.touch.offsetTop)===9,e.csstransforms3d=(j.csstransforms3d&&j.csstransforms3d.offsetLeft)===9&&j.csstransforms3d.offsetHeight===3,e.generatedcontent=(j.generatedcontent&&j.generatedcontent.offsetHeight)>=1,e.fontface=/src/i.test(h)&&h.indexOf(d.split(" ")[0])===0},g,d)}(['@font-face {font-family:"font";src:url("https://")}',["@media (",n.join("touch-enabled),("),h,")","{#touch{top:9px;position:absolute}}"].join(""),["@media (",n.join("transform-3d),("),h,")","{#csstransforms3d{left:9px;position:absolute;height:3px;}}"].join(""),['#generatedcontent:after{content:"',l,'";visibility:hidden}'].join("")],["fontface","touch","csstransforms3d","generatedcontent"]);s.flexbox=function(){return J("flexOrder")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){try{var d=b.createElement("canvas"),e;e=!(!a.WebGLRenderingContext||!d.getContext("experimental-webgl")&&!d.getContext("webgl")),d=c}catch(f){e=!1}return e},s.touch=function(){return e.touch},s.geolocation=function(){return!!navigator.geolocation},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){for(var b=-1,c=p.length;++b<c;)if(a[p[b]+"WebSocket"])return!0;return"WebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&(a=e.csstransforms3d),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){return e.fontface},s.generatedcontent=function(){return e.generatedcontent},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="<svg/>",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var M in s)C(s,M)&&(x=M.toLowerCase(),e[x]=s[M](),v.push((e[x]?"":"no-")+x));return e.input||L(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,g.className+=" "+(b?"":"no-")+a,e[a]=b}return e},D(""),i=k=null,function(a,b){function g(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x<style>"+b+"</style>",d.insertBefore(c.lastChild,d.firstChild)}function h(){var a=k.elements;return typeof a=="string"?a.split(" "):a}function i(a){function m(){var a=j.cloneNode(!1);return k.shivMethods?(i(a),a):a}function n(a){var b=(c[a]||(c[a]=e(a))).cloneNode(!1);return k.shivMethods&&!d.test(a)?j.appendChild(b):b}var b,c={},e=a.createElement,f=a.createDocumentFragment,g=h(),j=f(),l=g.length;while(l--)b=g[l],c[b]=e(b),j.createElement(b);a.createElement=n,a.createDocumentFragment=m}function j(a){var b;return a.documentShived?a:(k.shivCSS&&!e&&(b=!!g(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),k.shivMethods&&!f&&(b=!i(a)),b&&(a.documentShived=b),a)}var c=a.html5||{},d=/^<|^(?:button|iframe|input|script|textarea)$/i,e,f;(function(){var c,d=b.createElement("a"),g=a.getComputedStyle,h=b.documentElement,i=b.body||(c=h.insertBefore(b.createElement("body"),h.firstChild));i.insertBefore(d,i.firstChild),d.hidden=!0,d.innerHTML="<xyz></xyz>",e=(d.currentStyle||g(d,null)).display=="none",f=d.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}(),i.removeChild(d),c&&h.removeChild(c)})();var k={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video".split(" "),shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:j};a.html5=k,j(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return o.call(a)=="[object Function]"}function e(a){return typeof a=="string"}function f(){}function g(a){return!a||a=="loaded"||a=="complete"||a=="uninitialized"}function h(){var a=p.shift();q=1,a?a.t?m(function(){(a.t=="c"?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){a!="img"&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l={},o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};y[c]===1&&(r=1,y[c]=[],l=b.createElement(a)),a=="object"?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),a!="img"&&(r||y[c]===2?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i(b=="c"?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),p.length==1&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=!!b.attachEvent,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return o.call(a)=="[object Array]"},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f<d;f++)g=a[f].split("="),(e=z[g.shift()])&&(c=e(c,g));for(f=0;f<b;f++)c=x[f](c);return c}function g(a,e,f,g,i){var j=b(a),l=j.autoCallback;j.url.split(".").pop().split("?").shift(),j.bypass||(e&&(e=d(e)?e:e[a]||e[g]||e[a.split("/").pop().split("?")[0]]||h),j.instead?j.instead(a,e,f,g,i):(y[j.url]?j.noexec=!0:y[j.url]=1,f.load(j.url,j.forceCSS||!j.forceJS&&"css"==j.url.split(".").pop().split("?").shift()?"c":c,j.noexec,j.attrs,j.timeout),(d(e)||d(l))&&f.load(function(){k(),e&&e(j.origUrl,i,g),l&&l(j.origUrl,i,g),y[j.url]=2})))}function i(a,b){function c(a,c){if(a){if(e(a))c||(j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}),g(a,j,b,0,h);else if(Object(a)===a)for(n in m=function(){var b=0,c;for(c in a)a.hasOwnProperty(c)&&b++;return b}(),a)a.hasOwnProperty(n)&&(!c&&!--m&&(d(j)?j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}:j[n]=function(a){return function(){var b=[].slice.call(arguments);a&&a.apply(this,b),l()}}(k[n])),g(a[n],j,b,n,h))}else!c&&l()}var h=!!a.test,i=a.load||a.both,j=a.callback||f,k=j,l=a.complete||f,m,n;c(h?a.yep:a.nope,!!i),i&&c(i)}var j,l,m=this.yepnope.loader;if(e(a))g(a,0,m,0);else if(w(a))for(j=0;j<a.length;j++)l=a[j],e(l)?g(l,0,m,0):w(l)?B(l):Object(l)===l&&i(l,m);else Object(a)===a&&i(a,m)},B.addPrefix=function(a,b){z[a]=b},B.addFilter=function(a){x.push(a)},B.errorTimeout=1e4,b.readyState==null&&b.addEventListener&&(b.readyState="loading",b.addEventListener("DOMContentLoaded",A=function(){b.removeEventListener("DOMContentLoaded",A,0),b.readyState="complete"},0)),a.yepnope=k(),a.yepnope.executeStack=h,a.yepnope.injectJs=function(a,c,d,e,i,j){var k=b.createElement("script"),l,o,e=e||B.errorTimeout;k.src=a;for(o in d)k.setAttribute(o,d[o]);c=j?h:c||f,k.onreadystatechange=k.onload=function(){!l&&g(k.readyState)&&(l=1,c(),k.onload=k.onreadystatechange=null)},m(function(){l||(l=1,c(1))},e),i?k.onload():n.parentNode.insertBefore(k,n)},a.yepnope.injectCss=function(a,c,d,e,g,i){var e=b.createElement("link"),j,c=i?h:c||f;e.href=a,e.rel="stylesheet",e.type="text/css";for(j in d)e.setAttribute(j,d[j]);g||(n.parentNode.insertBefore(e,n),m(c,0))}}(this,document),Modernizr.load=function(){yepnope.apply(window,[].slice.call(arguments,0))};
<|endoftext|>


<commit_before>// ==UserScript==
// @name         StackOverflow Post Reminder
// @namespace    https://stackoverflow.com/users/1454538/
// @author       enki
// @match        *://stackoverflow.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @require
// @run-at document-end
// ==/UserScript==

var reminders = {};

$(function () {
    Reminders.Load();

    // listen for changes on the
    GM_addValueChangeListener("reminders", function () {
        // reload reminders
        console.log("reminder data has changed, updating reminder list now...")
        Reminders.Load();
    });

    var postId = "1234",
        postUrl = "example.com/posts/1234",
        postTitle = "Post 1234",
        siteName = "example.com",
        reminderDate = "12/25/2015",
        reminderId = postId + siteName;

    var rem = new Reminder(reminderId, postId, postUrl, postTitle, siteName, reminderDate);

    Reminders.Add(rem);
    Reminders.Save();

    //make sure the reminder was added
    alert(reminders[reminderId].postTitle);

});

<commit_msg>included reminders.js in the require statement<commit_after>// ==UserScript==
// @name         StackOverflow Post Reminder
// @namespace    https://stackoverflow.com/users/1454538/
// @author       enki
// @match        *://stackoverflow.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_removeValueChangeListener
// @require      https://code.jquery.com/ui/1.11.4/jquery-ui.min.js
// @require      https://rawgit.com/enki-code/UserScripts/master/reminders.js
// @run-at document-end
// ==/UserScript==

var reminders = {};

$(function () {
    Reminders.Load();

    // listen for changes on the
    GM_addValueChangeListener("reminders", function () {
        // reload reminders
        console.log("reminder data has changed, updating reminder list now...")
        Reminders.Load();
    });

    var postId = "1234",
        postUrl = "example.com/posts/1234",
        postTitle = "Post 1234",
        siteName = "example.com",
        reminderDate = "12/25/2015",
        reminderId = postId + siteName;

    var rem = new Reminder(reminderId, postId, postUrl, postTitle, siteName, reminderDate);

    Reminders.Add(rem);
    Reminders.Save();

    //make sure the reminder was added
    alert(reminders[reminderId].postTitle);

});

<|endoftext|>


<commit_before>// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

// Run before every test, to disable User Welcome tour
before(function() {
    cy.login();

    cy.visit('/').then(() => {
        cy.tutorialClose();
    });
});
<commit_msg>cypress fix by Mauro<commit_after>// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

// Run before every test, to disable User Welcome tour
before(function() {
    cy.login().then(() => {
        cy.tutorialClose();
    });
});
<|endoftext|>


<commit_before>/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */

/*************************************************************
 *
 *  MathJax/extensions/CHTML-preview.js
 *
 *  Implements a fast preview using the Common-HTML output jax
 *  and then a slower update to the more accurate HTML-CSS output
 *  (or whatever the user has selected).
 *
 *  ---------------------------------------------------------------------
 *
 *  Copyright (c) 2014 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

(function (HUB,HTML) {

  var SETTINGS = HUB.config.menuSettings;

  var CHTMLpreview = MathJax.Extension["CHTML-preview"] = {
    version: "1.0",

    //
    //  Configuration for the chunking of the main output
    //  after the previews have been created, and other configuration.
    //
    config: HUB.CombineConfig("CHTML-preview",{
      Chunks: {EqnChunk: 10000, EqnChunkFactor: 1, EqnChunkDelay: 0},
      color: "inherit!important",
      updateTime: 10, updateDelay: 10,
      messageStyle: "none",
      disabled: false
    }),

    //
    //  Ajust the chunking of the output jax
    //
    Config: function () {
      HUB.Config({
        "HTML-CSS": this.config.Chunks,
        SVG: this.config.Chunks,
      });
      MathJax.Ajax.Styles({".MathJax_Preview":{color:this.config.color}});
      var update, delay, style, done, saved;
      var config = this.config;

      HUB.Register.MessageHook("Begin Math Output",function () {
        if (!done && !config.disabled && SETTINGS.CHTMLpreview &&
            SETTINGS.renderer !== "CommonHTML") {
          update = HUB.processUpdateTime; delay = HUB.processUpdateDelay;
          style = HUB.config.messageStyle;
          HUB.processUpdateTime = config.updateTime;
          HUB.processUpdateDelay = config.updateDelay;
          HUB.Config({messageStyle: config.messageStyle});
          MathJax.Message.Clear(0,0);
          saved = true;
        }
      });
      HUB.Register.MessageHook("End Math Output",function () {
        if (!done && saved) {
          HUB.processUpdateTime = update;
          HUB.processUpdateDelay = delay;
          HUB.Config({messageStyle: style});
          done = true;
        }
      });
    },

    //
    //  Insert a preview span, if there isn't one already,
    //  and call the CommonHTML output jax to create the preview
    //
    Preview: function (data) {
      if (this.config.disabled || !SETTINGS.CHTMLpreview ||
          SETTINGS.renderer === "CommonHTML") return;
      var preview = data.script.previousSibling;
      if (!preview || preview.className !== MathJax.Hub.config.preRemoveClass) {
        preview = HTML.Element("span",{className:MathJax.Hub.config.preRemoveClass});
        data.script.parentNode.insertBefore(preview,data.script);
      }
      preview.innerHTML = "";
      return this.postFilter(preview,data);
    },
    postFilter: function (preview,data) {
      try {
        data.math.root.toCommonHTML(preview);
      } catch (err) {
        //
        //  Load the CommonHTML jax if it is not already loaded
        //
        if (!data.math.root.toCommonHTML) {
          var queue = MathJax.Callback.Queue();
          HUB.RestartAfter(queue.Push(
            ["Require",MathJax.Ajax,"[MathJax]/jax/output/CommonHTML/config.js"],
            ["Require",MathJax.Ajax,"[MathJax]/jax/output/CommonHTML/jax.js"]
          ));
        }
        if (!err.restart) {throw err} // an actual error
        return MathJax.Callback.After(["postFilter",this,preview,data],err.restart);
      }
    },

    //
    //  Hook into the input jax postFilter to create the previews as
    //  the input jax are processed.
    //
    Register: function (name) {
      HUB.Register.StartupHook(name+" Jax Require",function () {
        var jax = MathJax.InputJax[name];
        jax.postfilterHooks.Add(["Preview",MathJax.Extension["CHTML-preview"]],50);
      });
    }
  }

  //
  //  Hook into each input jax
  //
  CHTMLpreview.Register("TeX");
  CHTMLpreview.Register("MathML");
  CHTMLpreview.Register("AsciiMath");

  HUB.Register.StartupHook("End Config",["Config",CHTMLpreview]);

})(MathJax.Hub,MathJax.HTML);

MathJax.Ajax.loadComplete("[MathJax]/extensions/CHTML-preview.js");

<commit_msg>Save preview in script.MathJax to fix issue #975.<commit_after>/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */

/*************************************************************
 *
 *  MathJax/extensions/CHTML-preview.js
 *
 *  Implements a fast preview using the Common-HTML output jax
 *  and then a slower update to the more accurate HTML-CSS output
 *  (or whatever the user has selected).
 *
 *  ---------------------------------------------------------------------
 *
 *  Copyright (c) 2014 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

(function (HUB,HTML) {

  var SETTINGS = HUB.config.menuSettings;

  var CHTMLpreview = MathJax.Extension["CHTML-preview"] = {
    version: "1.0",

    //
    //  Configuration for the chunking of the main output
    //  after the previews have been created, and other configuration.
    //
    config: HUB.CombineConfig("CHTML-preview",{
      Chunks: {EqnChunk: 10000, EqnChunkFactor: 1, EqnChunkDelay: 0},
      color: "inherit!important",
      updateTime: 10, updateDelay: 10,
      messageStyle: "none",
      disabled: false
    }),

    //
    //  Ajust the chunking of the output jax
    //
    Config: function () {
      HUB.Config({
        "HTML-CSS": this.config.Chunks,
        SVG: this.config.Chunks,
      });
      MathJax.Ajax.Styles({".MathJax_Preview":{color:this.config.color}});
      var update, delay, style, done, saved;
      var config = this.config;

      HUB.Register.MessageHook("Begin Math Output",function () {
        if (!done && !config.disabled && SETTINGS.CHTMLpreview &&
            SETTINGS.renderer !== "CommonHTML") {
          update = HUB.processUpdateTime; delay = HUB.processUpdateDelay;
          style = HUB.config.messageStyle;
          HUB.processUpdateTime = config.updateTime;
          HUB.processUpdateDelay = config.updateDelay;
          HUB.Config({messageStyle: config.messageStyle});
          MathJax.Message.Clear(0,0);
          saved = true;
        }
      });
      HUB.Register.MessageHook("End Math Output",function () {
        if (!done && saved) {
          HUB.processUpdateTime = update;
          HUB.processUpdateDelay = delay;
          HUB.Config({messageStyle: style});
          done = true;
        }
      });
    },

    //
    //  Insert a preview span, if there isn't one already,
    //  and call the CommonHTML output jax to create the preview
    //
    Preview: function (data) {
      if (this.config.disabled || !SETTINGS.CHTMLpreview ||
          SETTINGS.renderer === "CommonHTML") return;
      var preview = data.script.MathJax.preview || data.script.previousSibling;
      if (!preview || preview.className !== MathJax.Hub.config.preRemoveClass) {
        preview = HTML.Element("span",{className:MathJax.Hub.config.preRemoveClass});
        data.script.parentNode.insertBefore(preview,data.script);
        data.script.MathJax.preview = preview;
      }
      preview.innerHTML = "";
      return this.postFilter(preview,data);
    },
    postFilter: function (preview,data) {
      try {
        data.math.root.toCommonHTML(preview);
      } catch (err) {
        //
        //  Load the CommonHTML jax if it is not already loaded
        //
        if (!data.math.root.toCommonHTML) {
          var queue = MathJax.Callback.Queue();
          HUB.RestartAfter(queue.Push(
            ["Require",MathJax.Ajax,"[MathJax]/jax/output/CommonHTML/config.js"],
            ["Require",MathJax.Ajax,"[MathJax]/jax/output/CommonHTML/jax.js"]
          ));
        }
        if (!err.restart) {throw err} // an actual error
        return MathJax.Callback.After(["postFilter",this,preview,data],err.restart);
      }
    },

    //
    //  Hook into the input jax postFilter to create the previews as
    //  the input jax are processed.
    //
    Register: function (name) {
      HUB.Register.StartupHook(name+" Jax Require",function () {
        var jax = MathJax.InputJax[name];
        jax.postfilterHooks.Add(["Preview",MathJax.Extension["CHTML-preview"]],50);
      });
    }
  }

  //
  //  Hook into each input jax
  //
  CHTMLpreview.Register("TeX");
  CHTMLpreview.Register("MathML");
  CHTMLpreview.Register("AsciiMath");

  HUB.Register.StartupHook("End Config",["Config",CHTMLpreview]);

})(MathJax.Hub,MathJax.HTML);

MathJax.Ajax.loadComplete("[MathJax]/extensions/CHTML-preview.js");

<|endoftext|>


<commit_before>import * as fs from "fs"
import * as path from "path"
import sharp from "./safe-sharp"
import { createContentDigest, cpuCoreCount, slash } from "gatsby-core-utils"
import {
  defaultIcons,
  doesIconExist,
  addDigestToPath,
  favicons,
} from "./common"

sharp.simd(true)

// Handle Sharp's concurrency based on the Gatsby CPU count
// See: http://sharp.pixelplumbing.com/en/stable/api-utility/#concurrency
// See: https://www.gatsbyjs.org/docs/multi-core-builds/
sharp.concurrency(cpuCoreCount())

async function generateIcon(icon, srcIcon) {
  const imgPath = path.join(`public`, icon.src)

  // console.log(`generating icon: `, icon.src)
  // if (fs.existsSync(imgPath)) {
  //   console.log(`icon already Exists, not regenerating`)
  //   return true
  // }
  const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`)))

  // For vector graphics, instruct sharp to use a pixel density
  // suitable for the resolution we're rasterizing to.
  // For pixel graphics sources this has no effect.
  // Sharp accept density from 1 to 2400
  const density = Math.min(2400, Math.max(1, size))

  return sharp(srcIcon, { density })
    .resize({
      width: size,
      height: size,
      fit: `contain`,
      background: { r: 255, g: 255, b: 255, alpha: 0 },
    })
    .toFile(imgPath)
}

async function checkCache(cache, icon, srcIcon, srcIconDigest, callback) {
  const cacheKey = createContentDigest(`${icon.src}${srcIcon}${srcIconDigest}`)

  const created = cache.get(cacheKey, srcIcon)
  if (!created) {
    cache.set(cacheKey, true)

    try {
      await callback(icon, srcIcon)
    } catch (e) {
      cache.set(cacheKey, false)
      throw e
    }
  }
}

/**
 * Setup pluginOption defaults
 */
exports.onPreInit = (_, pluginOptions) => {
  pluginOptions.cache_busting_mode = pluginOptions.cache_busting_mode ?? `query`
  pluginOptions.include_favicon = pluginOptions.include_favicon ?? true
  pluginOptions.legacy = pluginOptions.legacy ?? true
  pluginOptions.theme_color_in_head = pluginOptions.theme_color_in_head ?? true
  pluginOptions.cacheDigest = null

  if (pluginOptions.cache_busting_mode !== `none` && pluginOptions.icon) {
    pluginOptions.cacheDigest = createContentDigest(
      fs.readFileSync(pluginOptions.icon)
    )
  }
}

exports.onPostBootstrap = async (
  { reporter, parentSpan, basePath },
  { localize, ...manifest }
) => {
  const activity = reporter.activityTimer(`Build manifest and related icons`, {
    parentSpan,
  })

  activity.start()

  let cache = new Map()

  await makeManifest({ cache, reporter, pluginOptions: manifest, basePath })

  if (Array.isArray(localize)) {
    const locales = [...localize]
    await Promise.all(
      locales.map(locale => {
        let cacheModeOverride = {}

        /* localization requires unique filenames for output files if a different src Icon is defined.
           otherwise one language would override anothers icons in automatic mode.
        */
        if (locale.hasOwnProperty(`icon`) && !locale.hasOwnProperty(`icons`)) {
          // console.debug(`OVERRIDING CACHE BUSTING`, locale)
          cacheModeOverride = { cache_busting_mode: `name` }
        }

        return makeManifest({
          cache,
          reporter,
          pluginOptions: {
            ...manifest,
            ...locale,
            ...cacheModeOverride,
          },
          shouldLocalize: true,
          basePath,
        })
      })
    )
  }
  activity.end()
}

/**
 * The complete Triforce, or one or more components of the Triforce.
 * @typedef {Object} makeManifestArgs
 * @property {Object} cache - from gatsby-node api
 * @property {Object} reporter - from gatsby-node api
 * @property {Object} pluginOptions - from gatsby-node api/gatsby config
 * @property {boolean?} shouldLocalize
 * @property {string?} basePath - string of base path frpvided by gatsby node
 */

/**
 * Build manifest
 * @param {makeManifestArgs}
 */
const makeManifest = async ({
  cache,
  reporter,
  pluginOptions,
  shouldLocalize = false,
  basePath = ``,
}) => {
  const { icon, ...manifest } = pluginOptions
  const suffix =
    shouldLocalize && pluginOptions.lang ? `_${pluginOptions.lang}` : ``

  const faviconIsEnabled = pluginOptions.include_favicon ?? true

  // Delete options we won't pass to the manifest.webmanifest.
  delete manifest.plugins
  delete manifest.legacy
  delete manifest.theme_color_in_head
  delete manifest.cache_busting_mode
  delete manifest.crossOrigin
  delete manifest.icon_options
  delete manifest.include_favicon

  // If icons are not manually defined, use the default icon set.
  if (!manifest.icons) {
    manifest.icons = [...defaultIcons]
  }

  // Specify extra options for each icon (if requested).
  if (pluginOptions.icon_options) {
    manifest.icons = manifest.icons.map(icon => {
      return {
        ...pluginOptions.icon_options,
        ...icon,
      }
    })
  }

  // Determine destination path for icons.
  let paths = {}
  manifest.icons.forEach(icon => {
    const iconPath = path.join(`public`, path.dirname(icon.src))
    if (!paths[iconPath]) {
      const exists = fs.existsSync(iconPath)
      //create destination directory if it doesn't exist
      if (!exists) {
        fs.mkdirSync(iconPath)
      }
      paths[iconPath] = true
    }
  })

  // Only auto-generate icons if a src icon is defined.
  if (typeof icon !== `undefined`) {
    // Check if the icon exists
    if (!doesIconExist(icon)) {
      throw new Error(
        `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.`
      )
    }

    const sharpIcon = sharp(icon)

    const metadata = await sharpIcon.metadata()

    if (metadata.width !== metadata.height) {
      reporter.warn(
        `The icon(${icon}) you provided to 'gatsby-plugin-manifest' is not square.\n` +
          `The icons we generate will be square and for the best results we recommend you provide a square icon.\n`
      )
    }

    //add cache busting
    const cacheMode =
      typeof pluginOptions.cache_busting_mode !== `undefined`
        ? pluginOptions.cache_busting_mode
        : `query`

    const iconDigest = createContentDigest(fs.readFileSync(icon))

    /**
     * Given an array of icon configs, generate the various output sizes from
     * the source icon image.
     */
    async function processIconSet(iconSet) {
      //if cacheBusting is being done via url query icons must be generated before cache busting runs
      if (cacheMode === `query`) {
        await Promise.all(
          iconSet.map(dstIcon =>
            checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
          )
        )
      }

      if (cacheMode !== `none`) {
        iconSet = iconSet.map(icon => {
          let newIcon = { ...icon }
          newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode)
          return newIcon
        })
      }

      //if file names are being modified by cacheBusting icons must be generated after cache busting runs
      if (cacheMode !== `query`) {
        await Promise.all(
          iconSet.map(dstIcon =>
            checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
          )
        )
      }

      return iconSet
    }

    manifest.icons = await processIconSet(manifest.icons)

    // If favicon is enabled, apply the same caching policy and generate
    // the resized image(s)
    if (faviconIsEnabled) {
      await processIconSet(favicons)

      if (metadata.format === `svg`) {
        fs.copyFileSync(icon, path.join(`public`, `favicon.svg`))
      }
    }
  }

  //Fix #18497 by prefixing paths
  manifest.icons = manifest.icons.map(icon => {
    return {
      ...icon,
      src: slash(path.join(basePath, icon.src)),
    }
  })

  if (manifest.start_url) {
    manifest.start_url = path.posix.join(basePath, manifest.start_url)
  }

  //Write manifest
  fs.writeFileSync(
    path.join(`public`, `manifest${suffix}.webmanifest`),
    JSON.stringify(manifest)
  )
}

exports.onCreateWebpackConfig = ({ actions, plugins }, pluginOptions) => {
  actions.setWebpackConfig({
    plugins: [
      plugins.define({
        __MANIFEST_PLUGIN_HAS_LOCALISATION__:
          pluginOptions.localize && pluginOptions.localize.length,
      }),
    ],
  })
}
<commit_msg>chore(gatsby-plugin-manifest): add pluginOptionsSchema (#27421)<commit_after>import * as fs from "fs"
import * as path from "path"
import sharp from "./safe-sharp"
import { createContentDigest, cpuCoreCount, slash } from "gatsby-core-utils"
import {
  defaultIcons,
  doesIconExist,
  addDigestToPath,
  favicons,
} from "./common"

sharp.simd(true)

// Handle Sharp's concurrency based on the Gatsby CPU count
// See: http://sharp.pixelplumbing.com/en/stable/api-utility/#concurrency
// See: https://www.gatsbyjs.org/docs/multi-core-builds/
sharp.concurrency(cpuCoreCount())

async function generateIcon(icon, srcIcon) {
  const imgPath = path.join(`public`, icon.src)

  // console.log(`generating icon: `, icon.src)
  // if (fs.existsSync(imgPath)) {
  //   console.log(`icon already Exists, not regenerating`)
  //   return true
  // }
  const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`)))

  // For vector graphics, instruct sharp to use a pixel density
  // suitable for the resolution we're rasterizing to.
  // For pixel graphics sources this has no effect.
  // Sharp accept density from 1 to 2400
  const density = Math.min(2400, Math.max(1, size))

  return sharp(srcIcon, { density })
    .resize({
      width: size,
      height: size,
      fit: `contain`,
      background: { r: 255, g: 255, b: 255, alpha: 0 },
    })
    .toFile(imgPath)
}

async function checkCache(cache, icon, srcIcon, srcIconDigest, callback) {
  const cacheKey = createContentDigest(`${icon.src}${srcIcon}${srcIconDigest}`)

  const created = cache.get(cacheKey, srcIcon)
  if (!created) {
    cache.set(cacheKey, true)

    try {
      await callback(icon, srcIcon)
    } catch (e) {
      cache.set(cacheKey, false)
      throw e
    }
  }
}

if (process.env.GATSBY_EXPERIMENTAL_PLUGIN_OPTION_VALIDATION) {
  exports.pluginOptionsSchema = ({ Joi }) => {
    Joi.object({
      name: Joi.string(),
      short_name: Joi.string(),
      description: Joi.string(),
      lang: Joi.string(),
      localize: Joi.array().items(
        Joi.object({
          start_url: Joi.string(),
          name: Joi.string(),
          short_name: Joi.string(),
          description: Joi.string(),
          lang: Joi.string(),
        })
      ),
      start_url: Joi.string(),
      background_color: Joi.string(),
      theme_color: Joi.string(),
      display: Joi.string(),
      legacy: Joi.boolean(),
      include_favicon: Joi.boolean(),
      icon: Joi.string(),
      theme_color_in_head: Joi.boolean(),
      crossOrigin: Joi.string().valid(`use-credentials`, `anonymous`),
      cache_busting_mode: Joi.string().valid(`query`, `name`, `none`),
      icons: Joi.array().items(
        Joi.object({
          src: Joi.string(),
          sizes: Joi.string(),
          type: Joi.string(),
        })
      ),
      icon_options: Joi.object({
        purpose: Joi.string(),
      }),
    })
  }
}

/**
 * Setup pluginOption defaults
 * TODO: Remove once pluginOptionsSchema is stable
 */
exports.onPreInit = (_, pluginOptions) => {
  pluginOptions.cache_busting_mode = pluginOptions.cache_busting_mode ?? `query`
  pluginOptions.include_favicon = pluginOptions.include_favicon ?? true
  pluginOptions.legacy = pluginOptions.legacy ?? true
  pluginOptions.theme_color_in_head = pluginOptions.theme_color_in_head ?? true
  pluginOptions.cacheDigest = null

  if (pluginOptions.cache_busting_mode !== `none` && pluginOptions.icon) {
    pluginOptions.cacheDigest = createContentDigest(
      fs.readFileSync(pluginOptions.icon)
    )
  }
}

exports.onPostBootstrap = async (
  { reporter, parentSpan, basePath },
  { localize, ...manifest }
) => {
  const activity = reporter.activityTimer(`Build manifest and related icons`, {
    parentSpan,
  })

  activity.start()

  let cache = new Map()

  await makeManifest({ cache, reporter, pluginOptions: manifest, basePath })

  if (Array.isArray(localize)) {
    const locales = [...localize]
    await Promise.all(
      locales.map(locale => {
        let cacheModeOverride = {}

        /* localization requires unique filenames for output files if a different src Icon is defined.
           otherwise one language would override anothers icons in automatic mode.
        */
        if (locale.hasOwnProperty(`icon`) && !locale.hasOwnProperty(`icons`)) {
          // console.debug(`OVERRIDING CACHE BUSTING`, locale)
          cacheModeOverride = { cache_busting_mode: `name` }
        }

        return makeManifest({
          cache,
          reporter,
          pluginOptions: {
            ...manifest,
            ...locale,
            ...cacheModeOverride,
          },
          shouldLocalize: true,
          basePath,
        })
      })
    )
  }
  activity.end()
}

/**
 * The complete Triforce, or one or more components of the Triforce.
 * @typedef {Object} makeManifestArgs
 * @property {Object} cache - from gatsby-node api
 * @property {Object} reporter - from gatsby-node api
 * @property {Object} pluginOptions - from gatsby-node api/gatsby config
 * @property {boolean?} shouldLocalize
 * @property {string?} basePath - string of base path frpvided by gatsby node
 */

/**
 * Build manifest
 * @param {makeManifestArgs}
 */
const makeManifest = async ({
  cache,
  reporter,
  pluginOptions,
  shouldLocalize = false,
  basePath = ``,
}) => {
  const { icon, ...manifest } = pluginOptions
  const suffix =
    shouldLocalize && pluginOptions.lang ? `_${pluginOptions.lang}` : ``

  const faviconIsEnabled = pluginOptions.include_favicon ?? true

  // Delete options we won't pass to the manifest.webmanifest.
  delete manifest.plugins
  delete manifest.legacy
  delete manifest.theme_color_in_head
  delete manifest.cache_busting_mode
  delete manifest.crossOrigin
  delete manifest.icon_options
  delete manifest.include_favicon

  // If icons are not manually defined, use the default icon set.
  if (!manifest.icons) {
    manifest.icons = [...defaultIcons]
  }

  // Specify extra options for each icon (if requested).
  if (pluginOptions.icon_options) {
    manifest.icons = manifest.icons.map(icon => {
      return {
        ...pluginOptions.icon_options,
        ...icon,
      }
    })
  }

  // Determine destination path for icons.
  let paths = {}
  manifest.icons.forEach(icon => {
    const iconPath = path.join(`public`, path.dirname(icon.src))
    if (!paths[iconPath]) {
      const exists = fs.existsSync(iconPath)
      //create destination directory if it doesn't exist
      if (!exists) {
        fs.mkdirSync(iconPath)
      }
      paths[iconPath] = true
    }
  })

  // Only auto-generate icons if a src icon is defined.
  if (typeof icon !== `undefined`) {
    // Check if the icon exists
    if (!doesIconExist(icon)) {
      throw new Error(
        `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.`
      )
    }

    const sharpIcon = sharp(icon)

    const metadata = await sharpIcon.metadata()

    if (metadata.width !== metadata.height) {
      reporter.warn(
        `The icon(${icon}) you provided to 'gatsby-plugin-manifest' is not square.\n` +
          `The icons we generate will be square and for the best results we recommend you provide a square icon.\n`
      )
    }

    //add cache busting
    const cacheMode =
      typeof pluginOptions.cache_busting_mode !== `undefined`
        ? pluginOptions.cache_busting_mode
        : `query`

    const iconDigest = createContentDigest(fs.readFileSync(icon))

    /**
     * Given an array of icon configs, generate the various output sizes from
     * the source icon image.
     */
    async function processIconSet(iconSet) {
      //if cacheBusting is being done via url query icons must be generated before cache busting runs
      if (cacheMode === `query`) {
        await Promise.all(
          iconSet.map(dstIcon =>
            checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
          )
        )
      }

      if (cacheMode !== `none`) {
        iconSet = iconSet.map(icon => {
          let newIcon = { ...icon }
          newIcon.src = addDigestToPath(icon.src, iconDigest, cacheMode)
          return newIcon
        })
      }

      //if file names are being modified by cacheBusting icons must be generated after cache busting runs
      if (cacheMode !== `query`) {
        await Promise.all(
          iconSet.map(dstIcon =>
            checkCache(cache, dstIcon, icon, iconDigest, generateIcon)
          )
        )
      }

      return iconSet
    }

    manifest.icons = await processIconSet(manifest.icons)

    // If favicon is enabled, apply the same caching policy and generate
    // the resized image(s)
    if (faviconIsEnabled) {
      await processIconSet(favicons)

      if (metadata.format === `svg`) {
        fs.copyFileSync(icon, path.join(`public`, `favicon.svg`))
      }
    }
  }

  //Fix #18497 by prefixing paths
  manifest.icons = manifest.icons.map(icon => {
    return {
      ...icon,
      src: slash(path.join(basePath, icon.src)),
    }
  })

  if (manifest.start_url) {
    manifest.start_url = path.posix.join(basePath, manifest.start_url)
  }

  //Write manifest
  fs.writeFileSync(
    path.join(`public`, `manifest${suffix}.webmanifest`),
    JSON.stringify(manifest)
  )
}

exports.onCreateWebpackConfig = ({ actions, plugins }, pluginOptions) => {
  actions.setWebpackConfig({
    plugins: [
      plugins.define({
        __MANIFEST_PLUGIN_HAS_LOCALISATION__:
          pluginOptions.localize && pluginOptions.localize.length,
      }),
    ],
  })
}
<|endoftext|>


<commit_before>// 06-04 09:00:16.261  1409  1409 D OpenGLRenderer: TextureCache::get:
var logPattern = /(.{18})\s*(\S+)\s*(\S+) (.) (\S+)\s*: (.*)/;
var outputNode;

var parseLine = function(line) {
  var match = logPattern.exec(line);
  if (!match) { return line; }

  var level;
  switch (match[4]) {
    case 'E':
      level = 'error';
      break;
    case 'W':
      level = 'warn';
      break;
    case 'I':
      level = 'info';
      break;
    case 'D':
      level = 'debug';
      break;
    case 'V':
      level = 'verbose';
      break;
    default:
      level = 'unknown'
  }

  return {
    'timestamp': match[1],
    'pid': match[2],
    'tid': match[3],
    'level': level,
    'tag': match[5],
    'message': match[6]
  }
}

var printLog = function(log) {
  return $.el.div({ 'class': 'log ' + log.level },
      $.el.span({ 'class': 'ts' }, log.timestamp),
      $.el.span({ 'class': 'pid' }, log.pid),
      $.el.span({ 'class': 'tid' }, log.tid),
      $.el.span({ 'class': 'tag' }, log.tag),
      $.el.span({ 'class': 'msg' }, log.message)
    );
}

var onmessage = function(e) {
  var log = parseLine(e.data);
  var dom;
  if (typeof log == "string") {
    console.warn("couldn't parse line: " + log);
    dom = $.el.div({ 'class': 'log raw' }, log);
  } else {
    dom = printLog(log);
  }
  dom.appendTo(outputNode);
}

window.onload = function() {
  outputNode = document.getElementById('logcat');
  window.webSocket = new WebSocket("ws://localhost:9000");
  window.webSocket.onmessage = onmessage;
}

<commit_msg>parse timestamp<commit_after>// Log lines look like this
// 06-04 09:00:16.261  1409  1409 D OpenGLRenderer: TextureCache::get:
var logPattern = /(.{18})\s*(\S+)\s*(\S+) (.) (\S+)\s*: (.*)/;
var tsPattern = /(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}).(\d{3})/;
var outputNode;

var parseDate = function(timestamp) {
  var m = tsPattern.exec(timestamp);
  var d = new Date();
  d.setMonth(m[1] - 1);
  d.setDate(m[2]);
  d.setHours(m[3]);
  d.setMinutes(m[4]);
  d.setSeconds(m[5]);
  d.setMilliseconds(m[6]);
  return d.getTime();
}

var parseLine = function(line) {
  var match = logPattern.exec(line);
  if (!match) { return line; }

  var timestamp = parseDate(match[1]);

  var level;
  switch (match[4]) {
    case 'E':
      level = 'error';
      break;
    case 'W':
      level = 'warn';
      break;
    case 'I':
      level = 'info';
      break;
    case 'D':
      level = 'debug';
      break;
    case 'V':
      level = 'verbose';
      break;
    default:
      level = 'unknown'
  }

  return {
    'timestamp': timestamp,
    'pid': match[2],
    'tid': match[3],
    'level': level,
    'tag': match[5],
    'message': match[6]
  }
}

var printLog = function(log) {
  return $.el.div({ 'class': 'log ' + log.level },
      $.el.span({ 'class': 'ts' }, new Date(log.timestamp).toLocaleTimeString()),
      $.el.span({ 'class': 'pid' }, log.pid),
      $.el.span({ 'class': 'tid' }, log.tid),
      $.el.span({ 'class': 'tag' }, log.tag),
      $.el.span({ 'class': 'msg' }, log.message)
    );
}

var onmessage = function(e) {
  var log = parseLine(e.data);
  var dom;
  if (typeof log == "string") {
    console.warn("couldn't parse line: " + log);
    dom = $.el.div({ 'class': 'log raw' }, log);
  } else {
    dom = printLog(log);
  }
  dom.appendTo(outputNode);
}

window.onload = function() {
  outputNode = document.getElementById('logcat');
  window.webSocket = new WebSocket("ws://localhost:9000");
  window.webSocket.onmessage = onmessage;
}

<|endoftext|>


<commit_before>cdb.admin.overlays.Zoom = cdb.core.View.extend({

  tagName: 'div',
  className: 'cartodb-zoom',

  events: {
    'click .zoom_in': 'zoom_in',
    'click .zoom_out': 'zoom_out'
  },

  default_options: {
    timeout: 0,
    msg: ''
  },

  template_name: 'table/views/overlays/zoom',

  initialize: function() {

    _.bindAll(this, "_close");

    this.template = this.getTemplate(this.template_name);

    this.map = this.options.map;

    _.defaults(this.options, this.default_options);

    this._setupModels();

  },

  _killEvent: function(e) {

    e && e.preventDefault();
    e && e.stopPropagation();

  },

  // Setup the internal and custom model
  _setupModels: function() {

    this.model = this.options.model;

    this.model.on("change:display", this._onChangeDisplay, this);
    this.model.on("change:y",       this._onChangeY, this);

    this.map.bind('change:zoom change:minZoom change:maxZoom', this._checkZoom, this);

    this.model.on("destroy", function() {
      this.$el.remove();
    }, this);

  },

  _onChangeY: function() {

    var y = this.model.get("y");
    this.$el.animate({ top: y }, 150);

    this.trigger("change_y", this);

  },

  _onChangeDisplay: function() {

    var display = this.model.get("display");

    if (display) {
      this.show();
    } else {
      this.hide();
    }

  },

  _checkZoom: function() {

    var zoom = this.map.get('zoom');

    this.$('.zoom_in')[ zoom < this.map.get('maxZoom') ? 'removeClass' : 'addClass' ]('disabled')
    this.$('.zoom_out')[ zoom > this.map.get('minZoom') ? 'removeClass' : 'addClass' ]('disabled')

    this.$el.find(".info").html(zoom);

    this._createTooltip(this.$el.find(".info"));
  },

  zoom_in: function(ev) {
    if (this.map.get("maxZoom") > this.map.getZoom()) {
      this.map.setZoom(this.map.getZoom() + 1);
    }
    ev.preventDefault();
    ev.stopPropagation();
  },

  zoom_out: function(ev) {
    if (this.map.get("minZoom") < this.map.getZoom()) {
      this.map.setZoom(this.map.getZoom() - 1);
    }
    ev.preventDefault();
    ev.stopPropagation();
  },

  _onMouseDown:      function() { },
  _onMouseEnterText: function() { },
  _onMouseLeaveText: function() { },
  _onMouseEnter:     function() { },
  _onMouseLeave:     function() { },

  show: function() {

    var self = this;

    this.$el.fadeIn(250, function() {
      self.trigger("change_y", self);
    });

  },

  hide: function(callback) {

    var self = this;

    callback && callback();

    this.$el.fadeOut(250, function() {
      self.trigger("change_y", self);
    });

  },

  _close: function(e) {

    this._killEvent(e);

    var self = this;

    this.hide(function() {
      self.trigger("remove", self);
    });

  },

  _toggleFullScreen: function(ev) {

    ev.stopPropagation();
    ev.preventDefault();

    var doc   = window.document;
    var docEl = doc.documentElement;

    if (this.options.doc) { // we use a custom element
      docEl = $(this.options.doc)[0];
    }

    var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen;
    var cancelFullScreen  = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen;

    var mapView = this.options.mapView;

    if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement) {

      requestFullScreen.call(docEl);

      if (mapView) {

        if (this.model.get("allowWheelOnFullscreen")) {
          mapView.options.map.set("scrollwheel", true);
        }

      }

    } else {

      cancelFullScreen.call(doc);

    }

  },

  render: function() {

    var self = this;

    this.$el.append(this.template());

    this.$el.css({ left: this.model.get("x"), top: this.model.get("y") });

    this._checkZoom();

    return this;

  },

  _createTooltip: function(elem) {
    debugger
    var tooltip = new cdb.common.TipsyTooltip({
      el: elem,
      gravity: 'w',
      title: function() {
        //return _.template(self._TEXTS.limits[type])({ name: ( self._TABS[tab].label || self._TABS[tab].title ) })
        return ("zoom level");
      }
    })
    this.addView(tooltip);
  }

});
<commit_msg>createTooltip inside render<commit_after>cdb.admin.overlays.Zoom = cdb.core.View.extend({

  tagName: 'div',
  className: 'cartodb-zoom',

  events: {
    'click .zoom_in': 'zoom_in',
    'click .zoom_out': 'zoom_out'
  },

  default_options: {
    timeout: 0,
    msg: ''
  },

  template_name: 'table/views/overlays/zoom',

  initialize: function() {

    _.bindAll(this, "_close");

    this.template = this.getTemplate(this.template_name);

    this.map = this.options.map;

    _.defaults(this.options, this.default_options);

    this._setupModels();

  },

  _killEvent: function(e) {

    e && e.preventDefault();
    e && e.stopPropagation();

  },

  // Setup the internal and custom model
  _setupModels: function() {

    this.model = this.options.model;

    this.model.on("change:display", this._onChangeDisplay, this);
    this.model.on("change:y",       this._onChangeY, this);

    this.map.bind('change:zoom change:minZoom change:maxZoom', this._checkZoom, this);

    this.model.on("destroy", function() {
      this.$el.remove();
    }, this);

  },

  _onChangeY: function() {

    var y = this.model.get("y");
    this.$el.animate({ top: y }, 150);

    this.trigger("change_y", this);

  },

  _onChangeDisplay: function() {

    var display = this.model.get("display");

    if (display) {
      this.show();
    } else {
      this.hide();
    }

  },

  _checkZoom: function() {

    var zoom = this.map.get('zoom');

    this.$('.zoom_in')[ zoom < this.map.get('maxZoom') ? 'removeClass' : 'addClass' ]('disabled')
    this.$('.zoom_out')[ zoom > this.map.get('minZoom') ? 'removeClass' : 'addClass' ]('disabled')

    this.$el.find(".info").html(zoom);

  },

  zoom_in: function(ev) {
    if (this.map.get("maxZoom") > this.map.getZoom()) {
      this.map.setZoom(this.map.getZoom() + 1);
    }
    ev.preventDefault();
    ev.stopPropagation();
  },

  zoom_out: function(ev) {
    if (this.map.get("minZoom") < this.map.getZoom()) {
      this.map.setZoom(this.map.getZoom() - 1);
    }
    ev.preventDefault();
    ev.stopPropagation();
  },

  _onMouseDown:      function() { },
  _onMouseEnterText: function() { },
  _onMouseLeaveText: function() { },
  _onMouseEnter:     function() { },
  _onMouseLeave:     function() { },

  show: function() {

    var self = this;

    this.$el.fadeIn(250, function() {
      self.trigger("change_y", self);
    });

  },

  hide: function(callback) {

    var self = this;

    callback && callback();

    this.$el.fadeOut(250, function() {
      self.trigger("change_y", self);
    });

  },

  _close: function(e) {

    this._killEvent(e);

    var self = this;

    this.hide(function() {
      self.trigger("remove", self);
    });

  },

  _toggleFullScreen: function(ev) {

    ev.stopPropagation();
    ev.preventDefault();

    var doc   = window.document;
    var docEl = doc.documentElement;

    if (this.options.doc) { // we use a custom element
      docEl = $(this.options.doc)[0];
    }

    var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen;
    var cancelFullScreen  = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen;

    var mapView = this.options.mapView;

    if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement) {

      requestFullScreen.call(docEl);

      if (mapView) {

        if (this.model.get("allowWheelOnFullscreen")) {
          mapView.options.map.set("scrollwheel", true);
        }

      }

    } else {

      cancelFullScreen.call(doc);

    }

  },

  render: function() {

    var self = this;

    this.$el.append(this.template());

    this.$el.css({ left: this.model.get("x"), top: this.model.get("y") });

    this._checkZoom();

    this._createTooltip();

    return this;

  },

  _createTooltip: function(elem) {

    var tooltip = new cdb.common.TipsyTooltip({
      el: this.$('.info'),
      gravity: 'w',
      title: function() {
        return ("zoom level");
      }
    })

    this.addView(tooltip);
  }

});
<|endoftext|>


<commit_before>"use strict";
/* jshint node: true */
// Log class

var config    = require('./config');
var plugins;
var connection;
var outbound;
var constants = require('./constants');
var util      = require('util');
var tty       = require('tty');

var logger = exports;

logger.LOGDATA      = 9;
logger.LOGPROTOCOL  = 8;
logger.LOGDEBUG     = 7;
logger.LOGINFO      = 6;
logger.LOGNOTICE    = 5;
logger.LOGWARN      = 4;
logger.LOGERROR     = 3;
logger.LOGCRIT      = 2;
logger.LOGALERT     = 1;
logger.LOGEMERG     = 0;

logger.colors = { // Makes me cringe spelling it this way...
    "DATA" : "green",
    "PROTOCOL" : "green",
    "DEBUG" : "grey",
    "INFO" : "cyan",
    "info" : "cyan",
    "NOTICE" : "blue",
    "WARN" : "red",
    "ERROR" : "red",
    "CRIT" : "red",
    "ALERT" : "red",
    "EMERG" : "red",
};

var stdout_is_tty = tty.isatty(process.stdout.fd);

function colorize (color, str) {
    if (!util.inspect.colors) { return str; }  // node util before Nov 2013
    if (!util.inspect.colors[color]) { return str; }  // unknown color
    return '\u001b[' + util.inspect.colors[color][0] + 'm' + str +
           '\u001b[' + util.inspect.colors[color][1] + 'm';
}

var loglevel = logger.LOGWARN;

var deferred_logs = [];

logger.dump_logs = function (exit) {
    while (deferred_logs.length > 0) {
        var log_item = deferred_logs.shift();
        var color = logger.colors[log_item.level];
        if (color && stdout_is_tty) {
            console.log(colorize(color,log_item.data));
        }
        else {
            console.log(log_item.data);
        }
    }
    if (exit) {
        process.exit(1);
    }
};

logger.log = function (level, data) {
    if (level === 'PROTOCOL') {
        data = data.replace(/\n/g, '\\n\n');
    }
    data = data.replace(/\r/g, '\\r')
               .replace(/\n$/, '');
    // todo - just buffer these up (defer) until plugins are loaded
    if (plugins && plugins.plugin_list) {
        while (deferred_logs.length > 0) {
            var log_item = deferred_logs.shift();
            plugins.run_hooks('log', logger, log_item);
        }
        plugins.run_hooks('log', logger, {
            'level' : level,
            'data'  : data
        });
    }
    else {
        deferred_logs.push({
            'level' : level,
            'data'  : data
        });
    }
};

logger.log_respond = function (retval, msg, data) {
    // any other return code is irrelevant
    if (retval === constants.cont) {
        var color = logger.colors[data.level];
        if (color && stdout_is_tty) {
            return console.log(colorize(color,data.data));
        }
        else {
            return console.log(data.data);
        }
    }
};

logger._init_loglevel = function () {
    var self = this;
    var _loglevel = config.get('loglevel', 'value', function () {
        self._init_loglevel();
    });
    if (_loglevel) {
        var loglevel_num = parseInt(_loglevel);
        if (!loglevel_num || loglevel_num === NaN) {
            this.log('info', 'loglevel: ' + _loglevel.toUpperCase());
            loglevel = logger[_loglevel.toUpperCase()];
        }
        else {
            loglevel = loglevel_num;
        }
        if (!loglevel) {
            loglevel = logger.LOGWARN;
        }
    }
};

logger.would_log = function (level) {
    if (loglevel < level) return false;
    return true;
};

var original_console_log = console.log;

logger._init_timestamps = function () {
    var self = this;
    var _timestamps = config.get('log_timestamps', 'value', function () {
        self._init_timestamps();
    });
    if (_timestamps) {
        console.log = original_console_log.bind(console, new Date().toISOString());
    }
    else {
        console.log = original_console_log;
    }
};

logger._init_loglevel();
logger._init_timestamps();

var level, key;
for (key in logger) {
    if(!logger.hasOwnProperty(key)) { continue; }

    if (key.match(/^LOG\w/)) {
        level = key.slice(3);
        logger[key.toLowerCase()] = (function(level, key) {
            return function() {
                if (loglevel < logger[key]) { return; }
                var levelstr = "[" + level + "]";
                var str = "";
                var uuidstr = "[-]";
                var pluginstr = "[core]";
                for (var i = 0; i < arguments.length; i++) {
                    var data = arguments[i];
                    if (typeof(data) === 'object') {
                        // if the object is a connection, we wish to add
                        // the connection id
                        if (data instanceof connection.Connection) {
                            uuidstr = "[" + data.uuid;
                            if (data.tran_count > 0) {
                                uuidstr += "." + data.tran_count;
                            }
                            uuidstr += "]";
                        }
                        else if (data instanceof plugins.Plugin) {
                            pluginstr = "[" + data.name + "]";
                        }
                        else if (data instanceof outbound.HMailItem) {
                            pluginstr = "[outbound]";
                            if (data.todo && data.todo.uuid) {
                                uuidstr = "[" + data.todo.uuid + "]";
                            }
                        }
                        else {
                            str += util.inspect(data);
                        }
                    }
                    else {
                        str += data;
                    }
                }
                logger.log(level, [levelstr, uuidstr, pluginstr, str].join(" "));
            };
        })(level, key);
    }
}

// load these down here so it sees all the logger methods compiled above
plugins = require('./plugins');
connection = require('./connection');
outbound = require('./outbound');
<commit_msg>logger, 3 changes to improve testability<commit_after>"use strict";
/* jshint node: true */
// Log class

var config    = require('./config');
var plugins;
var connection;
var outbound;
var constants = require('./constants');
var util      = require('util');
var tty       = require('tty');

var logger = exports;

logger.LOGDATA      = 9;
logger.LOGPROTOCOL  = 8;
logger.LOGDEBUG     = 7;
logger.LOGINFO      = 6;
logger.LOGNOTICE    = 5;
logger.LOGWARN      = 4;
logger.LOGERROR     = 3;
logger.LOGCRIT      = 2;
logger.LOGALERT     = 1;
logger.LOGEMERG     = 0;

logger.loglevel     = logger.LOGWARN;
logger.deferred_logs = [];

logger.colors = { // Makes me cringe spelling it this way...
    "DATA" : "green",
    "PROTOCOL" : "green",
    "DEBUG" : "grey",
    "INFO" : "cyan",
    "NOTICE" : "blue",
    "WARN" : "red",
    "ERROR" : "red",
    "CRIT" : "red",
    "ALERT" : "red",
    "EMERG" : "red",
};

var stdout_is_tty = tty.isatty(process.stdout.fd);

logger.colorize = function (color, str) {
    if (!util.inspect.colors) { return str; }  // node util before Nov 2013
    if (!util.inspect.colors[color]) { return str; }  // unknown color
    return '\u001b[' + util.inspect.colors[color][0] + 'm' + str +
           '\u001b[' + util.inspect.colors[color][1] + 'm';
};

var loglevel = logger.LOGWARN;

var deferred_logs = [];

logger.dump_logs = function (exit) {
    while (logger.deferred_logs.length > 0) {
        var log_item = logger.deferred_logs.shift();
        var color = logger.colors[log_item.level];
        if (color && stdout_is_tty) {
            console.log(logger.colorize(color,log_item.data));
        }
        else {
            console.log(log_item.data);
        }
    }
    if (exit) {
        process.exit(1);
    }
    return true;
};

logger.log = function (level, data) {
    if (level === 'PROTOCOL') {
        data = data.replace(/\n/g, '\\n\n');
    }
    data = data.replace(/\r/g, '\\r')
               .replace(/\n$/, '');

    var item = { 'level' : level, 'data'  : data };

    // buffer until plugins are loaded
    if (!plugins || !plugins.plugin_list) {
        logger.deferred_logs.push( item );
        return true;
    }

    // process buffered logs
    while (logger.deferred_logs.length > 0) {
        var log_item = logger.deferred_logs.shift();
        plugins.run_hooks('log', logger, log_item);
    }

    plugins.run_hooks('log', logger, item );
    return true;
};

logger.log_respond = function (retval, msg, data) {
    // any other return code is irrelevant
    if (retval !== constants.cont) { return false; }

    var color = logger.colors[data.level];
    if (color && stdout_is_tty) {
        console.log(logger.colorize(color,data.data));
        return true;
    }

    console.log(data.data);
    return true;
};

logger._init_loglevel = function () {
    var self = this;
    var _loglevel = config.get('loglevel', 'value', function () {
        self._init_loglevel();
    });
    if (_loglevel) {
        var loglevel_num = parseInt(_loglevel);
        if (!loglevel_num || isNaN(loglevel_num)) {
            this.log('INFO', 'loglevel: ' + _loglevel.toUpperCase());
            logger.loglevel = logger[_loglevel.toUpperCase()];
        }
        else {
            logger.loglevel = loglevel_num;
        }
        if (!logger.loglevel) {
            logger.loglevel = logger.LOGWARN;
        }
    }
};

logger.would_log = function (level) {
    if (logger.loglevel < level) { return false; }
    return true;
};

var original_console_log = console.log;

logger._init_timestamps = function () {
    var self = this;
    var _timestamps = config.get('log_timestamps', 'value', function () {
        self._init_timestamps();
    });
    if (_timestamps) {
        console.log = original_console_log.bind(console, new Date().toISOString());
    }
    else {
        console.log = original_console_log;
    }
};

logger._init_loglevel();
logger._init_timestamps();

var level, key;
for (key in logger) {
    if(!logger.hasOwnProperty(key)) { continue; }

    if (key.match(/^LOG\w/)) {
        level = key.slice(3);
        logger[key.toLowerCase()] = (function(level, key) {
            return function() {
                if (logger.loglevel < logger[key]) { return; }
                var levelstr = "[" + level + "]";
                var str = "";
                var uuidstr = "[-]";
                var pluginstr = "[core]";
                for (var i = 0; i < arguments.length; i++) {
                    var data = arguments[i];
                    if (typeof(data) === 'object') {
                        // if the object is a connection, we wish to add
                        // the connection id
                        if (data instanceof connection.Connection) {
                            uuidstr = "[" + data.uuid;
                            if (data.tran_count > 0) {
                                uuidstr += "." + data.tran_count;
                            }
                            uuidstr += "]";
                        }
                        else if (data instanceof plugins.Plugin) {
                            pluginstr = "[" + data.name + "]";
                        }
                        else if (data instanceof outbound.HMailItem) {
                            pluginstr = "[outbound]";
                            if (data.todo && data.todo.uuid) {
                                uuidstr = "[" + data.todo.uuid + "]";
                            }
                        }
                        else {
                            str += util.inspect(data);
                        }
                    }
                    else {
                        str += data;
                    }
                }
                logger.log(level, [levelstr, uuidstr, pluginstr, str].join(" "));
            };
        })(level, key);
    }
}

// load these down here so it sees all the logger methods compiled above
plugins = require('./plugins');
connection = require('./connection');
outbound = require('./outbound');
<|endoftext|>