<commit_before>// # Article Editor

/*global window, document, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable, JST */
(function () {
    "use strict";

    var PublishBar,
        TagWidget,
        ActionsWidget,
        MarkdownShortcuts = [
            {'key': 'Ctrl+B', 'style': 'bold'},
            {'key': 'Meta+B', 'style': 'bold'},
            {'key': 'Ctrl+I', 'style': 'italic'},
            {'key': 'Meta+I', 'style': 'italic'},
            {'key': 'Ctrl+Alt+U', 'style': 'strike'},
            {'key': 'Ctrl+Shift+K', 'style': 'code'},
            {'key': 'Meta+K', 'style': 'code'},
            {'key': 'Ctrl+Alt+1', 'style': 'h1'},
            {'key': 'Ctrl+Alt+2', 'style': 'h2'},
            {'key': 'Ctrl+Alt+3', 'style': 'h3'},
            {'key': 'Ctrl+Alt+4', 'style': 'h4'},
            {'key': 'Ctrl+Alt+5', 'style': 'h5'},
            {'key': 'Ctrl+Alt+6', 'style': 'h6'},
            {'key': 'Ctrl+Shift+L', 'style': 'link'},
            {'key': 'Ctrl+Shift+I', 'style': 'image'},
            {'key': 'Ctrl+Q', 'style': 'blockquote'},
            {'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
            {'key': 'Ctrl+U', 'style': 'uppercase'},
            {'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
            {'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
            {'key': 'Ctrl+Alt+W', 'style': 'selectword'},
            {'key': 'Ctrl+L', 'style': 'list'},
            {'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
            {'key': 'Meta+Alt+C', 'style': 'copyHTML'}
        ];

    // The publish bar associated with a post, which has the TagWidget and
    // Save button and options and such.
    // ----------------------------------------
    PublishBar = Ghost.View.extend({

        initialize: function () {
            this.addSubview(new TagWidget({el: this.$('#entry-categories'), model: this.model})).render();
            this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
        },

        render: function () { return this; }

    });

    // The Tag UI area associated with a post
    // ----------------------------------------
    TagWidget = Ghost.View.extend({
        render: function () { return this; }
    });

    // The Publish, Queue, Publish Now buttons
    // ----------------------------------------
    ActionsWidget = Ghost.View.extend({

        events: {
            'click [data-set-status]': 'handleStatus',
            'click .js-post-button': 'updatePost'
        },

        statusMap: {
            'draft': 'Save Draft',
            'published': 'Publish Now',
            'scheduled': 'Save Schedued Post',
            'queue': 'Add to Queue',
            'publish-on': 'Publish on...'
        },

        initialize: function () {
            var self = this;
            // Toggle publish
            shortcut.add("Ctrl+Alt+P", function () {
                self.toggleStatus();
            });
            shortcut.add("Ctrl+S", function () {
                self.updatePost();
            });
            shortcut.add("Meta+S", function () {
                self.updatePost();
            });
            this.listenTo(this.model, 'change:status', this.render);
            this.model.on('change:id', function (m) {
                Backbone.history.navigate('/editor/' + m.id);
            });
        },

        toggleStatus: function () {
            var self = this,
                keys = Object.keys(this.statusMap),
                model = this.model,
                prevStatus = this.model.get('status'),
                currentIndex = keys.indexOf(prevStatus),
                newIndex;

            if (keys[currentIndex + 1] === 'scheduled') { // TODO: Remove once scheduled posts work
                newIndex = currentIndex + 2 > keys.length - 1 ? 0 : currentIndex + 1;
            } else {
                newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
            }

            this.savePost({
                status: keys[newIndex]
            }).then(function () {
                Ghost.notifications.addItem({
                    type: 'success',
                    message: 'Your post: ' + model.get('title') + ' has been ' + keys[newIndex],
                    status: 'passive'
                });
            }, function (xhr) {
                var status = keys[newIndex];
                // Show a notification about the error
                self.reportSaveError(xhr, model, status);
                // Set the button text back to previous
                model.set({ status: prevStatus });
            });
        },

        setActiveStatus: function setActiveStatus(status, displayText) {
            // Set the publish button's action
            $('.js-post-button')
                .attr('data-status', status)
                .text(displayText);

            // Set the active action in the popup
            $('.splitbutton-save .editor-options li')
                .removeClass('active')
                .filter(['li[data-set-status="', status, '"]'].join(''))
                    .addClass('active');
        },

        handleStatus: function (e) {
            if (e) { e.preventDefault(); }
            var status = $(e.currentTarget).attr('data-set-status');

            this.setActiveStatus(status, this.statusMap[status]);

            // Dismiss the popup menu
            $('body').find('.overlay:visible').fadeOut();
        },

        updatePost: function (e) {
            if (e) { e.preventDefault(); }
            var self = this,
                model = this.model,
                $currentTarget = $(e.currentTarget),
                status = $currentTarget.attr('data-status'),
                prevStatus = model.get('status');

            if (status === 'publish-on') {
                return Ghost.notifications.addItem({
                    type: 'alert',
                    message: 'Scheduled publishing not supported yet.',
                    status: 'passive'
                });
            }
            if (status === 'queue') {
                return Ghost.notifications.addItem({
                    type: 'alert',
                    message: 'Scheduled publishing not supported yet.',
                    status: 'passive'
                });
            }

            this.savePost({
                status: status
            }).then(function () {
                Ghost.notifications.addItem({
                    type: 'success',
                    message: ['Your post "', model.get('title'), '" has been ', status, '.'].join(''),
                    status: 'passive'
                });
            }, function (xhr) {
                // Show a notification about the error
                self.reportSaveError(xhr, model, status);
                // Set the button text back to previous
                model.set({ status: prevStatus });
            });
        },

        savePost: function (data) {
            // TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
            _.each(this.model.blacklist, function (item) {
                this.model.unset(item);
            }, this);

            var saved = this.model.save(_.extend({
                title: $('#entry-title').val(),
                content_raw: Ghost.currentView.editor.getValue()
            }, data));

            // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
            // ourselves for more consistent promises.
            if (saved) {
                return saved;
            }
            return $.Deferred().reject();
        },

        reportSaveError: function (response, model, status) {
            var title = model.get('title') || '[Untitled]',
                message = 'Your post: ' + title + ' has not been ' + status;

            if (response) {
                // Get message from response
                message = Ghost.Views.Utils.getRequestErrorMessage(response);
            } else if (model.validationError) {
                // Grab a validation error
                message += "; " + model.validationError;
            }

            Ghost.notifications.addItem({
                type: 'error',
                message: message,
                status: 'passive'
            });
        },

        render: function () {
            this.$('.js-post-button').text(this.statusMap[this.model.get('status')]);
        }

    });

    // The entire /editor page's route (TODO: move all views to client side templates)
    // ----------------------------------------
    Ghost.Views.Editor = Ghost.View.extend({

        initialize: function () {

            // Add the container view for the Publish Bar
            this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();

            this.$('#entry-markdown').html(this.model.get('content_raw'));

            this.initMarkdown();
            this.renderPreview();

            $('.entry-content header, .entry-preview header').on('click', function () {
                $('.entry-content, .entry-preview').removeClass('active');
                $(this).closest('section').addClass('active');
            });

            $('.entry-title .icon-fullscreen').on('click', function (e) {
                e.preventDefault();
                $('body').toggleClass('fullscreen');
            });

            $('.options.up').on('click', function (e) {
                e.stopPropagation();
                $(this).next("ul").fadeToggle(200);
            });

            this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);

            // Shadow on Markdown if scrolled
            this.$('.CodeMirror-scroll').on('scroll', function (e) {
                if ($('.CodeMirror-scroll').scrollTop() > 10) {
                    $('.entry-markdown').addClass('scrolling');
                } else {
                    $('.entry-markdown').removeClass('scrolling');
                }
            });

            // Shadow on Preview if scrolled
            this.$('.entry-preview-content').on('scroll', function (e) {
                if ($('.entry-preview-content').scrollTop() > 10) {
                    $('.entry-preview').addClass('scrolling');
                } else {
                    $('.entry-preview').removeClass('scrolling');
                }
            });

            // Zen writing mode shortcut
            shortcut.add("Alt+Shift+Z", function () {
                $('body').toggleClass('zen');
            });

            $('.entry-markdown header, .entry-preview header').click(function (e) {
                $('.entry-markdown, .entry-preview').removeClass('active');
                $(e.target).closest('section').addClass('active');
            });

        },

        events: {
            'click .markdown-help': 'showHelp'
        },

        syncScroll: _.debounce(function (e) {
            var $codeViewport = $(e.target),
                $previewViewport = $('.entry-preview-content'),
                $codeContent = $('.CodeMirror-sizer'),
                $previewContent = $('.rendered-markdown'),

                // calc position
                codeHeight = $codeContent.height() - $codeViewport.height(),
                previewHeight = $previewContent.height() - $previewViewport.height(),
                ratio = previewHeight / codeHeight,
                previewPostition = $codeViewport.scrollTop() * ratio;

            // apply new scroll
            $previewViewport.scrollTop(previewPostition);
        }, 50),

        showHelp: function () {
            this.addSubview(new Ghost.Views.Modal({
                model: {
                    options: {
                        close: true,
                        type: "info",
                        style: "wide",
                        animation: 'fade'
                    },
                    content: {
                        template: 'markdown',
                        title: 'Markdown Help'
                    }
                }
            }));
        },

        // This updates the editor preview panel.
        // Currently gets called on every key press.
        // Also trigger word count update
        renderPreview: function () {
            var self = this,
                preview = document.getElementsByClassName('rendered-markdown')[0];
            preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
            this.$('.js-drop-zone').upload({editor: true});
            Countable.once(preview, function (counter) {
                self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
                self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
                self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
            });
        },

        // Markdown converter & markdown shortcut initialization.
        initMarkdown: function () {
            var self = this;

            this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
            this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
                mode: 'gfm',
                tabMode: 'indent',
                tabindex: "2",
                lineWrapping: true,
                dragDrop: false
            });

            // Inject modal for HTML to be viewed in
            shortcut.add("Ctrl+Alt+C", function () {
                self.showHTML();
            });
            shortcut.add("Ctrl+Alt+C", function () {
                self.showHTML();
            });

            _.each(MarkdownShortcuts, function (combo) {
                shortcut.add(combo.key, function () {
                    return self.editor.addMarkdown({style: combo.style});
                });
            });

            this.editor.on('change', function () {
                self.renderPreview();
            });
        },

        showHTML: function () {
            this.addSubview(new Ghost.Views.Modal({
                model: {
                    options: {
                        close: true,
                        type: "info",
                        style: "wide",
                        animation: 'fade'
                    },
                    content: {
                        template: 'copyToHTML',
                        title: 'Copied HTML'
                    }
                }
            }));
        },

        render: function () { return this; }
    });

}());
<commit_msg>Strip whitespace on titles<commit_after>// # Article Editor

/*global window, document, $, _, Backbone, Ghost, Showdown, CodeMirror, shortcut, Countable, JST */
(function () {
    "use strict";

    var PublishBar,
        TagWidget,
        ActionsWidget,
        MarkdownShortcuts = [
            {'key': 'Ctrl+B', 'style': 'bold'},
            {'key': 'Meta+B', 'style': 'bold'},
            {'key': 'Ctrl+I', 'style': 'italic'},
            {'key': 'Meta+I', 'style': 'italic'},
            {'key': 'Ctrl+Alt+U', 'style': 'strike'},
            {'key': 'Ctrl+Shift+K', 'style': 'code'},
            {'key': 'Meta+K', 'style': 'code'},
            {'key': 'Ctrl+Alt+1', 'style': 'h1'},
            {'key': 'Ctrl+Alt+2', 'style': 'h2'},
            {'key': 'Ctrl+Alt+3', 'style': 'h3'},
            {'key': 'Ctrl+Alt+4', 'style': 'h4'},
            {'key': 'Ctrl+Alt+5', 'style': 'h5'},
            {'key': 'Ctrl+Alt+6', 'style': 'h6'},
            {'key': 'Ctrl+Shift+L', 'style': 'link'},
            {'key': 'Ctrl+Shift+I', 'style': 'image'},
            {'key': 'Ctrl+Q', 'style': 'blockquote'},
            {'key': 'Ctrl+Shift+1', 'style': 'currentDate'},
            {'key': 'Ctrl+U', 'style': 'uppercase'},
            {'key': 'Ctrl+Shift+U', 'style': 'lowercase'},
            {'key': 'Ctrl+Alt+Shift+U', 'style': 'titlecase'},
            {'key': 'Ctrl+Alt+W', 'style': 'selectword'},
            {'key': 'Ctrl+L', 'style': 'list'},
            {'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
            {'key': 'Meta+Alt+C', 'style': 'copyHTML'}
        ];

    // The publish bar associated with a post, which has the TagWidget and
    // Save button and options and such.
    // ----------------------------------------
    PublishBar = Ghost.View.extend({

        initialize: function () {
            this.addSubview(new TagWidget({el: this.$('#entry-categories'), model: this.model})).render();
            this.addSubview(new ActionsWidget({el: this.$('#entry-actions'), model: this.model})).render();
        },

        render: function () { return this; }

    });

    // The Tag UI area associated with a post
    // ----------------------------------------
    TagWidget = Ghost.View.extend({
        render: function () { return this; }
    });

    // The Publish, Queue, Publish Now buttons
    // ----------------------------------------
    ActionsWidget = Ghost.View.extend({

        events: {
            'click [data-set-status]': 'handleStatus',
            'click .js-post-button': 'updatePost'
        },

        statusMap: {
            'draft': 'Save Draft',
            'published': 'Publish Now',
            'scheduled': 'Save Schedued Post',
            'queue': 'Add to Queue',
            'publish-on': 'Publish on...'
        },

        initialize: function () {
            var self = this;
            // Toggle publish
            shortcut.add("Ctrl+Alt+P", function () {
                self.toggleStatus();
            });
            shortcut.add("Ctrl+S", function () {
                self.updatePost();
            });
            shortcut.add("Meta+S", function () {
                self.updatePost();
            });
            this.listenTo(this.model, 'change:status', this.render);
            this.model.on('change:id', function (m) {
                Backbone.history.navigate('/editor/' + m.id);
            });
        },

        toggleStatus: function () {
            var self = this,
                keys = Object.keys(this.statusMap),
                model = this.model,
                prevStatus = this.model.get('status'),
                currentIndex = keys.indexOf(prevStatus),
                newIndex;

            if (keys[currentIndex + 1] === 'scheduled') { // TODO: Remove once scheduled posts work
                newIndex = currentIndex + 2 > keys.length - 1 ? 0 : currentIndex + 1;
            } else {
                newIndex = currentIndex + 1 > keys.length - 1 ? 0 : currentIndex + 1;
            }

            this.savePost({
                status: keys[newIndex]
            }).then(function () {
                Ghost.notifications.addItem({
                    type: 'success',
                    message: 'Your post: ' + model.get('title') + ' has been ' + keys[newIndex],
                    status: 'passive'
                });
            }, function (xhr) {
                var status = keys[newIndex];
                // Show a notification about the error
                self.reportSaveError(xhr, model, status);
                // Set the button text back to previous
                model.set({ status: prevStatus });
            });
        },

        setActiveStatus: function setActiveStatus(status, displayText) {
            // Set the publish button's action
            $('.js-post-button')
                .attr('data-status', status)
                .text(displayText);

            // Set the active action in the popup
            $('.splitbutton-save .editor-options li')
                .removeClass('active')
                .filter(['li[data-set-status="', status, '"]'].join(''))
                    .addClass('active');
        },

        handleStatus: function (e) {
            if (e) { e.preventDefault(); }
            var status = $(e.currentTarget).attr('data-set-status');

            this.setActiveStatus(status, this.statusMap[status]);

            // Dismiss the popup menu
            $('body').find('.overlay:visible').fadeOut();
        },

        updatePost: function (e) {
            if (e) { e.preventDefault(); }
            var self = this,
                model = this.model,
                $currentTarget = $(e.currentTarget),
                status = $currentTarget.attr('data-status'),
                prevStatus = model.get('status');

            if (status === 'publish-on') {
                return Ghost.notifications.addItem({
                    type: 'alert',
                    message: 'Scheduled publishing not supported yet.',
                    status: 'passive'
                });
            }
            if (status === 'queue') {
                return Ghost.notifications.addItem({
                    type: 'alert',
                    message: 'Scheduled publishing not supported yet.',
                    status: 'passive'
                });
            }

            this.savePost({
                status: status
            }).then(function () {
                Ghost.notifications.addItem({
                    type: 'success',
                    message: ['Your post "', model.get('title'), '" has been ', status, '.'].join(''),
                    status: 'passive'
                });
            }, function (xhr) {
                // Show a notification about the error
                self.reportSaveError(xhr, model, status);
                // Set the button text back to previous
                model.set({ status: prevStatus });
            });
        },

        savePost: function (data) {
            // TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
            _.each(this.model.blacklist, function (item) {
                this.model.unset(item);
            }, this);

            var saved = this.model.save(_.extend({
                title: $('#entry-title').val(),
                content_raw: Ghost.currentView.editor.getValue()
            }, data));

            // TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
            // ourselves for more consistent promises.
            if (saved) {
                return saved;
            }
            return $.Deferred().reject();
        },

        reportSaveError: function (response, model, status) {
            var title = model.get('title') || '[Untitled]',
                message = 'Your post: ' + title + ' has not been ' + status;

            if (response) {
                // Get message from response
                message = Ghost.Views.Utils.getRequestErrorMessage(response);
            } else if (model.validationError) {
                // Grab a validation error
                message += "; " + model.validationError;
            }

            Ghost.notifications.addItem({
                type: 'error',
                message: message,
                status: 'passive'
            });
        },

        render: function () {
            this.$('.js-post-button').text(this.statusMap[this.model.get('status')]);
        }

    });

    // The entire /editor page's route (TODO: move all views to client side templates)
    // ----------------------------------------
    Ghost.Views.Editor = Ghost.View.extend({

        initialize: function () {

            // Add the container view for the Publish Bar
            this.addSubview(new PublishBar({el: "#publish-bar", model: this.model})).render();

            this.$('#entry-markdown').html(this.model.get('content_raw'));

            this.initMarkdown();
            this.renderPreview();

            $('.entry-content header, .entry-preview header').on('click', function () {
                $('.entry-content, .entry-preview').removeClass('active');
                $(this).closest('section').addClass('active');
            });

            $('.entry-title .icon-fullscreen').on('click', function (e) {
                e.preventDefault();
                $('body').toggleClass('fullscreen');
            });

            $('.options.up').on('click', function (e) {
                e.stopPropagation();
                $(this).next("ul").fadeToggle(200);
            });

            this.$('.CodeMirror-scroll').on('scroll', this.syncScroll);

            // Shadow on Markdown if scrolled
            this.$('.CodeMirror-scroll').on('scroll', function (e) {
                if ($('.CodeMirror-scroll').scrollTop() > 10) {
                    $('.entry-markdown').addClass('scrolling');
                } else {
                    $('.entry-markdown').removeClass('scrolling');
                }
            });

            // Shadow on Preview if scrolled
            this.$('.entry-preview-content').on('scroll', function (e) {
                if ($('.entry-preview-content').scrollTop() > 10) {
                    $('.entry-preview').addClass('scrolling');
                } else {
                    $('.entry-preview').removeClass('scrolling');
                }
            });

            // Zen writing mode shortcut
            shortcut.add("Alt+Shift+Z", function () {
                $('body').toggleClass('zen');
            });

            $('.entry-markdown header, .entry-preview header').click(function (e) {
                $('.entry-markdown, .entry-preview').removeClass('active');
                $(e.target).closest('section').addClass('active');
            });

        },

        events: {
            'click .markdown-help': 'showHelp',
            'blur #entry-title': 'trimTitle'
        },

        syncScroll: _.debounce(function (e) {
            var $codeViewport = $(e.target),
                $previewViewport = $('.entry-preview-content'),
                $codeContent = $('.CodeMirror-sizer'),
                $previewContent = $('.rendered-markdown'),

                // calc position
                codeHeight = $codeContent.height() - $codeViewport.height(),
                previewHeight = $previewContent.height() - $previewViewport.height(),
                ratio = previewHeight / codeHeight,
                previewPostition = $codeViewport.scrollTop() * ratio;

            // apply new scroll
            $previewViewport.scrollTop(previewPostition);
        }, 50),

        showHelp: function () {
            this.addSubview(new Ghost.Views.Modal({
                model: {
                    options: {
                        close: true,
                        type: "info",
                        style: "wide",
                        animation: 'fade'
                    },
                    content: {
                        template: 'markdown',
                        title: 'Markdown Help'
                    }
                }
            }));
        },

        trimTitle: function () {
            var $title = $('#entry-title'),
                rawTitle = $title.val(),
                trimmedTitle = $.trim(rawTitle);

            if (rawTitle !== trimmedTitle) {
                $title.val(trimmedTitle);
            }
        },

        // This updates the editor preview panel.
        // Currently gets called on every key press.
        // Also trigger word count update
        renderPreview: function () {
            var self = this,
                preview = document.getElementsByClassName('rendered-markdown')[0];
            preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
            this.$('.js-drop-zone').upload({editor: true});
            Countable.once(preview, function (counter) {
                self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
                self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
                self.$('.entry-paragraph-count').text($.pluralize(counter.paragraphs, 'paragraph'));
            });
        },

        // Markdown converter & markdown shortcut initialization.
        initMarkdown: function () {
            var self = this;

            this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
            this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
                mode: 'gfm',
                tabMode: 'indent',
                tabindex: "2",
                lineWrapping: true,
                dragDrop: false
            });

            // Inject modal for HTML to be viewed in
            shortcut.add("Ctrl+Alt+C", function () {
                self.showHTML();
            });
            shortcut.add("Ctrl+Alt+C", function () {
                self.showHTML();
            });

            _.each(MarkdownShortcuts, function (combo) {
                shortcut.add(combo.key, function () {
                    return self.editor.addMarkdown({style: combo.style});
                });
            });

            this.editor.on('change', function () {
                self.renderPreview();
            });
        },

        showHTML: function () {
            this.addSubview(new Ghost.Views.Modal({
                model: {
                    options: {
                        close: true,
                        type: "info",
                        style: "wide",
                        animation: 'fade'
                    },
                    content: {
                        template: 'copyToHTML',
                        title: 'Copied HTML'
                    }
                }
            }));
        },

        render: function () { return this; }
    });

}());
<|endoftext|>


<commit_before>/**
 * Copyright reelyActive 2015
 * We believe in an open Internet of Things
 */

var localname = require("../../../../../lib/ble/data/gap/servicedata.js");
var assert = require ('assert');

// Constants for the scenario
var CURSOR = 0;
var ADVERTISER_DATA = {};

// Inputs for the scenario
var INPUT_DATA = '';

// Expected outputs for the scenario
var EXPECTED_DATA ={
	uuid:'';,
	data:'';
};

describe('ble data servicedata', function() {

  // Test the process function
  it('should parse BLE advertiser data service data', function() {
  	process(INPUT_DATA_APPLE, CURSOR, ADVERTISER_DATA);
    assert.deepEqual(ADVERTISER_DATA.serviceData, EXPECTED_DATA);
  });

});
<commit_msg>adding tests<commit_after>/**
 * Copyright reelyActive 2015
 * We believe in an open Internet of Things
 */

var servicedata = require("../../../../../lib/ble/data/gap/servicedata.js").process;
var assert = require ('assert');

// Constants for the scenario
var CURSOR = 0;
var ADVERTISER_DATA = {};

// Inputs for the scenario
var INPUT_DATA = '421655daba50e1fe0201050c097265656c794163746976650100';

// Expected outputs for the scenario
var EXPECTED_DATA ={
	uuid:"da55",
	data:"ba50e1fe0201050c097265656c794163746976650100",
};

describe('ble data servicedata', function() {

  // Test the process function
  it('should parse BLE advertiser data service data', function() {
  	servicedata(INPUT_DATA, CURSOR, ADVERTISER_DATA);
    assert.deepEqual(ADVERTISER_DATA.serviceData, EXPECTED_DATA);
  });

});
<|endoftext|>


<commit_before>
var fs = require('fs');
var exec = require ('child_process').exec;
var async = require ("async");
var app_path, ssh_file, local, email;
var i = 0;
var prompt = require ("prompt");
var config = {};

async.waterfall ([

    function (done){
        // Installs the whole setup
        console.log ("\n\nWELCOME TO THE NODE-AWS-DEPLOY INSTALLER");
        console.log ("This installs your application onto AWS and allows automatic deployment just by pushing to your remote git repository." +
            " Just answer a few questions and everything will be setup");
        prompt.start ();
        done ();
    },

    function (done){
        console.log (++i + ") Is this a 'remote' install? (If this is on a remote server the answer is y)");
        prompt.get (['(y/n)'], function (err, results){
            var answer = (results['(y/n)'] === 'y');
            local = !answer;
            config.sudo = (local) ? "" : "sudo";
            done ();
        });
    },

    function (done){
        console.log (++i + ") Enter your desired application's full path. (no file names. Example  '/home/ec2-user//MyCoolApplication')");
        prompt.get (['folder'], function (err, results){
            var answer = results['folder'];
            config.applicationDirectory = answer;
            try{
                fs.mkdir (answer, function (err){
                    app_path = answer;
                    done ();
                });
            }
            catch (err){
                done ();
            }
        });
    },

    function (done){
        console.log (++i + ") Enter your application's name");
        prompt.get (['name'], function (err, results){
            config.applicationName = results['name'];
            done ();
        });
    },
    function (done){
        console.log (++i + ") Enter your application's entry point (defaults to 'start.js')");
        prompt.get (['entry point'], function(err, results) {
            config.appEntry = results['entry point'];
            done ();
        });
    },
    function (done){
        if (local){
            console.log ("Please take a second ensure you are set up to clone your repository. This might require you to create an ssh key pair or simply use credentials.");
            console.log ("When you have your repository cloned press <enter> to continue.");
            prompt.get (['<enter>'], function(err, results) {
                done ();
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Enter your email address to seed the ssh key");
            prompt.get (['email'], function(err, results) {
                email = results['email'];
                done ();
            });
        }
        else{
            done ();
        }
    },

    function (done){
        if (!local){
            // go get the ssh key or create one if it doesn't exist
            try { ssh_file = fs.readFileSync ("/home/ec2-user/.ssh/id_rsa.pub");}
            catch (err) {ssh_file = null; console.log (err);}
            if (!ssh_file){
                console.log ("/tssh key not generated, generating a new one.");
console.log ('ssh-keygen -t rsa -C "' + email + '"');
                var child = exec ('echo HELLO', function (err, std, ster){
console.log ("debug here: err:%j, std:%j, ster:%j", err, std, ster);
                    if (err){done (err);}
                    else{
                        ssh_file = fs.readFileSync ("/home/ec2-user/.ssh/id_rsa.pub");
                        done ();
                    }
                });
            }
            else{
                console.log ("/tssh key already generated!");
                done ();
            }
        }
        else{
            done ();
        }
    },

    function (done){
        if (!local){
            console.log(++i + ") Please take a second to copy the ssh key from this server, listed below, and paste into your git repository service (such as github). This will authorize this server AMI to " +
                "clone your repository and perform git pulls. Press <enter> when done.");
            console.log ("\n" + ssh_file + "\n");
            prompt.get (['<enter>'], function(err, results) {
                done ();
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Now that your Git repository has been configured, enter the ssh url for remote access so this server can clone it:");
            prompt.get (['Git repository URL'], function (err, results){
                var git_url = results['Git repository URL'];
                if (git_url){
                    console.log ("  Cloning your repository...");
                    var child = exec ('cd ' + config.applicationDirectory + ' ; cd .. ; ' + config.sudo + ' git clone ' + git_url, function (err, std, ster){
                        if (err){
                            done (err);
                        }
                        else{
                            console.log (std);
                            done ();
                        }
                    });
                }
                else{
                    done ();
                }
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Enter the pull information, what triggers it how its configured, etc...");
            console.log ("\t pullPort: <set this to the port for a pull requests> - defaults to 8000");
            console.log ("\t pullKey: <path to a ssh key file for the HTTPS Server>");
            console.log ("\t pullCert: <path to a ssh cert file for the HTTPS Server>");
            console.log ("\t pullCa: <array of paths to the certificate authority files> (optional)");
            console.log ("\t pullPassphrase: <string - phrase that the certificate was generated with> (optional)");
            console.log ("\t pullSecret: <secret phrase that this server uses to identify as a valid pull request> (optional))");
            console.log ("\t pullBranch: <the branch that this server should pull from on pull requests> (defaults to master))");
            prompt.get (["pullPort", "pullKey", "pullCert", "pullCa", "pullPassphrase", "pullSecret", "pullBranch"], function (err, results){
                for (var k in results){
                    config[k] = results[k];
                }
                done ();
            });

        }
        else{
            done ();
        }
    },
    function (done) {
        if (!local){
            console.log (++i + ") Enter AWS SDK Information so that this server can talk to others on a pull request");
            console.log ("\t accessKeyId: <AWS API Access key 'XXXXXXXXXXXXXXXXXXXX'>");
            console.log ("\t secretAccessKey: <AWS Secret Access key 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'>");
            console.log ("\t region: <aws region that the servers are located in. EX 'us-east-1'>");
            prompt.get (["accessKeyId", "secretAccessKey", "region"], function (err, results){
                for (var k in results){
                    config[k] = results[k];
                }
                done ();
            });

        }
        else{
            done ();
        }
    }

], function (err){
    if (!err){
        var data = JSON.stringify (config);
        fs.writeFileSync ("app-config.json", data);
        console.log ("Success installed: " + config.applicationName + ". The Configuration has been written out to app-config.json");
        console.log ("To Launch the application type 'sudo start node-aws-deploy'");
        process.exit (0);
    }
    else{
        console.log ("There were errors. Errors:" + err);
        process.exit (1);
    }
});

<commit_msg>Initial<commit_after>
var fs = require('fs');
var exec = require ('child_process').exec;
var async = require ("async");
var app_path, ssh_file, local, email;
var i = 0;
var prompt = require ("prompt");
var config = {};

async.waterfall ([

    function (done){
        // Installs the whole setup
        console.log ("\n\nWELCOME TO THE NODE-AWS-DEPLOY INSTALLER");
        console.log ("This installs your application onto AWS and allows automatic deployment just by pushing to your remote git repository." +
            " Just answer a few questions and everything will be setup");
        prompt.start ();
        done ();
    },

    function (done){
        console.log (++i + ") Is this a 'remote' install? (If this is on a remote server the answer is y)");
        prompt.get (['(y/n)'], function (err, results){
            var answer = (results['(y/n)'] === 'y');
            local = !answer;
            config.sudo = (local) ? "" : "sudo";
            done ();
        });
    },

    function (done){
        console.log (++i + ") Enter your desired application's full path. (no file names. Example  '/home/ec2-user//MyCoolApplication')");
        prompt.get (['folder'], function (err, results){
            var answer = results['folder'];
            config.applicationDirectory = answer;
            try{
                fs.mkdir (answer, function (err){
                    app_path = answer;
                    done ();
                });
            }
            catch (err){
                done ();
            }
        });
    },

    function (done){
        console.log (++i + ") Enter your application's name");
        prompt.get (['name'], function (err, results){
            config.applicationName = results['name'];
            done ();
        });
    },
    function (done){
        console.log (++i + ") Enter your application's entry point (defaults to 'start.js')");
        prompt.get (['entry point'], function(err, results) {
            config.appEntry = results['entry point'];
            done ();
        });
    },
    function (done){
        if (local){
            console.log ("Please take a second ensure you are set up to clone your repository. This might require you to create an ssh key pair or simply use credentials.");
            console.log ("When you have your repository cloned press <enter> to continue.");
            prompt.get (['<enter>'], function(err, results) {
                done ();
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Enter your email address to seed the ssh key");
            prompt.get (['email'], function(err, results) {
                email = results['email'];
                done ();
            });
        }
        else{
            done ();
        }
    },

    function (done){
        if (!local){
            // go get the ssh key or create one if it doesn't exist
            try { ssh_file = fs.readFileSync ("/root/.ssh/id_rsa.pub");}
            catch (err) {ssh_file = null; console.log (err);}
            if (!ssh_file){
                console.log ("/tssh key not generated, generating a new one.");
console.log ('ssh-keygen -t rsa -C "' + email + '"');
                var child = exec ('echo HELLO', function (err, std, ster){
console.log ("debug here: err:%j, std:%j, ster:%j", err, std, ster);
                    if (err){done (err);}
                    else{
                        ssh_file = fs.readFileSync ("/home/ec2-user/.ssh/id_rsa.pub");
                        done ();
                    }
                });
            }
            else{
                console.log ("/tssh key already generated!");
                done ();
            }
        }
        else{
            done ();
        }
    },

    function (done){
        if (!local){
            console.log(++i + ") Please take a second to copy the ssh key from this server, listed below, and paste into your git repository service (such as github). This will authorize this server AMI to " +
                "clone your repository and perform git pulls. Press <enter> when done.");
            console.log ("\n" + ssh_file + "\n");
            prompt.get (['<enter>'], function(err, results) {
                done ();
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Now that your Git repository has been configured, enter the ssh url for remote access so this server can clone it:");
            prompt.get (['Git repository URL'], function (err, results){
                var git_url = results['Git repository URL'];
                if (git_url){
                    console.log ("  Cloning your repository...");
                    var child = exec ('cd ' + config.applicationDirectory + ' ; cd .. ; ' + config.sudo + ' git clone ' + git_url, function (err, std, ster){
                        if (err){
                            done (err);
                        }
                        else{
                            console.log (std);
                            done ();
                        }
                    });
                }
                else{
                    done ();
                }
            });
        }
        else{
            done ();
        }
    },
    function (done){
        if (!local){
            console.log (++i + ") Enter the pull information, what triggers it how its configured, etc...");
            console.log ("\t pullPort: <set this to the port for a pull requests> - defaults to 8000");
            console.log ("\t pullKey: <path to a ssh key file for the HTTPS Server>");
            console.log ("\t pullCert: <path to a ssh cert file for the HTTPS Server>");
            console.log ("\t pullCa: <array of paths to the certificate authority files> (optional)");
            console.log ("\t pullPassphrase: <string - phrase that the certificate was generated with> (optional)");
            console.log ("\t pullSecret: <secret phrase that this server uses to identify as a valid pull request> (optional))");
            console.log ("\t pullBranch: <the branch that this server should pull from on pull requests> (defaults to master))");
            prompt.get (["pullPort", "pullKey", "pullCert", "pullCa", "pullPassphrase", "pullSecret", "pullBranch"], function (err, results){
                for (var k in results){
                    config[k] = results[k];
                }
                done ();
            });

        }
        else{
            done ();
        }
    },
    function (done) {
        if (!local){
            console.log (++i + ") Enter AWS SDK Information so that this server can talk to others on a pull request");
            console.log ("\t accessKeyId: <AWS API Access key 'XXXXXXXXXXXXXXXXXXXX'>");
            console.log ("\t secretAccessKey: <AWS Secret Access key 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'>");
            console.log ("\t region: <aws region that the servers are located in. EX 'us-east-1'>");
            prompt.get (["accessKeyId", "secretAccessKey", "region"], function (err, results){
                for (var k in results){
                    config[k] = results[k];
                }
                done ();
            });

        }
        else{
            done ();
        }
    }

], function (err){
    if (!err){
        var data = JSON.stringify (config);
        fs.writeFileSync ("app-config.json", data);
        console.log ("Success installed: " + config.applicationName + ". The Configuration has been written out to app-config.json");
        console.log ("To Launch the application type 'sudo start node-aws-deploy'");
        process.exit (0);
    }
    else{
        console.log ("There were errors. Errors:" + err);
        process.exit (1);
    }
});

<|endoftext|>


<commit_before>"use strict";

class GSUI {
	static noop() {}
	static popup = document.createElement( "gsui-popup" );
	static dragshield = document.createElement( "gsui-dragshield" );

	// .........................................................................
	static clamp( n, min, max ) {
		return (
			min < max
				? Math.max( min, Math.min( n || 0, max ) )
				: Math.max( max, Math.min( n || 0, min ) )
		);
	}

	// .........................................................................
	static diffObjects( a, b ) {
		let empty = true;
		const diff = Object.entries( b ).reduce( ( diff, [ bk, bv ] ) => {
			const av = a[ bk ];
			const newval = av === bv ? undefined :
				typeof bv !== "object" || bv === null ? bv :
				typeof av !== "object" || av === null
					? Object.freeze( JSON.parse( JSON.stringify( bv ) ) )
					: GSUI.diffObjects( av, bv );

			if ( newval !== undefined ) {
				empty = false;
				diff[ bk ] = newval;
			}
			return diff;
		}, {} );

		Object.keys( a ).forEach( ak => {
			if ( !( ak in b ) ) {
				empty = false;
				diff[ ak ] = undefined;
			}
		} );
		return empty ? undefined : Object.freeze( diff );
	}

	// .........................................................................
	static findElem( root, graph ) {
		return typeof graph === "string"
			? GSUI.#findElemStr( root, graph )
			: Object.seal( Array.isArray( graph )
				? GSUI.#findElemArr( root, graph )
				: GSUI.#findElemObj( root, graph ) );
	}
	static #findElemArr( root, arr ) {
		return arr.map( sel => GSUI.findElem( root, sel ) );
	}
	static #findElemObj( root, obj ) {
		const ent = Object.entries( obj );

		ent.forEach( kv => kv[ 1 ] = GSUI.findElem( root, kv[ 1 ] ) );
		return Object.fromEntries( ent );
	}
	static #findElemStr( root, sel ) {
		if ( Array.isArray( root ) ) {
			let el;

			Array.prototype.find.call( root, r => el = GSUI.#findElemQuery( r, sel ) );
			return el || null;
		}
		return GSUI.#findElemQuery( root, sel );
	}
	static #findElemQuery( root, sel ) {
		return root.matches( sel )
			? root
			: root.querySelector( sel );
	}

	// .........................................................................
	static dispatchEv( el, component, eventName, ...args ) {
		el.dispatchEvent( new CustomEvent( "gsuiEvents", {
			bubbles: true,
			detail: { component, eventName, args },
		} ) );
	}
	static listenEv( el, cbs ) {
		el.addEventListener( "gsuiEvents", e => {
			const d = e.detail;
			const cbs2 = cbs[ d.component ] || cbs.default;
			const fn = cbs2 && ( cbs2[ d.eventName ] || cbs2.default );

			if ( fn && fn( d, e.target, e ) !== true ) {
				e.stopPropagation();
				e.stopImmediatePropagation();
			}
		} );
	}

	// .........................................................................
	static #templates = new Map();
	static setTemplate( tmpId, fn ) {
		GSUI.#templates.set( tmpId, fn );
	}
	static hasTemplate( tmpId ) {
		return GSUI.#templates.has( tmpId );
	}
	static getTemplate( tmpId, ...args ) {
		return GSUI.#templates.get( tmpId )( ...args );
	}

	// .........................................................................
	static createElem( tag, attr, ...children ) {
		return GSUI.#createElem( "http://www.w3.org/1999/xhtml", tag, attr, children );
	}
	static createElemSVG( tag, attr, ...children ) {
		return GSUI.#createElem( "http://www.w3.org/2000/svg", tag, attr, children );
	}
	static #createElem( ns, tag, attrObj, children ) {
		const el = document.createElementNS( ns, tag );

		GSUI.setAttr( el, attrObj );
		el.append( ...children.flat( 1 ).filter( ch => Boolean( ch ) || Number.isFinite( ch ) ) );
		return el;
	}
	static setAttr( el, attr, val ) {
		if ( typeof attr === "string" ) {
			GSUI.#setAttr( el, attr, val );
		} else if ( attr ) {
			Object.entries( attr ).forEach( kv => GSUI.#setAttr( el, ...kv ) );
		}
	}
	static #setAttr( el, attr, val ) {
		val !== false && val !== null && val !== undefined
			? el.setAttribute( attr, val === true ? "" : val )
			: el.removeAttribute( attr );
	}
	static getAttr( el, attr ) {
		return el.getAttribute( attr );
	}
	static getAttrNum( el, attr ) {
		const val = el.getAttribute( attr );
		const n = +val;

		if ( Number.isNaN( n ) ) {
			console.error( `GSUI.getAttrNum: ${ attr } is NaN (${ val })` );
		}
		return n;
	}

	// .........................................................................
	static observeSizeOf( el, fn ) {
		if ( GSUI.#resizeMap.has( el ) ) {
			GSUI.#resizeMap.get( el ).push( fn );
		} else {
			GSUI.#resizeMap.set( el, [ fn ] );
		}
		GSUI.#resizeObs.observe( el );
	}
	static unobserveSizeOf( el, fn ) {
		const fns = GSUI.#resizeMap.get( el );
		const fnInd = fns.indexOf( fn );

		GSUI.#resizeObs.unobserve( el );
		if ( fnInd > -1 ) {
			fns.splice( fnInd, 1 );
			if ( fns.length === 0 ) {
				GSUI.#resizeMap.delete( el );
			}
		}
	}
	static #resizeMap = new Map();
	static #resizeObs = new ResizeObserver( entries => {
		entries.forEach( e => {
			GSUI.#resizeMap.get( e.target )
				.forEach( fn => fn( e.contentRect.width, e.contentRect.height ) );
		} );
	} );

	// .........................................................................
	static emptyElem( el ) {
		while ( el.lastChild ) {
			el.lastChild.remove();
		}
	}

	// .........................................................................
	static unselectText() {
		window.getSelection().removeAllRanges();
	}

	// .........................................................................
	static recallAttributes( el, props ) {
		Object.entries( props ).forEach( ( [ p, val ] ) => {
			el.hasAttribute( p )
				? el.attributeChangedCallback( p, null, el.getAttribute( p ) )
				: GSUI.#setAttr( el, p, val );
		} );
	}
}

document.body.prepend( GSUI.dragshield, GSUI.popup );
<commit_msg>gsui.js: #findElemObj, check if the selector arent falsy<commit_after>"use strict";

class GSUI {
	static noop() {}
	static popup = document.createElement( "gsui-popup" );
	static dragshield = document.createElement( "gsui-dragshield" );

	// .........................................................................
	static clamp( n, min, max ) {
		return (
			min < max
				? Math.max( min, Math.min( n || 0, max ) )
				: Math.max( max, Math.min( n || 0, min ) )
		);
	}

	// .........................................................................
	static diffObjects( a, b ) {
		let empty = true;
		const diff = Object.entries( b ).reduce( ( diff, [ bk, bv ] ) => {
			const av = a[ bk ];
			const newval = av === bv ? undefined :
				typeof bv !== "object" || bv === null ? bv :
				typeof av !== "object" || av === null
					? Object.freeze( JSON.parse( JSON.stringify( bv ) ) )
					: GSUI.diffObjects( av, bv );

			if ( newval !== undefined ) {
				empty = false;
				diff[ bk ] = newval;
			}
			return diff;
		}, {} );

		Object.keys( a ).forEach( ak => {
			if ( !( ak in b ) ) {
				empty = false;
				diff[ ak ] = undefined;
			}
		} );
		return empty ? undefined : Object.freeze( diff );
	}

	// .........................................................................
	static findElem( root, graph ) {
		return typeof graph === "string"
			? GSUI.#findElemStr( root, graph )
			: Object.seal( Array.isArray( graph )
				? GSUI.#findElemArr( root, graph )
				: GSUI.#findElemObj( root, graph ) );
	}
	static #findElemArr( root, arr ) {
		return arr.map( sel => GSUI.findElem( root, sel ) );
	}
	static #findElemObj( root, obj ) {
		if ( obj ) {
			const ent = Object.entries( obj );

			ent.forEach( kv => kv[ 1 ] = GSUI.findElem( root, kv[ 1 ] ) );
			return Object.fromEntries( ent );
		}
	}
	static #findElemStr( root, sel ) {
		if ( Array.isArray( root ) ) {
			let el;

			Array.prototype.find.call( root, r => el = GSUI.#findElemQuery( r, sel ) );
			return el || null;
		}
		return GSUI.#findElemQuery( root, sel );
	}
	static #findElemQuery( root, sel ) {
		return root.matches( sel )
			? root
			: root.querySelector( sel );
	}

	// .........................................................................
	static dispatchEv( el, component, eventName, ...args ) {
		el.dispatchEvent( new CustomEvent( "gsuiEvents", {
			bubbles: true,
			detail: { component, eventName, args },
		} ) );
	}
	static listenEv( el, cbs ) {
		el.addEventListener( "gsuiEvents", e => {
			const d = e.detail;
			const cbs2 = cbs[ d.component ] || cbs.default;
			const fn = cbs2 && ( cbs2[ d.eventName ] || cbs2.default );

			if ( fn && fn( d, e.target, e ) !== true ) {
				e.stopPropagation();
				e.stopImmediatePropagation();
			}
		} );
	}

	// .........................................................................
	static #templates = new Map();
	static setTemplate( tmpId, fn ) {
		GSUI.#templates.set( tmpId, fn );
	}
	static hasTemplate( tmpId ) {
		return GSUI.#templates.has( tmpId );
	}
	static getTemplate( tmpId, ...args ) {
		return GSUI.#templates.get( tmpId )( ...args );
	}

	// .........................................................................
	static createElem( tag, attr, ...children ) {
		return GSUI.#createElem( "http://www.w3.org/1999/xhtml", tag, attr, children );
	}
	static createElemSVG( tag, attr, ...children ) {
		return GSUI.#createElem( "http://www.w3.org/2000/svg", tag, attr, children );
	}
	static #createElem( ns, tag, attrObj, children ) {
		const el = document.createElementNS( ns, tag );

		GSUI.setAttr( el, attrObj );
		el.append( ...children.flat( 1 ).filter( ch => Boolean( ch ) || Number.isFinite( ch ) ) );
		return el;
	}
	static setAttr( el, attr, val ) {
		if ( typeof attr === "string" ) {
			GSUI.#setAttr( el, attr, val );
		} else if ( attr ) {
			Object.entries( attr ).forEach( kv => GSUI.#setAttr( el, ...kv ) );
		}
	}
	static #setAttr( el, attr, val ) {
		val !== false && val !== null && val !== undefined
			? el.setAttribute( attr, val === true ? "" : val )
			: el.removeAttribute( attr );
	}
	static getAttr( el, attr ) {
		return el.getAttribute( attr );
	}
	static getAttrNum( el, attr ) {
		const val = el.getAttribute( attr );
		const n = +val;

		if ( Number.isNaN( n ) ) {
			console.error( `GSUI.getAttrNum: ${ attr } is NaN (${ val })` );
		}
		return n;
	}

	// .........................................................................
	static observeSizeOf( el, fn ) {
		if ( GSUI.#resizeMap.has( el ) ) {
			GSUI.#resizeMap.get( el ).push( fn );
		} else {
			GSUI.#resizeMap.set( el, [ fn ] );
		}
		GSUI.#resizeObs.observe( el );
	}
	static unobserveSizeOf( el, fn ) {
		const fns = GSUI.#resizeMap.get( el );
		const fnInd = fns.indexOf( fn );

		GSUI.#resizeObs.unobserve( el );
		if ( fnInd > -1 ) {
			fns.splice( fnInd, 1 );
			if ( fns.length === 0 ) {
				GSUI.#resizeMap.delete( el );
			}
		}
	}
	static #resizeMap = new Map();
	static #resizeObs = new ResizeObserver( entries => {
		entries.forEach( e => {
			GSUI.#resizeMap.get( e.target )
				.forEach( fn => fn( e.contentRect.width, e.contentRect.height ) );
		} );
	} );

	// .........................................................................
	static emptyElem( el ) {
		while ( el.lastChild ) {
			el.lastChild.remove();
		}
	}

	// .........................................................................
	static unselectText() {
		window.getSelection().removeAllRanges();
	}

	// .........................................................................
	static recallAttributes( el, props ) {
		Object.entries( props ).forEach( ( [ p, val ] ) => {
			el.hasAttribute( p )
				? el.attributeChangedCallback( p, null, el.getAttribute( p ) )
				: GSUI.#setAttr( el, p, val );
		} );
	}
}

document.body.prepend( GSUI.dragshield, GSUI.popup );
<|endoftext|>


<commit_before>const Ratings = require("../src/Ratings");
const Period = require("../src/Period");
const Time = require("../src/Time");

class Match {
    // [Team] -> Seed -> Match
    constructor(teams, seed) {
        this.teams = teams;
        this.seed = seed;
    }

    // Match ~> Team
    get home() {
        return this.teams[0];
    }

    // Match ~> Team
    get away() {
        return this.teams[1];
    }

    // Match ~> Ratings
    get ratings() {
        return this.teams.map(team => team.rating).toRatings();
    }

    // Match ~> Period
    get firstHalf() {
        const timeLength = new Time().setMinutes(45);
        const seed = this.seed.append("firstHalf");

        return new Period(timeLength, this.ratings, seed);
    }

    // Match ~> Period
    get secondHalf() {
        const timeLength = new Time().setMinutes(45);
        const seed = this.seed.append("secondHalf");

        return new Period(timeLength, this.ratings, seed);
    }

    // Match ~> Boolean
    get isExtraTime() {
        return false;
    }

    // Match ~> Boolean
    get isPenaltyShootout() {
        return false;
    }

    // Match ~> Nullable Team
    get winner() {
        return this.occurrences.goals.winner(this.teams);
    }

    // Match ~> Boolean
    get isDraw() {
        return this.occurrences.goals.isDraw;
    }

    // Match ~> [Period]
    get periods() {
        return [this.firstHalf, this.secondHalf];
    }

    // Match ~> OccurrenceCollection Occurrences
    get occurrences() {
        return this.firstHalf.occurrences.append(this.secondHalf.occurrences);
    }
}

module.exports = Match;
<commit_msg>Tidies up imports<commit_after>const Ratings = require("./Ratings");
const Period = require("./Period");
const Time = require("./Time");

class Match {
    // [Team] -> Seed -> Match
    constructor(teams, seed) {
        this.teams = teams;
        this.seed = seed;
    }

    // Match ~> Team
    get home() {
        return this.teams[0];
    }

    // Match ~> Team
    get away() {
        return this.teams[1];
    }

    // Match ~> Ratings
    get ratings() {
        return this.teams.map(team => team.rating).toRatings();
    }

    // Match ~> Period
    get firstHalf() {
        const timeLength = new Time().setMinutes(45);
        const seed = this.seed.append("firstHalf");

        return new Period(timeLength, this.ratings, seed);
    }

    // Match ~> Period
    get secondHalf() {
        const timeLength = new Time().setMinutes(45);
        const seed = this.seed.append("secondHalf");

        return new Period(timeLength, this.ratings, seed);
    }

    // Match ~> Boolean
    get isExtraTime() {
        return false;
    }

    // Match ~> Boolean
    get isPenaltyShootout() {
        return false;
    }

    // Match ~> Nullable Team
    get winner() {
        return this.occurrences.goals.winner(this.teams);
    }

    // Match ~> Boolean
    get isDraw() {
        return this.occurrences.goals.isDraw;
    }

    // Match ~> [Period]
    get periods() {
        return [this.firstHalf, this.secondHalf];
    }

    // Match ~> OccurrenceCollection Occurrences
    get occurrences() {
        return this.firstHalf.occurrences.append(this.secondHalf.occurrences);
    }
}

module.exports = Match;
<|endoftext|>


<commit_before>import { Model as ObjectionModel } from 'objection'

import './RelationSynchronizer'
import './RelationValidator'
import './inflect'

const as = require('as-type')

export class Model extends ObjectionModel {
	static descriptiveName = null
	static eager = null
	static eagerFilters = null

	static $$app = null

	static app(app) {
		if(!app.isNil) {
			this.$$app = app
		} else {
			return this.$$app
		}
	}

	$app() {
		return this.constructor.app()
	}

	static findById(id) {
		return this.query().findById(id)
	}

	static findByRouteParameter(value) {
		return this.findById(value)
	}

	static routeBind(name, description) {
		description = description || `${name} Value`

		this.app().routes.bind(name, (value, resolve, reject) => {
			this.findByRouteParameter(value).then(row => {
				if(row.isNil) {
					reject(new NotFoundError(`${this.describe()} Not found`))
					return
				}

				resolve(row)
			})
		}, { swagger: { name, description } })
	}

	static buildRelations() {
		return { }
	}

	static getRelations() {
		if(this.relationMappings.isNil) {
			this.relationMappings = this.buildRelations()
		}

		return super.getRelations()
	}

	static hasOne(modelClass, foreignKey = null, localKey = null) {
		return this._hasOneOrMany(this.HasOneRelation, modelClass, foreignKey, localKey)
	}

	static hasMany(modelClass, foreignKey = null, localKey = null) {
		return this._hasOneOrMany(this.HasManyRelation, modelClass, foreignKey, localKey)
	}

	static _hasOneOrMany(relation, modelClass, foreignKey = null, localKey = null) {
		foreignKey = foreignKey || inflect.foreignKey(this.name)

		if(localKey.isNil) {
			localKey = this.getFullIdColumn()
		} else {
			localKey = `${this.tableName}.${localKey}`
		}

		return {
			relation: relation,
			modelClass: modelClass,
			join: {
				from: `${modelClass.tableName}.${foreignKey}`,
				to: localKey
			}
		}
	}

	static belongsTo(modelClass, foreignKey = null, otherKey = null) {
		foreignKey = foreignKey || inflect.foreignKey(modelClass.name)

		if(otherKey.isNil) {
			otherKey = modelClass.getFullIdColumn()
		} else {
			otherKey = `${modelClass.tableName}.${otherKey}`
		}

		return {
			relation: this.BelongsToOneRelation,
			modelClass: modelClass,
			join: {
				from: `${this.tableName}.${foreignKey}`,
				to: otherKey
			}
		}
	}

	static belongsToMany(modelClass, tableName = null, foreignKey = null, otherKey = null) {
		tableName = tableName || [ this.tableName, modelClass.tableName ].sort().join('_')
		foreignKey = foreignKey || inflect.foreignKey(modelClass.name)
		otherKey = otherKey || inflect.foreignKey(this.name)

		return {
			relation: this.ManyToManyRelation,
			modelClass: modelClass,
			join: {
				from: this.getFullIdColumn(),
				through: {
					from: `${tableName}.${otherKey}`,
					to: `${tableName}.${foreignKey}`
				},
				to: modelClass.getFullIdColumn()
			}
		}
	}

	$sync(relation, ids) {
		return (new RelationSynchronizer(this, relation)).sync(ids)
	}

	$relate(relation, ids) {
		return (new RelationSynchronizer(this, relation)).relate(ids)
	}

	$unrelate(relation, ids) {
		return (new RelationSynchronizer(this, relation)).unrelate(ids)
	}

	$parseDatabaseJson(json) {
		json = super.$parseDatabaseJson(json)
		const schema = this.constructor.jsonSchema

		if(schema.isNil || schema.properties.isNil) {
			return json
		}

		for(const field of Object.keys(schema.properties)) {
			if(typeof json[field] === 'undefined') {
				continue
			}

			const property = schema.properties[field]

			// Boolean values can be converted to JSON as "0"/"1", convert back to boolean
			if(property.type === 'boolean') {
				json[field] = as.boolean(json[field])
			}

			// Number values can be converted to JSON as strings, convert back to numbers
			if(property.type === 'number' || property.type === 'float' || property.type === 'double') {
				json[field] = as.float(json[field])
			} else if(property.type === 'integer') {
				json[field] = as.integer(json[field])
			}
		}

		return json
	}

	$beforeValidate(jsonSchema, json, opt) {
		const properties = jsonSchema.properties || { }

		for(const field of Object.keys(properties)) {
			const value = json[field]

			if(value.isNil) {
				continue
			}

			const fieldType = properties[field].type

			if(fieldType === 'boolean') {
				json[field] = as.boolean(json[field])
			} else if(fieldType === 'integer') {
				json[field] = as.integer(json[field])
			} else if(fieldType === 'number' || fieldType === 'float' || fieldType === 'double') {
				json[field] = as.float(json[field])
			}
		}

		return super.$beforeValidate(jsonSchema, json, opt)
	}

	$beforeSave(inserting) {
		return new Promise(resolve => {
			const now = (new Date).toISOString()
			this.updated_at = now

			if(inserting) {
				this.created_at = now
			}

			resolve()
		}).then(() => RelationValidator(this))
	}

	$beforeInsert() {
		return this.$beforeSave(true)
	}

	$beforeUpdate() {
		return this.$beforeSave(false)
	}

	static describe() {
		if(!this.descriptiveName.isNil) {
			return this.descriptiveName
		}

		return inflect.singular(this.tableName)
	}

}
<commit_msg>Fixed bug in Model.$beforeValidate that wasn’t handling arrays for json schema type<commit_after>import { Model as ObjectionModel } from 'objection'

import './RelationSynchronizer'
import './RelationValidator'
import './inflect'

const as = require('as-type')

export class Model extends ObjectionModel {
	static descriptiveName = null
	static eager = null
	static eagerFilters = null

	static $$app = null

	static app(app) {
		if(!app.isNil) {
			this.$$app = app
		} else {
			return this.$$app
		}
	}

	$app() {
		return this.constructor.app()
	}

	static findById(id) {
		return this.query().findById(id)
	}

	static findByRouteParameter(value) {
		return this.findById(value)
	}

	static routeBind(name, description) {
		description = description || `${name} Value`

		this.app().routes.bind(name, (value, resolve, reject) => {
			this.findByRouteParameter(value).then(row => {
				if(row.isNil) {
					reject(new NotFoundError(`${this.describe()} Not found`))
					return
				}

				resolve(row)
			})
		}, { swagger: { name, description } })
	}

	static buildRelations() {
		return { }
	}

	static getRelations() {
		if(this.relationMappings.isNil) {
			this.relationMappings = this.buildRelations()
		}

		return super.getRelations()
	}

	static hasOne(modelClass, foreignKey = null, localKey = null) {
		return this._hasOneOrMany(this.HasOneRelation, modelClass, foreignKey, localKey)
	}

	static hasMany(modelClass, foreignKey = null, localKey = null) {
		return this._hasOneOrMany(this.HasManyRelation, modelClass, foreignKey, localKey)
	}

	static _hasOneOrMany(relation, modelClass, foreignKey = null, localKey = null) {
		foreignKey = foreignKey || inflect.foreignKey(this.name)

		if(localKey.isNil) {
			localKey = this.getFullIdColumn()
		} else {
			localKey = `${this.tableName}.${localKey}`
		}

		return {
			relation: relation,
			modelClass: modelClass,
			join: {
				from: `${modelClass.tableName}.${foreignKey}`,
				to: localKey
			}
		}
	}

	static belongsTo(modelClass, foreignKey = null, otherKey = null) {
		foreignKey = foreignKey || inflect.foreignKey(modelClass.name)

		if(otherKey.isNil) {
			otherKey = modelClass.getFullIdColumn()
		} else {
			otherKey = `${modelClass.tableName}.${otherKey}`
		}

		return {
			relation: this.BelongsToOneRelation,
			modelClass: modelClass,
			join: {
				from: `${this.tableName}.${foreignKey}`,
				to: otherKey
			}
		}
	}

	static belongsToMany(modelClass, tableName = null, foreignKey = null, otherKey = null) {
		tableName = tableName || [ this.tableName, modelClass.tableName ].sort().join('_')
		foreignKey = foreignKey || inflect.foreignKey(modelClass.name)
		otherKey = otherKey || inflect.foreignKey(this.name)

		return {
			relation: this.ManyToManyRelation,
			modelClass: modelClass,
			join: {
				from: this.getFullIdColumn(),
				through: {
					from: `${tableName}.${otherKey}`,
					to: `${tableName}.${foreignKey}`
				},
				to: modelClass.getFullIdColumn()
			}
		}
	}

	$sync(relation, ids) {
		return (new RelationSynchronizer(this, relation)).sync(ids)
	}

	$relate(relation, ids) {
		return (new RelationSynchronizer(this, relation)).relate(ids)
	}

	$unrelate(relation, ids) {
		return (new RelationSynchronizer(this, relation)).unrelate(ids)
	}

	$parseDatabaseJson(json) {
		json = super.$parseDatabaseJson(json)
		const schema = this.constructor.jsonSchema

		if(schema.isNil || schema.properties.isNil) {
			return json
		}

		for(const field of Object.keys(schema.properties)) {
			if(typeof json[field] === 'undefined') {
				continue
			}

			const property = schema.properties[field]

			// Boolean values can be converted to JSON as "0"/"1", convert back to boolean
			if(property.type === 'boolean') {
				json[field] = as.boolean(json[field])
			}

			// Number values can be converted to JSON as strings, convert back to numbers
			if(property.type === 'number' || property.type === 'float' || property.type === 'double') {
				json[field] = as.float(json[field])
			} else if(property.type === 'integer') {
				json[field] = as.integer(json[field])
			}
		}

		return json
	}

	$beforeValidate(jsonSchema, json, opt) {
		const properties = jsonSchema.properties || { }

		for(const field of Object.keys(properties)) {
			const value = json[field]

			if(value.isNil) {
				continue
			}

			let fieldType = properties[field].type

			if(Array.isArray(fieldType) && fieldType.length > 1) {
				fieldType = fieldType[0]
			}

			if(fieldType === 'boolean') {
				json[field] = as.boolean(json[field])
			} else if(fieldType === 'integer') {
				json[field] = as.integer(json[field])
			} else if(fieldType === 'number' || fieldType === 'float' || fieldType === 'double') {
				json[field] = as.float(json[field])
			}
		}

		return super.$beforeValidate(jsonSchema, json, opt)
	}

	$beforeSave(inserting) {
		return new Promise(resolve => {
			const now = (new Date).toISOString()
			this.updated_at = now

			if(inserting) {
				this.created_at = now
			}

			resolve()
		}).then(() => RelationValidator(this))
	}

	$beforeInsert() {
		return this.$beforeSave(true)
	}

	$beforeUpdate() {
		return this.$beforeSave(false)
	}

	static describe() {
		if(!this.descriptiveName.isNil) {
			return this.descriptiveName
		}

		return inflect.singular(this.tableName)
	}

}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import Toggle from 'react-toggle';
import 'react-toggle/style.css';

import ReadableList from 'common/ReadableList';
import Contributor from 'Main/Contributor';

import CORE_CHANGELOG from '../CHANGELOG';

class Changelog extends React.PureComponent {
  static propTypes = {
    changelog: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
    limit: PropTypes.number,
    includeCore: PropTypes.bool,
  };
  static defaultProps = {
    includeCore: true,
  };
  constructor() {
    super();
    this.state = {
      includeCore: true,
    };
  }

  render() {
    const { changelog, limit, includeCore } = this.props;

    const mergedChangelog = includeCore && this.state.includeCore ? [ ...CORE_CHANGELOG, ...changelog ].sort((a, b) => b.date - a.date) : changelog;
    return (

        {includeCore && (

            <Toggle
              defaultChecked={this.state.includeCore}
              icons={false}
              onChange={event => this.setState({ includeCore: event.target.checked })}
              id="core-entries-toggle"
            />

              <dfn data-tip="Turn this off to only see changes to this spec's implementation.">Shared changes</dfn>

        )}

          {mergedChangelog
            .filter((_, i) => !limit || i < limit)
            .map((entry, i) => {
              const { date, changes, contributors } = entry;
              const isFromCoreChangelog = CORE_CHANGELOG.includes(entry);
              // The index of the entry provides us with a unique never changing key, which speeds up the Shared Changes toggle
              const index = isFromCoreChangelog ? CORE_CHANGELOG.indexOf(entry) : changelog.indexOf(entry);

              return (
                <li
                  key={isFromCoreChangelog ? `core-${index}` : `spec-${index}`}
                  className={`flex wrapable ${includeCore && isFromCoreChangelog ? 'text-muted' : ''}`}
                >

                    {date.toLocaleDateString()}

                    {changes}

                    <ReadableList>
                      {contributors.map(contributor => <Contributor key={contributor.nickname} {...contributor} />)}
                    </ReadableList>

              );
            })}

    );
  }
}

export default Changelog;
<commit_msg>White space between toggle and shared changes text<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import Toggle from 'react-toggle';
import 'react-toggle/style.css';

import ReadableList from 'common/ReadableList';
import Contributor from 'Main/Contributor';

import CORE_CHANGELOG from '../CHANGELOG';

class Changelog extends React.PureComponent {
  static propTypes = {
    changelog: PropTypes.oneOfType([PropTypes.array, PropTypes.string]).isRequired,
    limit: PropTypes.number,
    includeCore: PropTypes.bool,
  };
  static defaultProps = {
    includeCore: true,
  };
  constructor() {
    super();
    this.state = {
      includeCore: true,
    };
  }

  render() {
    const { changelog, limit, includeCore } = this.props;

    const mergedChangelog = includeCore && this.state.includeCore ? [ ...CORE_CHANGELOG, ...changelog ].sort((a, b) => b.date - a.date) : changelog;
    return (

        {includeCore && (

            <Toggle
              defaultChecked={this.state.includeCore}
              icons={false}
              onChange={event => this.setState({ includeCore: event.target.checked })}
              id="core-entries-toggle"
            />{' '}

              <dfn data-tip="Turn this off to only see changes to this spec's implementation.">Shared changes</dfn>

        )}

          {mergedChangelog
            .filter((_, i) => !limit || i < limit)
            .map((entry, i) => {
              const { date, changes, contributors } = entry;
              const isFromCoreChangelog = CORE_CHANGELOG.includes(entry);
              // The index of the entry provides us with a unique never changing key, which speeds up the Shared Changes toggle
              const index = isFromCoreChangelog ? CORE_CHANGELOG.indexOf(entry) : changelog.indexOf(entry);

              return (
                <li
                  key={isFromCoreChangelog ? `core-${index}` : `spec-${index}`}
                  className={`flex wrapable ${includeCore && isFromCoreChangelog ? 'text-muted' : ''}`}
                >

                    {date.toLocaleDateString()}

                    {changes}

                    <ReadableList>
                      {contributors.map(contributor => <Contributor key={contributor.nickname} {...contributor} />)}
                    </ReadableList>

              );
            })}

    );
  }
}

export default Changelog;
<|endoftext|>


<commit_before>import React from 'react';

const changes = `
05-05-2017 - Added Aura of Mercy and Sacrifice healing done statistic.
05-05-2017 - Changed the report selection interface.
04-05-2017 - Minor importance suggestions are now hidden behind a toggle by default.
01-05-2017 - Added overhealing suggestions and relaxed the non healing time / dead GCD time suggestions (they should be marked as minor issues more often).
27-04-2017 - Prevent caching of fights list so it updates with live logs. You can now directly link specific tabs on the results page.
23-04-2017 - Changed the LotM suggestion to be based on CPM and be much more forgiving. Added death indicators to the mana graph.
21-04-2017 - Added HPS numbers to item healing done. Damaging HS are now included in cast counts. Recommended cast efficiency of CS with CM reduced to 30%. The recommended unused IoL is now adjusted based on if you have CM, DP and/or 4PT19.
21-04-2017 - Added suggestion/issue importance indicators and show a positive message when there are no major issues.
20-04-2017 - Since Easter is over bosses will no longer resurrect in the mana graph<dfn data-tip="This may cause issues with bosses that do actually resurrect, but I will fix that once I encounter such a boss.">*</dfn>. All bosses in a fight get shown instead of just the first<dfn data-tip="If boss adds are classified as bosses they will be shown as well. So far this doesn't appear to happen, but I will fix it if I encounter this.">*</dfn>. This should make the boss health for Mythic Botanist work properly.
19-04-2017 - Added a Mana tab for analyzing your mana usage. This also shows boss health since it is often used as a target for mana levels.
18-04-2017 - Added Velen's Future Sight to cast efficiency. Fixed the parser crashing instead of showing a descriptive error when selected player doesn't appear in a log. Fixed a crash when buff with stacks expired without ever applying.
17-04-2017 - Added LotM suggestion when casting it too often, added Divine Protection and BoS to cast efficiency display. Added Divine Purpose proc indicator.
15-04-2017 - Added Tyr's Deliverance healing contribution.
15-04-2017 - Added Sacred Dawn healing contribution. Note that (when SD wasn't up before) Sacred Dawn increases the Light of Dawn healing on yourself on the initial cast, but other players get healed for the normal amount without the SD healing increase. The calculation is aware of this.
15-04-2017 - Cast efficiency now takes the extra amount of fight time you had to cast an ability into consideration (e.g. a single cast of 3 minute cooldown during a 3:30 fight will have a higher cast efficiency than during a 5:30 fight).
15-04-2017 - Added a Talents tab.
14-04-2017 - Added Suggestions which will give you (way too many) suggestions to improve. This may need to be tuned a bit; any feedback in that regard is welcome. Moved Cast efficiency and Mastery effectiveness player breakdown to separate tabs.
11-04-2017 - Added Rule of Law to cast efficiency and grouped spells by category.
10-04-2017 - Show an error when parsing crashes (usually caused by not having advanced combat logging on). Renamed Casts Per Minute to Cast Efficiency. Show absolute amount of casts in Cast Efficiency. Added Arcane Torrent to Cast Efficiency (only shown if you cast it at least once).
09-04-2017 - Added Casts Per Minute table with very basic recommendations.
08-04-2017 - Added Heals on beacon statistic.
08-04-2017 - Added Wowhead tooltips and show T19 4 set bonus gain.
08-04-2017 - Added Beacon of the Lightbringer mastery radius support!
08-04-2017 - Improve beacon healing tracking accuracy and it now works properly with Beacon of Virtue.
08-04-2017 - Total healing done count now includes absorbed healing.
07-04-2017 - New layout with many usability improvements!
04-04-2017 - Add an Always Be Casting (ABC) module that checks your Non healing time and dead GCD time (this is shown in the tooltip).
29-03-2017 - Fixed a bug where Maraad's healing statistic would show 0% healing after getting only 1 Maraad's charge.
29-03-2017 - Update healing bonuses to the 7.2 values (DoS & Ilterendi nerfs).
27-03-2017 - Added Maraad's Dying Breath healing statistic.
27-03-2017 - Added Prydaz and Obsidian Stone Spaulders healing statistic.
26-03-2017 - Added Chain of Thrayn healing statistic.
26-03-2017 - Completely refactor the core, rename to Holy Paladin Analyzer.
25-03-2017 - Added cast ratios statistic.
`.split('\n');

class Changelog extends React.PureComponent {
  constructor() {
    super();
    this.state = {
      expanded: false,
    };
  }

  render() {
    const limit = this.state.expanded ? null : 5;

    return (

        {changes.filter((_, i) => limit === null || i <= limit).map(change => (

        ))}
        {limit !== null && (
           this.setState({ expanded: true })}>More
        )}

    );
  }
}

export default Changelog;
<commit_msg>Changelog<commit_after>import React from 'react';

const changes = `
07-05-2017 - Untangled many lines - you shouldn't notice a difference.
05-05-2017 - Added Aura of Mercy and Sacrifice healing done statistic.
05-05-2017 - Changed the report selection interface.
04-05-2017 - Minor importance suggestions are now hidden behind a toggle by default.
01-05-2017 - Added overhealing suggestions and relaxed the non healing time / dead GCD time suggestions (they should be marked as minor issues more often).
27-04-2017 - Prevent caching of fights list so it updates with live logs. You can now directly link specific tabs on the results page.
23-04-2017 - Changed the LotM suggestion to be based on CPM and be much more forgiving. Added death indicators to the mana graph.
21-04-2017 - Added HPS numbers to item healing done. Damaging HS are now included in cast counts. Recommended cast efficiency of CS with CM reduced to 30%. The recommended unused IoL is now adjusted based on if you have CM, DP and/or 4PT19.
21-04-2017 - Added suggestion/issue importance indicators and show a positive message when there are no major issues.
20-04-2017 - Since Easter is over bosses will no longer resurrect in the mana graph<dfn data-tip="This may cause issues with bosses that do actually resurrect, but I will fix that once I encounter such a boss.">*</dfn>. All bosses in a fight get shown instead of just the first<dfn data-tip="If boss adds are classified as bosses they will be shown as well. So far this doesn't appear to happen, but I will fix it if I encounter this.">*</dfn>. This should make the boss health for Mythic Botanist work properly.
19-04-2017 - Added a Mana tab for analyzing your mana usage. This also shows boss health since it is often used as a target for mana levels.
18-04-2017 - Added Velen's Future Sight to cast efficiency. Fixed the parser crashing instead of showing a descriptive error when selected player doesn't appear in a log. Fixed a crash when buff with stacks expired without ever applying.
17-04-2017 - Added LotM suggestion when casting it too often, added Divine Protection and BoS to cast efficiency display. Added Divine Purpose proc indicator.
15-04-2017 - Added Tyr's Deliverance healing contribution.
15-04-2017 - Added Sacred Dawn healing contribution. Note that (when SD wasn't up before) Sacred Dawn increases the Light of Dawn healing on yourself on the initial cast, but other players get healed for the normal amount without the SD healing increase. The calculation is aware of this.
15-04-2017 - Cast efficiency now takes the extra amount of fight time you had to cast an ability into consideration (e.g. a single cast of 3 minute cooldown during a 3:30 fight will have a higher cast efficiency than during a 5:30 fight).
15-04-2017 - Added a Talents tab.
14-04-2017 - Added Suggestions which will give you (way too many) suggestions to improve. This may need to be tuned a bit; any feedback in that regard is welcome. Moved Cast efficiency and Mastery effectiveness player breakdown to separate tabs.
11-04-2017 - Added Rule of Law to cast efficiency and grouped spells by category.
10-04-2017 - Show an error when parsing crashes (usually caused by not having advanced combat logging on). Renamed Casts Per Minute to Cast Efficiency. Show absolute amount of casts in Cast Efficiency. Added Arcane Torrent to Cast Efficiency (only shown if you cast it at least once).
09-04-2017 - Added Casts Per Minute table with very basic recommendations.
08-04-2017 - Added Heals on beacon statistic.
08-04-2017 - Added Wowhead tooltips and show T19 4 set bonus gain.
08-04-2017 - Added Beacon of the Lightbringer mastery radius support!
08-04-2017 - Improve beacon healing tracking accuracy and it now works properly with Beacon of Virtue.
08-04-2017 - Total healing done count now includes absorbed healing.
07-04-2017 - New layout with many usability improvements!
04-04-2017 - Add an Always Be Casting (ABC) module that checks your Non healing time and dead GCD time (this is shown in the tooltip).
29-03-2017 - Fixed a bug where Maraad's healing statistic would show 0% healing after getting only 1 Maraad's charge.
29-03-2017 - Update healing bonuses to the 7.2 values (DoS & Ilterendi nerfs).
27-03-2017 - Added Maraad's Dying Breath healing statistic.
27-03-2017 - Added Prydaz and Obsidian Stone Spaulders healing statistic.
26-03-2017 - Added Chain of Thrayn healing statistic.
26-03-2017 - Completely refactor the core, rename to Holy Paladin Analyzer.
25-03-2017 - Added cast ratios statistic.
`.split('\n');

class Changelog extends React.PureComponent {
  constructor() {
    super();
    this.state = {
      expanded: false,
    };
  }

  render() {
    const limit = this.state.expanded ? null : 5;

    return (

        {changes.filter((_, i) => limit === null || i <= limit).map(change => (

        ))}
        {limit !== null && (
           this.setState({ expanded: true })}>More
        )}

    );
  }
}

export default Changelog;
<|endoftext|>