<commit_before>function initType(a,b,c,d,e,f,g,h,i,j,k,l){}//IGNORE
function initTypeProtoI(a,b,c,d,e,f){}//IGNORE
function initTypeProto(a,b,c,d,e,f,g){}//IGNORE
function inheritProto(a,b,c){}//IGNORE
function exists(x){}//IGNORE
function Exception(){}//IGNORE
function isOfType(a,b){}//IGNORE
function getBottom(){}//IGNORE
function String$(x,l){}//IGNORE
function TypeCategory(a,b){}//IGNORE
function ArraySequence(x){}//IGNORE
var exports,Container,$finished,Cloneable,smaller,larger,Correspondence,Object$,IdentifiableObject;//IGNORE
var Iterable,Iterator;//IGNORE

function Sized(wat) {
    return wat;
}
initTypeProtoI(Sized, 'ceylon.language.Sized', Container);
Sized.$$.prototype.getEmpty = function() {
    return this.getSize() == 0;
}
exports.Sized=Sized;

function Category(wat) {
    return wat;
}
initType(Category, 'ceylon.language.Category');
Category.$$.prototype.containsEvery = function(keys) {
    for (var i = 0; i < keys.length; i++) {
        if (!this.contains(keys[i])) {
            return false;
        }
    }
    return true;
}
Category.$$.prototype.containsAny = function(keys) {
    for (var i = 0; i < keys.length; i++) {
        if (this.contains(keys[i])) {
            return true;
        }
    }
    return false;
}
exports.Category=Category;

function Collection(wat) {
    return wat;
}
initTypeProtoI(Collection, 'ceylon.language.Collection', Iterable, Sized, Category, Cloneable);
var Collection$proto = Collection.$$.prototype;
Collection$proto.contains = function(obj) {
    var iter = this.getIterator();
    var item;
    while ((item = iter.next()) !== $finished) {
        if (exists(item) && item.equals(obj)) {
            return true;
        }
    }
    return false;
}
exports.Collection=Collection;

function FixedSized(wat) {
    return wat;
}
initTypeProtoI(FixedSized, 'ceylon.language.FixedSized', Collection);
var FixedSized$proto = FixedSized.$$.prototype;
FixedSized$proto.getFirst = function() {
    var e = this.getIterator().next();
    return e === $finished ? null : e;
}
exports.FixedSized=FixedSized;

function Some(wat) {
    return wat;
}
initTypeProtoI(Some, 'ceylon.language.Some', FixedSized, ContainerWithFirstElement);
var $Some = Some.$$;
$Some.prototype.getFirst = function() {
    var e = this.getIterator().next();
    if (e === $finished) throw Exception();
    return e;
}
$Some.prototype.getEmpty = function() { return false; }
$Some.prototype.getFirst = function() {
    var _e = this.getIterator().next();
    if (_e === $finished) throw Exception(String$("Some.first should never get Finished!"));
    return _e;
}
exports.Some=Some;

function None(wat) {
    return wat;
}
initTypeProtoI(None, 'ceylon.language.None', FixedSized, ContainerWithFirstElement);
var None$proto = None.$$.prototype;
None$proto.getFirst = function() { return null; }
None$proto.getIterator = function() { return emptyIterator; }
None$proto.getSize = function() { return 0; }
None$proto.getEmpty = function() { return true; }
None$proto.getFirst = function() { return null; }
exports.None=None;

function Ranged(wat) {
    return wat;
}
initType(Ranged, 'ceylon.language.Ranged');
exports.Ranged=Ranged;

function List(wat) {
    return wat;
}
initTypeProtoI(List, 'ceylon.language.List', Collection, Correspondence, Ranged, Cloneable);
var List$proto = List.$$.prototype;
List$proto.getSize = function() {
    var li = this.getLastIndex();
    return li === null ? 0 : li.getSuccessor();
}
List$proto.defines = function(idx) {
    var li = this.getLastIndex();
    if (li === null) li = -1;
    return li.compare(idx) !== smaller;
}
List$proto.getIterator = function() {
    return ListIterator(this);
}
List$proto.equals = function(other) {
    if (isOfType(other, 'ceylon.language.List') && other.getSize().equals(this.getSize())) {
        for (var i = 0; i < this.getSize(); i++) {
            var mine = this.item(i);
            var theirs = other.item(i);
            if (((mine === null) && theirs) || !(mine && mine.equals(theirs))) {
                return false;
            }
        }
        return true;
    }
    return false;
}
List$proto.getHash = function() {
    var hc=1;
    var iter=this.getIterator();
    var e; while ((e = iter.next()) != $finished) {
        hc*=31;
        if (e !== null) {
            hc += e.getHash();
        }
    }
    return hc;
}
List$proto.getString = function() {
    var s = '{';
    var first = true;
    var iter = this.getIterator();
    var item;
    while ((item = iter.next()) !== $finished) {
        s += first ? ' ' : ', ';
        if (exists(item)) {
            s += item.getString();
        } else {
            s += 'null';
        }
        first = false;
    }
    if (!first) {
        s += ' ';
    }
    s += '}';
    return String$(s);
}
List$proto.findLast = function(select) {
    var li = this.getLastIndex();
    if (li !== null) {
        while (li>=0) {
            var e = this.item(li);
            if (e !== null && select(e)) {
                return e;
            }
            li = li.getPredecessor();
        }
    }
    return null;
}
List$proto.withLeading = function(other) {
    var sb = SequenceBuilder();
    sb.append(other);
    sb.appendAll(this);
    return sb.getSequence();
}
List$proto.withTrailing = function(other) {
    var sb = SequenceBuilder();
    sb.appendAll(this);
    sb.append(other);
    return sb.getSequence();
}
exports.List=List;

function ListIterator(list) {
    var that = new ListIterator.$$;
    that.list=list;
    that.index=0;
    that.lastIndex=list.getLastIndex();
    if (that.lastIndex === null) {
        that.lastIndex = -1;
    } else {
        that.lastIndex = that.lastIndex;
    }
    return that;
}
initTypeProtoI(ListIterator, 'ceylon.language.ListIterator', Iterator);
ListIterator.$$.prototype.next = function() {
    if (this.index <= this.lastIndex) {
        return this.list.item(this.index++);
    }
    return $finished;
}

function Empty() {
    var that = new Empty.$$;
    that.value = [];
    return that;
}
initTypeProtoI(Empty, 'ceylon.language.Empty', None, Ranged, Cloneable, List);
var Empty$proto = Empty.$$.prototype;
Empty$proto.getEmpty = function() { return true; }
Empty$proto.defines = function(x) { return false; }
Empty$proto.getKeys = function() { return TypeCategory(this, 'ceylon.language.Integer'); }
Empty$proto.definesEvery = function(x) { return false; }
Empty$proto.definesAny = function(x) { return false; }
Empty$proto.items = function(x) { return this; }
Empty$proto.getSize = function() { return 0; }
Empty$proto.item = function(x) { return null; }
Empty$proto.getFirst = function() { return null; }
Empty$proto.segment = function(a,b) { return this; }
Empty$proto.span = function(a,b) { return this; }
Empty$proto.getIterator = function() { return emptyIterator; }
Empty$proto.getString = function() { return String$("{}"); }
Empty$proto.contains = function(x) { return false; }
Empty$proto.getLastIndex = function() { return null; }
Empty$proto.getClone = function() { return this; }
Empty$proto.count = function(x) { return 0; }
Empty$proto.getReversed = function() { return this; }
Empty$proto.skipping = function(skip) { return this; }
Empty$proto.taking = function(take) { return this; }
Empty$proto.by = function(step) { return this; }
Empty$proto.$every = function(f) { return false; }
Empty$proto.any = function(f) { return false; }
Empty$proto.$sort = function(f) { return this; }
Empty$proto.$map = function(f) { return this; }
Empty$proto.fold = function(i,r) { return i; }
Empty$proto.find = function(f) { return null; }
Empty$proto.findLast = function(f) { return null; }
Empty$proto.$filter = function(f) { return this; }
Empty$proto.getCoalesced = function() { return this; }
Empty$proto.getIndexed = function() { return this; }
Empty$proto.withLeading = function(other) {
    return new ArraySequence([other]);
}
Empty$proto.withTrailing = function(other) {
    return new ArraySequence([other]);
}
Empty$proto.chain = function(other) { return other; }

var $empty = Empty();

function EmptyIterator() {
    var that = new EmptyIterator.$$;
    return that;
}
initTypeProto(EmptyIterator, 'ceylon.language.EmptyIterator', IdentifiableObject, Iterator);
var EmptyIterator$proto = EmptyIterator.$$.prototype;
EmptyIterator$proto.next = function() { return $finished; }
var emptyIterator=EmptyIterator();

exports.empty=$empty;
exports.Empty=Empty;
exports.emptyIterator=emptyIterator;

function Comprehension(makeNextFunc, compr) {
    if (compr===undefined) {compr = new Comprehension.$$;}
    IdentifiableObject(compr);
    compr.makeNextFunc = makeNextFunc;
    return compr;
}
initTypeProto(Comprehension, 'ceylon.language.Comprehension', IdentifiableObject, Iterable);
var Comprehension$proto = Comprehension.$$.prototype;
Comprehension$proto.getIterator = function() {
    return ComprehensionIterator(this.makeNextFunc());
}
exports.Comprehension=Comprehension;

function ComprehensionIterator(nextFunc, it) {
    if (it===undefined) {it = new ComprehensionIterator.$$;}
    IdentifiableObject(it);
    it.next = nextFunc;
    return it;
}
initTypeProto(ComprehensionIterator, 'ceylon.language.ComprehensionIterator',
        IdentifiableObject, Iterator);

function ChainedIterator(first, second, chained) {
    if (chained===undefined) {chained = new ChainedIterator.$$;}
    IdentifiableObject(chained);
    chained.it = first.getIterator();
    chained.second = second;
    return chained;
}
initTypeProto(ChainedIterator, 'ceylon.language.ChainedIterator',
        IdentifiableObject, Iterator);
var ChainedIterator$proto = ChainedIterator.$$.prototype;
ChainedIterator$proto.next = function() {
    var elem = this.it.next();
    if ((elem===$finished) && (this.second!==undefined)) {
        this.it = this.second.getIterator();
        this.second = undefined;
        elem = this.it.next();
    }
    return elem;
}
exports.ChainedIterator=ChainedIterator;
<commit_msg>implement LazyList (#100)<commit_after>function initType(a,b,c,d,e,f,g,h,i,j,k,l){}//IGNORE
function initTypeProtoI(a,b,c,d,e,f){}//IGNORE
function initTypeProto(a,b,c,d,e,f,g){}//IGNORE
function inheritProto(a,b,c){}//IGNORE
function exists(x){}//IGNORE
function Exception(){}//IGNORE
function isOfType(a,b){}//IGNORE
function getBottom(){}//IGNORE
function String$(x,l){}//IGNORE
function TypeCategory(a,b){}//IGNORE
function ArraySequence(x){}//IGNORE
var exports,Container,$finished,Cloneable,smaller,larger,Correspondence,Object$,IdentifiableObject;//IGNORE
var Iterable,Iterator;//IGNORE

function Sized(wat) {
    return wat;
}
initTypeProtoI(Sized, 'ceylon.language.Sized', Container);
Sized.$$.prototype.getEmpty = function() {
    return this.getSize() == 0;
}
exports.Sized=Sized;

function Category(wat) {
    return wat;
}
initType(Category, 'ceylon.language.Category');
Category.$$.prototype.containsEvery = function(keys) {
    for (var i = 0; i < keys.length; i++) {
        if (!this.contains(keys[i])) {
            return false;
        }
    }
    return true;
}
Category.$$.prototype.containsAny = function(keys) {
    for (var i = 0; i < keys.length; i++) {
        if (this.contains(keys[i])) {
            return true;
        }
    }
    return false;
}
exports.Category=Category;

function Collection(wat) {
    return wat;
}
initTypeProtoI(Collection, 'ceylon.language.Collection', Iterable, Sized, Category, Cloneable);
var Collection$proto = Collection.$$.prototype;
Collection$proto.contains = function(obj) {
    var iter = this.getIterator();
    var item;
    while ((item = iter.next()) !== $finished) {
        if (exists(item) && item.equals(obj)) {
            return true;
        }
    }
    return false;
}
exports.Collection=Collection;

function FixedSized(wat) {
    return wat;
}
initTypeProtoI(FixedSized, 'ceylon.language.FixedSized', Collection);
var FixedSized$proto = FixedSized.$$.prototype;
FixedSized$proto.getFirst = function() {
    var e = this.getIterator().next();
    return e === $finished ? null : e;
}
exports.FixedSized=FixedSized;

function Some(wat) {
    return wat;
}
initTypeProtoI(Some, 'ceylon.language.Some', FixedSized, ContainerWithFirstElement);
var $Some = Some.$$;
$Some.prototype.getFirst = function() {
    var e = this.getIterator().next();
    if (e === $finished) throw Exception();
    return e;
}
$Some.prototype.getEmpty = function() { return false; }
$Some.prototype.getFirst = function() {
    var _e = this.getIterator().next();
    if (_e === $finished) throw Exception(String$("Some.first should never get Finished!"));
    return _e;
}
exports.Some=Some;

function None(wat) {
    return wat;
}
initTypeProtoI(None, 'ceylon.language.None', FixedSized, ContainerWithFirstElement);
var None$proto = None.$$.prototype;
None$proto.getFirst = function() { return null; }
None$proto.getIterator = function() { return emptyIterator; }
None$proto.getSize = function() { return 0; }
None$proto.getEmpty = function() { return true; }
None$proto.getFirst = function() { return null; }
exports.None=None;

function Ranged(wat) {
    return wat;
}
initType(Ranged, 'ceylon.language.Ranged');
exports.Ranged=Ranged;

function List(wat) {
    return wat;
}
initTypeProtoI(List, 'ceylon.language.List', Collection, Correspondence, Ranged, Cloneable);
var List$proto = List.$$.prototype;
List$proto.getSize = function() {
    var li = this.getLastIndex();
    return li === null ? 0 : li.getSuccessor();
}
List$proto.defines = function(idx) {
    var li = this.getLastIndex();
    if (li === null) li = -1;
    return li.compare(idx) !== smaller;
}
List$proto.getIterator = function() {
    return ListIterator(this);
}
List$proto.equals = function(other) {
    if (isOfType(other, 'ceylon.language.List') && other.getSize().equals(this.getSize())) {
        for (var i = 0; i < this.getSize(); i++) {
            var mine = this.item(i);
            var theirs = other.item(i);
            if (((mine === null) && theirs) || !(mine && mine.equals(theirs))) {
                return false;
            }
        }
        return true;
    }
    return false;
}
List$proto.getHash = function() {
    var hc=1;
    var iter=this.getIterator();
    var e; while ((e = iter.next()) != $finished) {
        hc*=31;
        if (e !== null) {
            hc += e.getHash();
        }
    }
    return hc;
}
List$proto.getString = function() {
    var s = '{';
    var first = true;
    var iter = this.getIterator();
    var item;
    while ((item = iter.next()) !== $finished) {
        s += first ? ' ' : ', ';
        if (exists(item)) {
            s += item.getString();
        } else {
            s += 'null';
        }
        first = false;
    }
    if (!first) {
        s += ' ';
    }
    s += '}';
    return String$(s);
}
List$proto.findLast = function(select) {
    var li = this.getLastIndex();
    if (li !== null) {
        while (li>=0) {
            var e = this.item(li);
            if (e !== null && select(e)) {
                return e;
            }
            li = li.getPredecessor();
        }
    }
    return null;
}
List$proto.withLeading = function(other) {
    var sb = SequenceBuilder();
    sb.append(other);
    sb.appendAll(this);
    return sb.getSequence();
}
List$proto.withTrailing = function(other) {
    var sb = SequenceBuilder();
    sb.appendAll(this);
    sb.append(other);
    return sb.getSequence();
}
exports.List=List;

function ListIterator(list) {
    var that = new ListIterator.$$;
    that.list=list;
    that.index=0;
    that.lastIndex=list.getLastIndex();
    if (that.lastIndex === null) {
        that.lastIndex = -1;
    } else {
        that.lastIndex = that.lastIndex;
    }
    return that;
}
initTypeProtoI(ListIterator, 'ceylon.language.ListIterator', Iterator);
ListIterator.$$.prototype.next = function() {
    if (this.index <= this.lastIndex) {
        return this.list.item(this.index++);
    }
    return $finished;
}

function Empty() {
    var that = new Empty.$$;
    that.value = [];
    return that;
}
initTypeProtoI(Empty, 'ceylon.language.Empty', None, Ranged, Cloneable, List);
var Empty$proto = Empty.$$.prototype;
Empty$proto.getEmpty = function() { return true; }
Empty$proto.defines = function(x) { return false; }
Empty$proto.getKeys = function() { return TypeCategory(this, 'ceylon.language.Integer'); }
Empty$proto.definesEvery = function(x) { return false; }
Empty$proto.definesAny = function(x) { return false; }
Empty$proto.items = function(x) { return this; }
Empty$proto.getSize = function() { return 0; }
Empty$proto.item = function(x) { return null; }
Empty$proto.getFirst = function() { return null; }
Empty$proto.segment = function(a,b) { return this; }
Empty$proto.span = function(a,b) { return this; }
Empty$proto.getIterator = function() { return emptyIterator; }
Empty$proto.getString = function() { return String$("{}"); }
Empty$proto.contains = function(x) { return false; }
Empty$proto.getLastIndex = function() { return null; }
Empty$proto.getClone = function() { return this; }
Empty$proto.count = function(x) { return 0; }
Empty$proto.getReversed = function() { return this; }
Empty$proto.skipping = function(skip) { return this; }
Empty$proto.taking = function(take) { return this; }
Empty$proto.by = function(step) { return this; }
Empty$proto.$every = function(f) { return false; }
Empty$proto.any = function(f) { return false; }
Empty$proto.$sort = function(f) { return this; }
Empty$proto.$map = function(f) { return this; }
Empty$proto.fold = function(i,r) { return i; }
Empty$proto.find = function(f) { return null; }
Empty$proto.findLast = function(f) { return null; }
Empty$proto.$filter = function(f) { return this; }
Empty$proto.getCoalesced = function() { return this; }
Empty$proto.getIndexed = function() { return this; }
Empty$proto.withLeading = function(other) {
    return new ArraySequence([other]);
}
Empty$proto.withTrailing = function(other) {
    return new ArraySequence([other]);
}
Empty$proto.chain = function(other) { return other; }

var $empty = Empty();

function EmptyIterator() {
    var that = new EmptyIterator.$$;
    return that;
}
initTypeProto(EmptyIterator, 'ceylon.language.EmptyIterator', IdentifiableObject, Iterator);
var EmptyIterator$proto = EmptyIterator.$$.prototype;
EmptyIterator$proto.next = function() { return $finished; }
var emptyIterator=EmptyIterator();

exports.empty=$empty;
exports.Empty=Empty;
exports.emptyIterator=emptyIterator;

function Comprehension(makeNextFunc, compr) {
    if (compr===undefined) {compr = new Comprehension.$$;}
    IdentifiableObject(compr);
    compr.makeNextFunc = makeNextFunc;
    return compr;
}
initTypeProto(Comprehension, 'ceylon.language.Comprehension', IdentifiableObject, Iterable);
var Comprehension$proto = Comprehension.$$.prototype;
Comprehension$proto.getIterator = function() {
    return ComprehensionIterator(this.makeNextFunc());
}
exports.Comprehension=Comprehension;

function ComprehensionIterator(nextFunc, it) {
    if (it===undefined) {it = new ComprehensionIterator.$$;}
    IdentifiableObject(it);
    it.next = nextFunc;
    return it;
}
initTypeProto(ComprehensionIterator, 'ceylon.language.ComprehensionIterator',
        IdentifiableObject, Iterator);

function ChainedIterator(first, second, chained) {
    if (chained===undefined) {chained = new ChainedIterator.$$;}
    IdentifiableObject(chained);
    chained.it = first.getIterator();
    chained.second = second;
    return chained;
}
initTypeProto(ChainedIterator, 'ceylon.language.ChainedIterator',
        IdentifiableObject, Iterator);
var ChainedIterator$proto = ChainedIterator.$$.prototype;
ChainedIterator$proto.next = function() {
    var elem = this.it.next();
    if ((elem===$finished) && (this.second!==undefined)) {
        this.it = this.second.getIterator();
        this.second = undefined;
        elem = this.it.next();
    }
    return elem;
}
exports.ChainedIterator=ChainedIterator;

function LazyList(elems, lst) {
    if (lst===undefined) {lst = new LazyList.$$;}
    IdentifiableObject(lst);
    lst.elems = elems;
    return lst;
}
initTypeProto(LazyList, 'ceylon.language.LazyList', IdentifiableObject, List);
var LazyList$proto = LazyList.$$.prototype;
LazyList$proto.getIterator = function() { return this.elems.getIterator(); }
LazyList$proto.getClone = function() { return this; }
LazyList$proto.getFirst = function() { return this.elems.getFirst(); }
LazyList$proto.getLast = function() { return this.elems.getLast(); }
LazyList$proto.findLast = function(selecting) {
    return this.elems.findLast(selecting);
}
LazyList$proto.getSize = function() {
    if (isOfType(this.elems, 'ceylon.language.Sized')) {
        return this.elems.getSize();
    }
    var it = this.elems.getIterator();
    var count = 0;
    while (it.next() !== $finished) {++count;}
    return count;
}
LazyList$proto.getLastIndex = function() {
    var size = this.getSize();
    return size>0 ? (size-1) : null;
}
LazyList$proto.getEmpty = function() { return this.elems.getEmpty(); }
LazyList$proto.item = function(index) {
    var it = this.elems.getIterator();
    var elem;
    var count = 0;
    while ((elem=it.next()) !== $finished) {
        if (count++ == index) { return elem; }
    }
    return null;
}
LazyList$proto.getReversed = function() { return this.elems.getSequence().getReversed(); }
LazyList$proto.equals = function(other) {
    if (!isOfType(other, 'ceylon.language.List')) { return false; }
    var it1 = this.elems.getIterator();
    var it2 = other.getIterator();
    var e1;
    while ((e1=it1.next()) !== $finished) {
        var e2 = it2.next();
        var n1 = e1===null;
        if ((n1 ^ (e2===null)) || !(n1 || e1.equals(e2))) { return false; }
    }
    return it2.next()===$finished;
}
LazyList$proto.segment = function(from, length) {
    if (length <= 0) { return $empty; }
    var seg = (from > 0) ? this.elems.skipping(from) : this.elems;
    return LazyList(seg.taking(length));
}
LazyList$proto.span = function(from, to) {
    if ((to === undefined) || (to === null)) {
        return (from > 0) ? LazyList(this.elems.skipping(from)) : this;
    }
    if (to < 0) { to = 0; }
    if (from < 0) { from = 0; }
    if (to < from) {
        var seg = (to > 0) ? this.elems.skipping(to) : this.elems;
        return seq.taking(from-to+1).getSequence().getReversed();
    }
    var seq = (from > 0) ? this.elems.skipping(from) : this.elems;
    return LazyList(seq.taking(to-from+1));
}
exports.LazyList=LazyList;<|endoftext|>


<commit_before>/* ===========================================================
 * trumbowyg.activecontrols.js v1.0
 * Plugin to display active formattig buttons in buttons pane
 * http://alex-d.github.com/Trumbowyg
 * ===========================================================
 * Author : Rastislav Švarba (ra100)
 *          Twitter : @ra100
 *          Website : ra100.net
 */

(function ($) {
    'use strict';

    $.trumbowyg.tagToButton = {
        'strong': 'strong',
        'b': 'strong',
        'em': 'em',
        'i': 'em',
        'p': 'p',
        'center': 'justifyCenter',
        'left': 'justifyLeft',
        'right': 'justifyRight',
        'justify': 'justifyFull',
        'ol': 'orderedList',
        'ul': 'unorderedList',
        'a': 'link',
        'img': 'insertImage',
        'u': 'underline',
        'strike': 'strikethrough',
        'del': 'del',
        'h1': 'h1',
        'h2': 'h2',
        'h3': 'h3',
        'h4': 'h4',
        'h5': 'h5',
        'h6': 'h6',
        'blockquote': 'blockquote'
    };

    $.extend(true, $.trumbowyg, {
        opts: {
            on: {
                activecontrols: {
                    events: 'mouseup keydown',
                    handler: function (event, t) {
                        t.saveSelection();
                        if (t.selection !== null) {
                            t.o.controlButtonActivate(t.selection.commonAncestorContainer.parentNode, t);
                        }
                    }
                }
            },
            activeTags: [],

            // Active tags
            controlButtonActivate: function (element, t) {
                var tags = [],
                    newTags = [];
                t.o.getTagsRecursive(element, tags);
                newTags = t.o.activeTags.filter(function (val) {
                    if (tags.indexOf(val) < 0) {
                        t.$btnPane.find('.' + t.o.prefix + t.tagToButton[val.toLowerCase()] + '-button').removeClass('active');
                        return false;
                    }
                });
                for (var i in tags) {
                    var tag = tags[i];
                    if (newTags.indexOf(tag) < 0) {
                        t.$btnPane.find('.' + t.o.prefix + t.tagToButton[tag.toLowerCase()] + '-button').addClass('active');
                    }
                }
                t.o.activeTags = tags;
            },
            getTagsRecursive: function (element, tags) {
                var tag = element.tagName;
                if (tag === 'DIV')
                    return;
                if (tag === 'P' && element.style.textAlign !== "") {
                    tags.push(element.style.textAlign);
                }
                tags.push(tag);
                this.getTagsRecursive(element.parentNode, tags);
            }
        }
    });
})(jQuery);
<commit_msg>fix t.tagToButton -> $.trumbowyg.tagToButton<commit_after>/* ===========================================================
 * trumbowyg.activecontrols.js v1.0
 * Plugin to display active formattig buttons in buttons pane
 * http://alex-d.github.com/Trumbowyg
 * ===========================================================
 * Author : Rastislav Švarba (ra100)
 *          Twitter : @ra100
 *          Website : ra100.net
 */

(function ($) {
    'use strict';

    $.trumbowyg.tagToButton = {
        'strong': 'strong',
        'b': 'strong',
        'em': 'em',
        'i': 'em',
        'p': 'p',
        'center': 'justifyCenter',
        'left': 'justifyLeft',
        'right': 'justifyRight',
        'justify': 'justifyFull',
        'ol': 'orderedList',
        'ul': 'unorderedList',
        'a': 'link',
        'img': 'insertImage',
        'u': 'underline',
        'strike': 'strikethrough',
        'del': 'del',
        'h1': 'h1',
        'h2': 'h2',
        'h3': 'h3',
        'h4': 'h4',
        'h5': 'h5',
        'h6': 'h6',
        'blockquote': 'blockquote'
    };

    $.extend(true, $.trumbowyg, {
        opts: {
            on: {
                activecontrols: {
                    events: 'mouseup keydown',
                    handler: function (event, t) {
                        t.saveSelection();
                        if (t.selection !== null) {
                            t.o.controlButtonActivate(t.selection.commonAncestorContainer.parentNode, t);
                        }
                    }
                }
            },
            activeTags: [],

            // Active tags
            controlButtonActivate: function (element, t) {
                var tags = [],
                    newTags = [];
                t.o.getTagsRecursive(element, tags);
                newTags = t.o.activeTags.filter(function (val) {
                    if (tags.indexOf(val) < 0) {
                        t.$btnPane.find('.' + t.o.prefix + $.trumbowyg.tagToButton[val.toLowerCase()] + '-button').removeClass('active');
                        return false;
                    }
                });
                for (var i in tags) {
                    var tag = tags[i];
                    if (newTags.indexOf(tag) < 0) {
                        t.$btnPane.find('.' + t.o.prefix + $.trumbowyg.tagToButton[tag.toLowerCase()] + '-button').addClass('active');
                    }
                }
                t.o.activeTags = tags;
            },
            getTagsRecursive: function (element, tags) {
                var tag = element.tagName;
                if (tag === 'DIV')
                    return;
                if (tag === 'P' && element.style.textAlign !== "") {
                    tags.push(element.style.textAlign);
                }
                tags.push(tag);
                this.getTagsRecursive(element.parentNode, tags);
            }
        }
    });
})(jQuery);
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2012-2013 André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */

(function Cyclic(global) {
"use strict";

const Object = global.Object,
      Function = global.Function,
      Array = global.Array,
      Error = global.Error,
      Set = global.Set,
      WeakMap = global.WeakMap;

const Array_prototype_join = Array.prototype.join,
      Array_prototype_toLocaleString = Array.prototype.toLocaleString,
      Error_prototype_toString = Error.prototype.toString;

const Function_call = Function.prototype.call.bind(Function.prototype.call);
const wm = new WeakMap(), cs = new Set();

function checkCycle(fn, o, ...args) {
  if (typeof this == 'function' || typeof o == 'object' && o !== null) {
    if (wm.has(o)) {
      return "";
    }
    wm.set(o, null);
  } else {
    if (cs.has(o)) {
      return "" + o;
    }
    cs.add(o);
  }
  try {
    return Function_call(fn, o, ...args);
  } finally {
    if (typeof this == 'function' || typeof o == 'object' && o !== null) {
      wm.delete(o);
    } else {
      cs.delete(o);
    }
  }
}

Object.defineProperty(Object.assign(Array.prototype, {
  join(separator) {
    return checkCycle(Array_prototype_join, this, separator);
  }
}), "join", {enumerable: false});

Object.defineProperty(Object.assign(Array.prototype, {
  toLocaleString() {
    return checkCycle(Array_prototype_toLocaleString, this);
  }
}), "toLocaleString", {enumerable: false});

Object.defineProperty(Object.assign(Error.prototype, {
  toString() {
    return checkCycle(Error_prototype_toString, this);
  }
}), "toString", {enumerable: false});

})(this);
<commit_msg>Fix copy-paste bug<commit_after>/*
 * Copyright (c) 2012-2013 André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */

(function Cyclic(global) {
"use strict";

const Object = global.Object,
      Function = global.Function,
      Array = global.Array,
      Error = global.Error,
      Set = global.Set,
      WeakMap = global.WeakMap;

const Array_prototype_join = Array.prototype.join,
      Array_prototype_toLocaleString = Array.prototype.toLocaleString,
      Error_prototype_toString = Error.prototype.toString;

const Function_call = Function.prototype.call.bind(Function.prototype.call);
const wm = new WeakMap(), cs = new Set();

function checkCycle(fn, o, ...args) {
  if (typeof o == 'function' || typeof o == 'object' && o !== null) {
    if (wm.has(o)) {
      return "";
    }
    wm.set(o, null);
  } else {
    if (cs.has(o)) {
      return "" + o;
    }
    cs.add(o);
  }
  try {
    return Function_call(fn, o, ...args);
  } finally {
    if (typeof o == 'function' || typeof o == 'object' && o !== null) {
      wm.delete(o);
    } else {
      cs.delete(o);
    }
  }
}

Object.defineProperty(Object.assign(Array.prototype, {
  join(separator) {
    return checkCycle(Array_prototype_join, this, separator);
  }
}), "join", {enumerable: false});

Object.defineProperty(Object.assign(Array.prototype, {
  toLocaleString() {
    return checkCycle(Array_prototype_toLocaleString, this);
  }
}), "toLocaleString", {enumerable: false});

Object.defineProperty(Object.assign(Error.prototype, {
  toString() {
    return checkCycle(Error_prototype_toString, this);
  }
}), "toString", {enumerable: false});

})(this);
<|endoftext|>


<commit_before>/**
  * Copyright (c) 2012 Ivo Wetzel.
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  * copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  * THE SOFTWARE.
  */

// Imports --------------------------------------------------------------------
var BISON = require('./lib/bison'),
    ObjectList = require('./lib/ObjectList'),
    WebSocketServer = require('./lib/WebSocket'),
    Maple = require('./Maple');

// Main Server ----------------------------------------------------------------
// ----------------------------------------------------------------------------
Maple.Server = Maple.Class(function(clientClass, messageTypes) {

    // Clients
    this._socket = null;
    this._clients = new ObjectList();
    this._clientClass = clientClass || Maple.ServerClient;
    this._messageTypes = messageTypes || {};

}, {

    $version: '0.2',

    /**
      * {Booelan} Starts the server in case it's not already running.
      *
      * Returns `true` on success.
      */
    start: function(options) {

        if (this.isRunning()) {
            return false;
        }

        this._bytesSend = 0;

        // Ticking
        this._tickTime = 0;
        this._tickRate = Math.floor(1000 / (options.tickRate || 30));
        this._tickCount = 0;
        this._tickInterval = null;

        // Game Time
        this._startTime = -1;
        this._realTime = 0;
        this._frameTime = -1;

        // More logic stuff
        this._randomSeed = 500000 + Math.floor((Math.random() * 1000000));
        this._randomState = 0;
        this._logicRate = options.logicRate || 1; // Used for throttling logic ticks
        this._syncRate = options.syncRate || 30; // Send down tick count to clients every X updates

        this._isRunning = false;

        var that = this;
        this._tickInterval = setInterval(function() {
            that._update();

        }, this._tickRate);

        this._frameTime = Date.now();
        this._tickCount = 1;
        this._isRunning = true;

        if (options.socket !== false) {
            this._createSocket();
            this._socket.listen(options.port);
        }

        return true;

    },

    /**
      * {Boolean} Returns whether the server is running or not.
      */
    isRunning: function() {
        return this._isRunning;
    },

    /**
      * Broadcase a message.
      */
    broadcast: function(type, data, clients, excluded) {

        // Add type and tick to the message, unless we're just sending a
        // single Number for tick syncing
        if (type !== null) {

            var msg = [this.messageTypeToId(type), this.getTick()];
            if (data !== undefined) {
                msg.push.apply(msg, data);
            }

            data = msg;

        }

        data = BISON.encode(data);

        // Fall back to everyone!
        if (!clients) {
            clients = this._clients;
        }

        this._clients.each(function(client) {

            if (!excluded || excluded.indexOf(client) === -1) {
                this._bytesSend += client.sendRaw(data);
            }

        }, this);

    },

    stop: function() {

        if (!this.isRunning()) {
            return false;
        }

        this._isRunning = false;
        clearInterval(this._tickInterval);

        this.broadcast(Maple.Message.STOP);
        this._clients.each(function(client) {
            client.close();
        });

        if (this._socket) {
            this._socket.close();
        }

        return true;

    },

    _createSocket: function() {

        // Socket
        this._socket = new WebSocketServer();

        // Setup socket callbacks
        var that = this;
        this._socket.on('data', function(conn, raw, binary) {
            that._data(conn, raw);
        });

        this._socket.on('end', function(conn) {

            var client = that._clients.get(conn.clientId);
            if (client) {
                that._clients.remove(client);
                that.disconnected(client);
            }

        });

        this._socket.on('request', function(req, res) {
            that.requested(req, res);
        });

    },

    // Handling of incoming data and game logic -------------------------------
    _data: function(conn, raw) {

        // Do some basic filtering to prevent easy ways
        // of breaking the server
        var msg;
        try {
            msg = BISON.decode(raw);

        } catch(e) {
            this._error(conn, Maple.Error.INVALID_DATA);
            return;
        }

        if (msg.length < 2 || !(msg instanceof Array)) {
            this._error(conn, Maple.Error.MESSAGE_TOO_SHORT);
            return;
        }

        // Get message details
        var type = msg[0],
            tick = msg[1],
            data = msg.slice(2),
            client = this._clients.get(conn.clientId);

        // More checks for new connections
        if (type === Maple.Message.CONNECT) {

            if (!client) {

                // Throw up on unsupported version
                if (typeof data[0] !== 'string' || data[0] !== Maple.Server.$version) {
                    this._error(conn, Maple.Error.UNSUPPORTED_VERSION);

                // Add client to list and give the id to the connection
                } else {
                    client = new this._clientClass(this, conn, !!data[1]);
                    conn.clientId = this._clients.add(client);

                    this._bytesSend += client.send(Maple.Message.START, [
                        this._tickRate,
                        this._logicRate,
                        this._syncRate,
                        this._randomSeed,
                        this._messageTypes
                    ]);

                    this.connected(client);
                }

            } else {
                this._error(conn, Maple.Error.ALREADY_CONNECTED);
                this._clients.remove(client);
            }

        } else if (client) {

            // Handle time sync and ping detection
            // we simply echo back here
            if (type === Maple.Message.SYNC) {
                this._bytesSend += client.send(Maple.Message.SYNC, data);

            } else {

                type = this.messageTypeFromId(type);
                if (this.message(client, type, tick, data) !== true) {
                    client.message(type, tick, data);
                }

            }

        }

    },

    _error: function(conn, type) {
        this._bytesSend += conn.send(BISON.encode([Maple.Message.ERROR, 0, type]));
        conn.close();
    },

    _update: function() {

        var now = Date.now();

        this._realTime += (now - this._frameTime);
        while(this._tickTime < this._realTime && this.isRunning()) {

            if (this._startTime === -1) {
                this._startTime = Date.now();
                this.started();
            }

            // Sync clients tick count
            if (this._tickCount % this._syncRate === 0) {
                this.broadcast(null, this._tickCount % 250, this._players);
            }

            if (this._tickCount % this._logicRate === 0) {
                this.update(this._tickTime, this._tickCount);
            }

            this._tickCount++;
            this._randomState = this._tickCount;
            this._tickTime += this._tickRate;

        }

        this._frameTime = now;

    },

    // Abstract methods -------------------------------------------------------

    /**
      * Callback for when the "game" is started.
      */
    started: function() {

    },

    /**
      * The game update callback.
      *
      * - @t {Integer} is the current game time.
      * - @tick {Integer} is the current tick count.
      */
    update: function(time, tick) {

    },

    /**
      * Callback for when the "game" is stopped.
      */
    stopped: function() {

    },

    /**
      * Callback for when a client connects to the server.
      *
      * @client {Server.Client}
      */
    connected: function(client) {

    },

    /**
      * Callback for when a client disconnects from the server.
      *
      * @client {Server.Client}
      */
    disconnected: function(client) {

    },

    /**
      * {Boolean} Callback for any messages received from a @client.
      *
      * - @client {Server.Client} The client
      * - @type {Integer} Message type
      * - @tick {Integer} Client side tick at which the message was send.
      * - @data {Array} Message data
      *
      * Return `true` to indicate that the message was handled and prevent it
      * from being forwarded to {Maple.Client#message}.
      */
    message: function(client, type, tick, data) {

    },

    /**
      * Handler for HTTP Request which reach the server on the port its bound to.
      *
      * You can use this like the vanilla HTTP interface from Node.
      */
    requested: function(req, res) {

    },

    // Getter -----------------------------------------------------------------

    /**
      * {Integer} Returns the "synced" game time in milliseconds.
      *
      * Please note that this is not 100% accurate and should only be used for
      * client side logic like drawing and effects.
      *
      * For synced actions between server and clinet always use the `tick` count.
      */
    getTime: function() {
        return this._tickTime;
    },

    /**
      * {Integer} Returns the number of synced game ticks that happend since the
      *           server was started.
      */
    getTick: function() {
        return this._tickCount;
    },

    /**
      * {ObjectList} Returns the list of currently connected clients.
      */
    getClients: function() {
        return this._clients;
    },

    /**
      * {Float} Returns a synced* random number between `0` and `1`.
      *
      * Note: This is only synced if the game code actual is the same on both sides.
      *
      * E.g: Doing 2 calls to this method on the client, but doing 3 on the server
      * will have the potential to unsync the RNG until the next tick.
      */
    getRandom: function() {
        this._randomState = (1103515245 * (this._randomState + this._randomSeed) + 12345) % 0x100000000;
        return this._randomState / 0x100000000;
    },

    // Conversions ------------------------------------------------------------

    /**
      * {Integer} Converts the message @type {String|Integer} into a integer ID representing for network transmission.
      */
    messageTypeToId: function(type) {

        if (typeof type === 'string') {
            var id = this._messageTypes.indexOf(type);
            if (id === -1) {
                this.logError('Undefined type id for "' + type + '"');
            }

            return id;

        } else if (typeof type === 'number') {
            return type;

        } else {
            this.logError('Invalid type value: "' + type + '"');
        }

    },

    /**
      * {String} Converts the message @id {Integer} into the corresponding message type string.
      */
    messageTypeFromId: function(id) {

        if (id >= 0 && id < this._messageTypes.length) {
            return this._messageTypes[id];

        } else {
            this.logError('Undefined type string for "' + id + '"');
        }

    },

    // Helpers ----------------------------------------------------------------

    /**
      * {var[]} A wrapper for `console.log()` which also gives information about the current state of the server.
      */
    log: function() {
        console.log.apply(console, this._log(arguments));
    },

    /**
      * {var[]} A wrapper for `console.error()` which also gives information about the current state of the server.
      */
    logError: function() {
        console.error.apply(console, this._log(arguments));
    },

    /**
      * {var[]} A wrapper for `console.warn()` which also gives information about the current state of the server.
      */
    logWarning: function() {
        console.warn.apply(console, this._log(arguments));
    },

    _log: function(args) {
        var parts = Array.prototype.slice.call(args),
            info = '[Cc: ' + this.getClients().length
                    + '  T: ' + this.getTime()
                    + '  I: ' + this.getTick()
                    + '  R: ' + this.getRandom() + ']\n';

        parts.unshift(info);
        parts.push('\n');
        return parts;
    }

});

/**
  * {Maple.Server.Client} Simple client abstraction for the Maple Server.
  *
  * @conn {WebSocketConnection}
  */
Maple.ServerClient = Maple.Class(function(server, conn, isBinary) {

    this.id = conn.id;
    this.clientId = -1;
    this._conn = conn;
    this.server = server;
    this.isBinary = isBinary || false;
    this._messageArray = [0, 0];

}, {

    /**
      * Handler for messages received by the client which were not handled
      * by the server
      */
    message: function(type, tick, data) {

    },

    /**
      * Sends a @msg {Array} to the server, @type {Number}.
      */
    send: function(type, data) {

        // Add type and tick to the message
        this._messageArray.length = 2;
        this._messageArray[0] = this.server.messageTypeToId(type);
        this._messageArray[1] = this.server.getTick();

        if (data !== undefined) {
            this._messageArray.push.apply(this._messageArray, data);
        }

        // Make the message as small as possible and send it
        return this._conn.send(BISON.encode(this._messageArray), this.isBinary);

    },

    /**
      * {Integer} Sends down the raw @data {String} to the client
      */
    sendRaw: function(data) {
        return this._conn.send(data, this.isBinary);
    }

});

<commit_msg>Keep track of outgoing bytes per second<commit_after>/**
  * Copyright (c) 2012 Ivo Wetzel.
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  * copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
  *
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  * THE SOFTWARE.
  */

// Imports --------------------------------------------------------------------
var BISON = require('./lib/bison'),
    ObjectList = require('./lib/ObjectList'),
    WebSocketServer = require('./lib/WebSocket'),
    Maple = require('./Maple');

// Main Server ----------------------------------------------------------------
// ----------------------------------------------------------------------------
Maple.Server = Maple.Class(function(clientClass, messageTypes) {

    // Clients
    this._socket = null;
    this._clients = new ObjectList();
    this._clientClass = clientClass || Maple.ServerClient;
    this._messageTypes = messageTypes || {};

}, {

    $version: '0.2',

    /**
      * {Booelan} Starts the server in case it's not already running.
      *
      * Returns `true` on success.
      */
    start: function(options) {

        if (this.isRunning()) {
            return false;
        }

        // Traffic
        this._oldBytesSend = 0;
        this._bytesSend = 0;
        this._bytesTime = Date.now();
        this._bytesStats = [];
        this._bytesStatId = 0;
        this._bytesSendPerSecond = 0;

        // Ticking
        this._tickTime = 0;
        this._tickRate = Math.floor(1000 / (options.tickRate || 30));
        this._tickCount = 0;
        this._tickInterval = null;

        // Game Time
        this._startTime = -1;
        this._realTime = 0;
        this._frameTime = -1;

        // More logic stuff
        this._randomSeed = 500000 + Math.floor((Math.random() * 1000000));
        this._randomState = 0;
        this._logicRate = options.logicRate || 1; // Used for throttling logic ticks
        this._syncRate = options.syncRate || 30; // Send down tick count to clients every X updates

        this._isRunning = false;

        var that = this;
        this._tickInterval = setInterval(function() {
            that._update();

        }, this._tickRate);

        this._frameTime = Date.now();
        this._tickCount = 1;
        this._isRunning = true;

        if (options.socket !== false) {
            this._createSocket();
            this._socket.listen(options.port);
        }

        return true;

    },

    /**
      * {Boolean} Returns whether the server is running or not.
      */
    isRunning: function() {
        return this._isRunning;
    },

    /**
      * Broadcase a message.
      */
    broadcast: function(type, data, clients, excluded) {

        // Add type and tick to the message, unless we're just sending a
        // single Number for tick syncing
        if (type !== null) {

            var msg = [this.messageTypeToId(type), this.getTick()];
            if (data !== undefined) {
                msg.push.apply(msg, data);
            }

            data = msg;

        }

        data = BISON.encode(data);

        // Fall back to everyone!
        if (!clients) {
            clients = this._clients;
        }

        this._clients.each(function(client) {

            if (!excluded || excluded.indexOf(client) === -1) {
                this._bytesSend += client.sendRaw(data);
            }

        }, this);

    },

    stop: function() {

        if (!this.isRunning()) {
            return false;
        }

        this._isRunning = false;
        clearInterval(this._tickInterval);

        this.broadcast(Maple.Message.STOP);
        this._clients.each(function(client) {
            client.close();
        });

        if (this._socket) {
            this._socket.close();
        }

        return true;

    },

    _createSocket: function() {

        // Socket
        this._socket = new WebSocketServer();

        // Setup socket callbacks
        var that = this;
        this._socket.on('data', function(conn, raw, binary) {
            that._data(conn, raw);
        });

        this._socket.on('end', function(conn) {

            var client = that._clients.get(conn.clientId);
            if (client) {
                that._clients.remove(client);
                that.disconnected(client);
            }

        });

        this._socket.on('request', function(req, res) {
            that.requested(req, res);
        });

    },

    // Handling of incoming data and game logic -------------------------------
    _data: function(conn, raw) {

        // Do some basic filtering to prevent easy ways
        // of breaking the server
        var msg;
        try {
            msg = BISON.decode(raw);

        } catch(e) {
            this._error(conn, Maple.Error.INVALID_DATA);
            return;
        }

        if (msg.length < 2 || !(msg instanceof Array)) {
            this._error(conn, Maple.Error.MESSAGE_TOO_SHORT);
            return;
        }

        // Get message details
        var type = msg[0],
            tick = msg[1],
            data = msg.slice(2),
            client = this._clients.get(conn.clientId);

        // More checks for new connections
        if (type === Maple.Message.CONNECT) {

            if (!client) {

                // Throw up on unsupported version
                if (typeof data[0] !== 'string' || data[0] !== Maple.Server.$version) {
                    this._error(conn, Maple.Error.UNSUPPORTED_VERSION);

                // Add client to list and give the id to the connection
                } else {
                    client = new this._clientClass(this, conn, !!data[1]);
                    conn.clientId = this._clients.add(client);

                    this._bytesSend += client.send(Maple.Message.START, [
                        this._tickRate,
                        this._logicRate,
                        this._syncRate,
                        this._randomSeed,
                        this._messageTypes
                    ]);

                    this.connected(client);
                }

            } else {
                this._error(conn, Maple.Error.ALREADY_CONNECTED);
                this._clients.remove(client);
            }

        } else if (client) {

            // Handle time sync and ping detection
            // we simply echo back here
            if (type === Maple.Message.SYNC) {
                this._bytesSend += client.send(Maple.Message.SYNC, data);

            } else {

                type = this.messageTypeFromId(type);
                if (this.message(client, type, tick, data) !== true) {
                    client.message(type, tick, data);
                }

            }

        }

    },

    _error: function(conn, type) {
        this._bytesSend += conn.send(BISON.encode([Maple.Message.ERROR, 0, type]));
        conn.close();
    },

    _update: function() {

        var now = Date.now();

        this._realTime += (now - this._frameTime);
        while(this._tickTime < this._realTime && this.isRunning()) {

            if (this._startTime === -1) {
                this._startTime = Date.now();
                this.started();
            }

            // Sync clients tick count
            if (this._tickCount % this._syncRate === 0) {
                this.broadcast(null, this._tickCount % 250, this._players);
            }

            if (this._tickCount % this._logicRate === 0) {
                this.update(this._tickTime, this._tickCount);
            }

            this._tickCount++;
            this._randomState = this._tickCount;
            this._tickTime += this._tickRate;

        }

        if (now > this._bytesTime + 100) {

            this._bytesStats[this._bytesStatId] = this._bytesSend - this._oldBytesSend;
            this._oldBytesSend = this._bytesSend;
            this._bytesStatId++;
            this._bytesStatId = this._bytesStatId % 10;

            this._bytesSendPerSecond = this._bytesStats.reduce(function(p, c) {
                return p + c;
            }) / 10;

            this._bytesTime = now;

        }
        this._frameTime = now;

    },

    // Abstract methods -------------------------------------------------------

    /**
      * Callback for when the "game" is started.
      */
    started: function() {

    },

    /**
      * The game update callback.
      *
      * - @t {Integer} is the current game time.
      * - @tick {Integer} is the current tick count.
      */
    update: function(time, tick) {

    },

    /**
      * Callback for when the "game" is stopped.
      */
    stopped: function() {

    },

    /**
      * Callback for when a client connects to the server.
      *
      * @client {Server.Client}
      */
    connected: function(client) {

    },

    /**
      * Callback for when a client disconnects from the server.
      *
      * @client {Server.Client}
      */
    disconnected: function(client) {

    },

    /**
      * {Boolean} Callback for any messages received from a @client.
      *
      * - @client {Server.Client} The client
      * - @type {Integer} Message type
      * - @tick {Integer} Client side tick at which the message was send.
      * - @data {Array} Message data
      *
      * Return `true` to indicate that the message was handled and prevent it
      * from being forwarded to {Maple.Client#message}.
      */
    message: function(client, type, tick, data) {

    },

    /**
      * Handler for HTTP Request which reach the server on the port its bound to.
      *
      * You can use this like the vanilla HTTP interface from Node.
      */
    requested: function(req, res) {

    },

    // Getter -----------------------------------------------------------------

    /**
      * {Integer} Returns the "synced" game time in milliseconds.
      *
      * Please note that this is not 100% accurate and should only be used for
      * client side logic like drawing and effects.
      *
      * For synced actions between server and clinet always use the `tick` count.
      */
    getTime: function() {
        return this._tickTime;
    },

    /**
      * {Integer} Returns the number of synced game ticks that happend since the
      *           server was started.
      */
    getTick: function() {
        return this._tickCount;
    },

    /**
      * {ObjectList} Returns the list of currently connected clients.
      */
    getClients: function() {
        return this._clients;
    },

    /**
      * {Float} Returns a synced* random number between `0` and `1`.
      *
      * Note: This is only synced if the game code actual is the same on both sides.
      *
      * E.g: Doing 2 calls to this method on the client, but doing 3 on the server
      * will have the potential to unsync the RNG until the next tick.
      */
    getRandom: function() {
        this._randomState = (1103515245 * (this._randomState + this._randomSeed) + 12345) % 0x100000000;
        return this._randomState / 0x100000000;
    },

    // Conversions ------------------------------------------------------------

    /**
      * {Integer} Converts the message @type {String|Integer} into a integer ID representing for network transmission.
      */
    messageTypeToId: function(type) {

        if (typeof type === 'string') {
            var id = this._messageTypes.indexOf(type);
            if (id === -1) {
                this.logError('Undefined type id for "' + type + '"');
            }

            return id;

        } else if (typeof type === 'number') {
            return type;

        } else {
            this.logError('Invalid type value: "' + type + '"');
        }

    },

    /**
      * {String} Converts the message @id {Integer} into the corresponding message type string.
      */
    messageTypeFromId: function(id) {

        if (id >= 0 && id < this._messageTypes.length) {
            return this._messageTypes[id];

        } else {
            this.logError('Undefined type string for "' + id + '"');
        }

    },

    // Helpers ----------------------------------------------------------------

    /**
      * {var[]} A wrapper for `console.log()` which also gives information about the current state of the server.
      */
    log: function() {
        console.log.apply(console, this._log(arguments));
    },

    /**
      * {var[]} A wrapper for `console.error()` which also gives information about the current state of the server.
      */
    logError: function() {
        console.error.apply(console, this._log(arguments));
    },

    /**
      * {var[]} A wrapper for `console.warn()` which also gives information about the current state of the server.
      */
    logWarning: function() {
        console.warn.apply(console, this._log(arguments));
    },

    _log: function(args) {
        var parts = Array.prototype.slice.call(args),
            info = '[Cc: ' + this.getClients().length
                    + '  T: ' + this.getTime()
                    + '  I: ' + this.getTick()
                    + '  R: ' + this.getRandom() + ']\n';

        parts.unshift(info);
        parts.push('\n');
        return parts;
    }

});

/**
  * {Maple.Server.Client} Simple client abstraction for the Maple Server.
  *
  * @conn {WebSocketConnection}
  */
Maple.ServerClient = Maple.Class(function(server, conn, isBinary) {

    this.id = conn.id;
    this.clientId = -1;
    this._conn = conn;
    this.server = server;
    this.isBinary = isBinary || false;
    this._messageArray = [0, 0];

}, {

    /**
      * Handler for messages received by the client which were not handled
      * by the server
      */
    message: function(type, tick, data) {

    },

    /**
      * Sends a @msg {Array} to the server, @type {Number}.
      */
    send: function(type, data) {

        // Add type and tick to the message
        this._messageArray.length = 2;
        this._messageArray[0] = this.server.messageTypeToId(type);
        this._messageArray[1] = this.server.getTick();

        if (data !== undefined) {
            this._messageArray.push.apply(this._messageArray, data);
        }

        // Make the message as small as possible and send it
        return this._conn.send(BISON.encode(this._messageArray), this.isBinary);

    },

    /**
      * {Integer} Sends down the raw @data {String} to the client
      */
    sendRaw: function(data) {
        return this._conn.send(data, this.isBinary);
    }

});

<|endoftext|>


<commit_before>import fs from 'fs';
import _ from 'lodash';
import initalSettings from './_initialSettings';
import createJSON from './_jsonCreator';

class Settings {
  constructor(jsonPrefix, wipeOldData) {
    this.PATH = createJSON(`${jsonPrefix || ''}.settings`);
    this.data = Object.assign({}, initalSettings);
    this.lastSync = 0;

    if (fs.existsSync(this.PATH) && !wipeOldData) {
      this._load();
    } else {
      this._save(true);
      // DEV: Handle windows users running as admin...
      fs.chmodSync(this.PATH, '777');
    }
    this.coupled = true;

    this._hooks = {};
  }

  uncouple() {
    this.coupled = false;
  }

  get(key, defaultValue = null) {
    if (!this.coupled) {
      this._load();
    }
    return typeof this.data[key] === 'undefined' ? defaultValue : this.data[key];
  }

  onChange(key, fn) {
    this._hooks[key] = this._hooks[key] || [];
    this._hooks[key].push(fn);
  }

  set(key, value) {
    if (this.coupled) {
      if (this._hooks[key] && this.data[key] !== value) {
        this._hooks[key].forEach((hookFn) => hookFn(value));
      }
      this.data[key] = value;
      this._save();
    }
  }

  _load(retryCount = 5) {
    let userSettings;
    try {
      userSettings = JSON.parse(fs.readFileSync(this.PATH, 'utf8'));
    } catch (e) {
      if (retryCount > 0) {
        setTimeout(this._load.bind(this, retryCount - 1), 10);
        if (global.Logger) Logger.error('Failed to load settings JSON file, retyring in 10 milliseconds');
        return;
      }
      userSettings = {};
      if (global.Logger) Logger.error('Failed to load settings JSON file, giving up and resetting');
    }
    this.data = _.extend({}, initalSettings, userSettings);
  }

  _save(force) {
    const now = (new Date()).getTime();
    // During some save events (like resize) we need to queue the disk writes
    // so that we don't blast the disk every millisecond
    if ((now - this.lastSync > 250 || force)) {
      if (this.data) {
        try {
          fs.writeFileSync(this.PATH, JSON.stringify(this.data, null, 4));
        } catch (e) {
          if (this.saving) clearTimeout(this.saving);
          this.saving = setTimeout(this._save.bind(this), 275);
        }
      }
      if (this.saving) clearTimeout(this.saving);
    } else {
      if (this.saving) clearTimeout(this.saving);
      this.saving = setTimeout(this._save.bind(this), 275);
    }
    this.lastSync = now;
  }

  destroy() {
    this.data = null;
    fs.unlinkSync(this.PATH);
  }
}

export default Settings;
<commit_msg>Only fire onChange event once we update the value<commit_after>import fs from 'fs';
import _ from 'lodash';
import initalSettings from './_initialSettings';
import createJSON from './_jsonCreator';

class Settings {
  constructor(jsonPrefix, wipeOldData) {
    this.PATH = createJSON(`${jsonPrefix || ''}.settings`);
    this.data = Object.assign({}, initalSettings);
    this.lastSync = 0;

    if (fs.existsSync(this.PATH) && !wipeOldData) {
      this._load();
    } else {
      this._save(true);
      // DEV: Handle windows users running as admin...
      fs.chmodSync(this.PATH, '777');
    }
    this.coupled = true;

    this._hooks = {};
  }

  uncouple() {
    this.coupled = false;
  }

  get(key, defaultValue = null) {
    if (!this.coupled) {
      this._load();
    }
    return typeof this.data[key] === 'undefined' ? defaultValue : this.data[key];
  }

  onChange(key, fn) {
    this._hooks[key] = this._hooks[key] || [];
    this._hooks[key].push(fn);
  }

  set(key, value) {
    if (this.coupled) {
      const valChanged = this.data[key] !== value;
      this.data[key] = value;
      if (this._hooks[key] && valChanged) {
        this._hooks[key].forEach((hookFn) => hookFn(value));
      }
      this._save();
    }
  }

  _load(retryCount = 5) {
    let userSettings;
    try {
      userSettings = JSON.parse(fs.readFileSync(this.PATH, 'utf8'));
    } catch (e) {
      if (retryCount > 0) {
        setTimeout(this._load.bind(this, retryCount - 1), 10);
        if (global.Logger) Logger.error('Failed to load settings JSON file, retyring in 10 milliseconds');
        return;
      }
      userSettings = {};
      if (global.Logger) Logger.error('Failed to load settings JSON file, giving up and resetting');
    }
    this.data = _.extend({}, initalSettings, userSettings);
  }

  _save(force) {
    const now = (new Date()).getTime();
    // During some save events (like resize) we need to queue the disk writes
    // so that we don't blast the disk every millisecond
    if ((now - this.lastSync > 250 || force)) {
      if (this.data) {
        try {
          fs.writeFileSync(this.PATH, JSON.stringify(this.data, null, 4));
        } catch (e) {
          if (this.saving) clearTimeout(this.saving);
          this.saving = setTimeout(this._save.bind(this), 275);
        }
      }
      if (this.saving) clearTimeout(this.saving);
    } else {
      if (this.saving) clearTimeout(this.saving);
      this.saving = setTimeout(this._save.bind(this), 275);
    }
    this.lastSync = now;
  }

  destroy() {
    this.data = null;
    fs.unlinkSync(this.PATH);
  }
}

export default Settings;
<|endoftext|>


<commit_before>'use strict';

// RegExps for URLs to be captured
const RE_COURSES  = /\/api\/v2\/goals\/\d+\?/;
const RE_QUIZZES  = /\/api\/v2\/.*?\/study\?/;
const RE_SETTINGS = /\/api\/v2\/settings\?/;

let xhrResponses = {};

// Initialization
{
	// Custom data attributes to communicate with the embedded script.
	const iframeParent = 'body';
	const iframeId     = `__better_iknow_messaging_${Date.now()}__`;
	const matchUrls    = JSON.stringify([RE_COURSES, RE_QUIZZES, RE_SETTINGS].map((re) => re.source));

	// Waits for iframe injection by the embedded script.
	const iframeObserver = new MutationObserver((records) => {
		records.filter((record) => (record.type === 'childList')).forEach((record) => {
			Array.prototype.slice.call(record.addedNodes).filter((addedNode) => (addedNode.id === iframeId)).forEach((addedNode) => {
				// Stops the observation.
				iframeObserver.disconnect();

				// Sets a message event handler.
				addedNode.contentWindow.addEventListener('message', (event) => {
					if (event.origin !== 'http://iknow.jp') {
						return;
					}

					// Stores the XHR response data.
					const data = JSON.parse(event.data);
					const key = data.url.replace(/\?.*$/, '?');
					xhrResponses[key] = data.text;
				});
			});
		});
	});
	iframeObserver.observe(document.querySelector(iframeParent), {childList: true});

	// Appends the script.
	const script = document.createElement('script');
	script.src = chrome.extension.getURL('/xhr_captor.js');
	script.dataset.iframeParent = iframeParent;
	script.dataset.iframeId     = iframeId;
	script.dataset.matchUrls    = matchUrls;
	document.getElementsByTagName('head')[0].appendChild(script);
}

// Dictation
{
	let contents = null;
	let settings = {
		apps: {
			content_volume: 1.0,
			effect_volume:  1.0,
		},
	};

	// Waits for displaying the dictation quiz screen.
	// TODO: Optimize the timing of the prepation for dictation.
	const divObserver = new MutationObserver((records) => {
		records.filter((record) => (record.type === 'attributes' && record.attributeName === 'class')).forEach((record) => {
			if (record.target.classList.contains('current_screen')) {
				SoundPlayer.mute();
				prepareForDictation();
			} else {
				SoundPlayer.unmute();
			}
		});
	});
	divObserver.observe(document.getElementById('dictation_quiz_screen'), {attributes: true});

	const SoundPlayer = (() => {
		const player = document.getElementById('sound_player');

		return {
			mute: () => {
				// setVolume() in sound_player.swf ignores falsy value. (it doesn't accept 0)
				// And the actual volume (v: [0.0, 1.0]) is caliculated as below:
				//   v = Math.round(volume) / 10;
				// Therefore, to mute it, needs to set volume in the interval (0, 0.5).
				player.setVolume(0.4);
			},
			unmute: () => {
				const contentVolume = (settings.apps) ? (settings.apps.content_volume || 0.0) : 1.0;
				player.setVolume(10 * contentVolume);
			},
		};
	})();

	const VoicePlayer = (() => {
		const id = `__better_iknow_audio_${Date.now()}__`;
		document.querySelector('#dictation_quiz_screen').insertAdjacentHTML('beforeend', `
<audio controls id="${id}" style="width: 100%;"></audio>
`);
		const audio = document.getElementById(id);

		return {
			setSource: (url) => {
				audio.src = url;
				audio.load();
			},
			serVolume: (volume) => {
				audio.volume = volume;
			},
			play: () => {
				audio.currentTime = 0.0;
				audio.play();
			},
			rewind: (sec) => {
				audio.currentTime -= sec;
				audio.play();
			},
			forward: (sec) => {
				audio.currentTime += sec;
			},
		};
	})();

	/**
	 *	Prepares the internal states for dictation.
	 *	Makes the contents data from XHR data for key input check, gets the settings data for effect volume control.
	 */
	function prepareForDictation() {
		// Clears the contents data.
		contents = null;

		// Gets quizzes and courses data from the stored XHR data.
		const urls = Object.keys(xhrResponses);
		const quizIndex = urls.findIndex((url) => RE_QUIZZES.test(url));
		const quizzes = (quizIndex !== -1) ? JSON.parse(xhrResponses[urls[quizIndex]]) : null;
		const courses = urls.filter((url) => RE_COURSES.test(url)).map((url) => JSON.parse(xhrResponses[url]));

		// Makes contents data from quizzes and courses data.
		if (quizzes && courses) {
			contents = makeContents(quizzes, courses);
		}
		if (!contents) {
			console.log('ERROR: Cannot make contents data')
		}

		//
		const index = getCurrentSetenceIndex();
		if (index >= 0) {
			VoicePlayer.setSource(contents[index].soundUrl);
		}

		// Gets settings data from the stored XHR data.
		const settingsIndex = urls.findIndex((url) => RE_SETTINGS.test(url));
		if (settingsIndex !== -1) {
			settings = JSON.parse(xhrResponses[urls[settingsIndex]]);
		}
	}

	/**
	 *	Makes contents from XHR data.
	 */
	function makeContents(quizzes, courses) {
		if (!quizzes || !courses) {
			return null;
		}

		return quizzes.map((content) => {
			const course = courses.find((course) => (content.goal_id === course.id));
			if (!course || !course.goal_items) {
				console.log(`ERROR: goal_id (${content.goal_id}) is not found in courses (%o)`, courses);
				return null;
			}

			const item = course.goal_items.find((goalItem) => (content.item_id === goalItem.item.id));
			if (!item || !item.sentences || item.sentences.length < 1) {
				console.log(`ERROR: item_id (${content.item_id}) is not found in goal_items (%o)`, course.goal_items);
				return null;
			}

			const sentence = item.sentences.find((sentence) => (sentence.cue && (content.content_id === sentence.cue.id)));
			if (!sentence) {
				console.log(`ERROR: content_id (${content.content_id}) is not found in sentences (%o)`, item.sentences);
				return null;
			}

			return {
				sentence: sentence.cue.text.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, ''),
				soundUrl: sentence.sound,
			};
		});
	}

	/**
	 *	Plays the sound effect.
	 */
	const playIncorrect = (() => {
		const audio = new Audio('//iknow.jp/_assets/apps/common/spell_incorrect.mp3');
		audio.load();

		return () => {
			audio.pause();
			if (settings.apps) {
				audio.volume = settings.apps.effect_volume || 0.0;
			}
			audio.play();
		};
	})();

	/**
	 *	Displays an incorrect input letter.
	 */
	const displayIncorrect = (() => {
		// Makes a span element to display an incorrect input letter.
		const span = document.createElement('span');
		document.body.appendChild(span);

		return (letter) => {
			// Gets the element of the cursor.
			const cursor = document.querySelector('#dictation_quiz_screen .letter.cursor');
			if (!cursor) {
				return;
			}

			// Stops current trasition effect.
			span.style.transition = 'none';

			// Hides the incorrect input letter.
			if (!letter) {
				span.style.display = 'hidden';
				return;
			}

			// Copies all CSS properties as text from the cursor.
			span.style = getComputedStyle(cursor).cssText;

			// Sets the CSS properties about color.
			['color', 'textFillColor', 'webkitTextFillColor'].forEach((prop) => {span.style[prop] = '#f33'});

			// Sets the position of the incorrect letter same to the cursor.
			const rect = cursor.getBoundingClientRect();
			span.style.position = 'fixed';
			span.style.left = `${rect.left}px`;
			span.style.top  = `${rect.top}px`;

			// Sets the text.
			span.textContent = letter.charAt(0);

			// Begins fade out from the next frame.
			setTimeout(() => {
				span.style.transition = 'opacity 1s ease-out';
				span.style.opacity = 0;
			}, 17);	// For some reason, requestAnimationFrame only works after clicking the client area of browser.
		};
	})();

	/**
	 *	Handles key events and stops its propagation to the Dictation app if necessary.
	 */
	window.addEventListener('keydown', (() => {
		const keyHandlers = {
			' ': () => {
				VoicePlayer.play();
			},
			'ArrowLeft': () => {
				VoicePlayer.rewind(1.0);
			},
			'ArrowRight': () => {
				VoicePlayer.forward(1.0);
			},
			'Backspace': () => {
				// TODO: implement
			},
			'Enter': () => {},
		};

		function handleSpecialKeys(event) {
			if (keyHandlers[event.key]) {
				keyHandlers[event.key]();
				return true;
			} else {
				return false;
			}
		}

		return (event) => {
			if (!isTypingMode()) {
				return;
			}

			if (handleSpecialKeys(event)) {
				event.preventDefault();
				event.stopPropagation();
				return;
			}

			if (!/^[a-zA-Z]$/.test(event.key)) {
				return;
			}

			if (!contents || contents.length === 0) {
				console.log('ERROR: Cannot get contents');
				return;
			}

			const index = getCurrentSetenceIndex();
			if (index < 0) {
				console.log('ERROR: Cannot get index');
				return;
			}

			const cursorPos = getCurrentCursorPos();
			if (cursorPos < 0) {
				console.log('ERROR: Cannot get cursorPos');
				return;
			}

			const sentence = contents[index].sentence;
			if (event.key.toLowerCase() !== sentence.charAt(cursorPos).toLowerCase()) {
				playIncorrect();
				displayIncorrect(event.key);

				event.preventDefault();
				event.stopPropagation();
				return;

			} else {
				displayIncorrect('');
			}

			setTimeout(() => {
				if (isSentenceCompleted()) {
					clickEnter();
				}
			}, 100);
		};
	})(), true);

	/**
	 *	Clicks the "Enter" button in the Dictation app.
	 */
	function clickEnter() {
		document.getElementById('nav_enter').click();
	}

	/**
	 *	Returns whether the Dictation app is in typing mode or not.
	 */
	function isTypingMode() {
		return (document.getElementById('dictation_quiz_screen').offsetHeight > 0 && !document.querySelector('.paused'));
	}

	/**
	 *	Gets the position of the cursor in the Dictation app.
	 */
	function getCurrentCursorPos() {
		const spans = document.querySelectorAll('#dictation_quiz_screen .word, #dictation_quiz_screen .letter, #dictation_quiz_screen .space, #dictation_quiz_screen .excluded, #dictation_quiz_screen .punctuation');
		let pos = 0;
		for (let i = 0; i < spans.length; i++) {
			if (spans[i].classList.contains('cursor')) {
				return pos;
			}
			pos += spans[i].textContent.length;
		}
		return -1;
	}

	/**
	 *	Gets the current index of the sentences in the Dictation app.
	 */
	function getCurrentSetenceIndex() {
		return document.querySelectorAll('#top-panel ul.steps li.step-filled').length - 1;
	}

	/**
	 *	Gets the current sentence in the Dictation app.
	 */
	function getCurrentSentence() {
		return Array.prototype.slice.call(document.querySelectorAll('#dictation_quiz_screen .letter, #dictation_quiz_screen .space')).map(function(e) {return (e.textContent) ? e.textContent : ' ';}).join('');
	}

	/**
	 *	Returns whether the sentence in the Dictation app has been input or not.
	 */
	function isSentenceCompleted() {
		return !Array.prototype.slice.call(document.querySelectorAll('#dictation_quiz_screen .typeable')).some((span) => !span.textContent);
	}
}
<commit_msg>Factor out XHR handler and set audio volume timely<commit_after>'use strict';

// RegExps for URLs to be captured
const RE_COURSES  = /\/api\/v2\/goals\/\d+\?/;
const RE_QUIZZES  = /\/api\/v2\/.*?\/study\?/;
const RE_SETTINGS = /\/api\/v2\/settings\?/;

// Initialization
((callback, param) => {
	if (!param || !param.iframeParent || !param.iframeId || !param.origin || !callback) {
		throw new Error('Invalid argument');
	}

	// Waits for iframe injection by the embedded script.
	let iframeObserver = new MutationObserver((records) => {
		records.filter((record) => (record.type === 'childList')).forEach((record) => {
			Array.prototype.slice.call(record.addedNodes).filter((addedNode) => (addedNode.id === param.iframeId)).forEach((addedNode) => {
				// Stops the observation.
				iframeObserver.disconnect();
				iframeObserver = null;

				// Sets a message event handler.
				addedNode.contentWindow.addEventListener('message', (event) => {
					if (event.origin !== param.origin) {
						return;
					}

					const data = JSON.parse(event.data);
					callback(data.url, data.text)
				});
			});
		});
	});
	iframeObserver.observe(document.querySelector(param.iframeParent), {childList: true});

	// Appends the script.
	const script = document.createElement('script');
	script.src = chrome.extension.getURL('/xhr_captor.js');
	script.dataset.iframeParent = param.iframeParent;
	script.dataset.iframeId     = param.iframeId;
	script.dataset.matchUrls    = param.matchUrls;
	document.getElementsByTagName('head')[0].appendChild(script);

})(XhrHandler, {
	iframeParent: 'body',
	iframeId:     `__better_iknow_messaging_${Date.now()}__`,
	matchUrls:    JSON.stringify([RE_COURSES, RE_QUIZZES, RE_SETTINGS].map((re) => re.source)),
	origin:       'http://iknow.jp',
});

// Waits for displaying the dictation quiz screen.
// TODO: Optimize the timing of the prepation for dictation.
const divObserver = new MutationObserver((records) => {
	records.filter((record) => (record.type === 'attributes' && record.attributeName === 'class')).forEach((record) => {
		if (record.target.classList.contains('current_screen')) {
			SoundPlayer.mute();
			prepareForDictation();
		} else {
			SoundPlayer.unmute();
		}
	});
});
divObserver.observe(document.getElementById('dictation_quiz_screen'), {attributes: true});

let xhrResponses = {};
let contents = null;
let settings = {
	apps: {
		content_volume: 1.0,
		effect_volume:  1.0,
	},
};

/**
 *	Handles XHR and stores them.
 */
function XhrHandler(url, text) {
	if (RE_SETTINGS.test(url)) {
		const newSettings = JSON.parse(text);
		if (newSettings.apps) {
			settings.apps = Object.assign(settings.apps || {}, newSettings.apps);
		}

		VoicePlayer.setVolume((settings.apps) ? (settings.apps.content_volume || 0.0) : 1.0);
		if (isTypingMode()) {
			SoundPlayer.mute();
		}

		return;
	}

	// Stores the XHR response data.
	const key = url.replace(/\?.*$/, '?');
	xhrResponses[key] = text;
}

// Controls the original Flash sound player.
const SoundPlayer = (() => {
	const player = document.getElementById('sound_player');

	return {
		mute: () => {
			// setVolume() in sound_player.swf ignores falsy value. (it doesn't accept 0)
			// And the actual volume (v: [0.0, 1.0]) is caliculated as below:
			//   v = Math.round(volume) / 10;
			// Therefore, to mute it, needs to set volume in the interval (0, 0.5).
			player.stopSounds();
			player.setVolume(0.4);
		},
		unmute: () => {
			const contentVolume = (settings.apps) ? (settings.apps.content_volume || 0.0) : 1.0;
			player.setVolume(10 * contentVolume);
		},
	};
})();

// Controls the own voice player.
const VoicePlayer = (() => {
	const id = `__better_iknow_audio_${Date.now()}__`;
	document.querySelector('#dictation_quiz_screen').insertAdjacentHTML('beforeend', `
<audio controls id="${id}" style="width: 100%;"></audio>
`);
	const audio = document.getElementById(id);

	return {
		setSource: (url) => {
			audio.src = url;
			audio.load();
		},
		setVolume: (volume) => {
			audio.volume = volume;
		},
		play: () => {
			audio.currentTime = 0.0;
			audio.play();
		},
		rewind: (sec) => {
			audio.currentTime -= sec;
			audio.play();
		},
		forward: (sec) => {
			audio.currentTime += sec;
			audio.play();
		},
	};
})();

/**
 *	Prepares the internal states for dictation.
 *	Makes the contents data from XHR data for key input check, gets the settings data for effect volume control.
 */
function prepareForDictation() {
	// Clears the contents data.
	contents = null;

	// Gets quizzes and courses data from the stored XHR data.
	const urls = Object.keys(xhrResponses);
	const quizIndex = urls.findIndex((url) => RE_QUIZZES.test(url));
	const quizzes = (quizIndex !== -1) ? JSON.parse(xhrResponses[urls[quizIndex]]) : null;
	const courses = urls.filter((url) => RE_COURSES.test(url)).map((url) => JSON.parse(xhrResponses[url]));

	// Makes contents data from quizzes and courses data.
	if (quizzes && courses) {
		contents = makeContents(quizzes, courses);
	}
	if (!contents) {
		console.log('ERROR: Cannot make contents data')
	}

	// Sets voice properties.
	const index = getCurrentSetenceIndex();
	if (index >= 0) {
		VoicePlayer.setSource(contents[index].soundUrl);
	}
}

/**
 *	Makes contents from XHR data.
 */
function makeContents(quizzes, courses) {
	if (!quizzes || !courses) {
		return null;
	}

	return quizzes.map((content) => {
		const course = courses.find((course) => (content.goal_id === course.id));
		if (!course || !course.goal_items) {
			console.log(`ERROR: goal_id (${content.goal_id}) is not found in courses (%o)`, courses);
			return null;
		}

		const item = course.goal_items.find((goalItem) => (content.item_id === goalItem.item.id));
		if (!item || !item.sentences || item.sentences.length < 1) {
			console.log(`ERROR: item_id (${content.item_id}) is not found in goal_items (%o)`, course.goal_items);
			return null;
		}

		const sentence = item.sentences.find((sentence) => (sentence.cue && (content.content_id === sentence.cue.id)));
		if (!sentence) {
			console.log(`ERROR: content_id (${content.content_id}) is not found in sentences (%o)`, item.sentences);
			return null;
		}

		return {
			sentence: sentence.cue.text.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, ''),
			soundUrl: sentence.sound,
		};
	});
}

/**
 *	Plays the sound effect.
 */
const playIncorrect = (() => {
	const audio = new Audio('//iknow.jp/_assets/apps/common/spell_incorrect.mp3');
	audio.load();

	return () => {
		audio.pause();
		if (settings.apps) {
			audio.volume = settings.apps.effect_volume || 0.0;
		}
		audio.play();
	};
})();

/**
 *	Displays an incorrect input letter.
 */
const displayIncorrect = (() => {
	// Makes a span element to display an incorrect input letter.
	const span = document.createElement('span');
	document.body.appendChild(span);

	return (letter) => {
		// Gets the element of the cursor.
		const cursor = document.querySelector('#dictation_quiz_screen .letter.cursor');
		if (!cursor) {
			return;
		}

		// Stops current trasition effect.
		span.style.transition = 'none';

		// Hides the incorrect input letter.
		if (!letter) {
			span.style.display = 'hidden';
			return;
		}

		// Copies all CSS properties as text from the cursor.
		span.style = getComputedStyle(cursor).cssText;

		// Sets the CSS properties about color.
		['color', 'textFillColor', 'webkitTextFillColor'].forEach((prop) => {span.style[prop] = '#f33'});

		// Sets the position of the incorrect letter same to the cursor.
		const rect = cursor.getBoundingClientRect();
		span.style.position = 'fixed';
		span.style.left = `${rect.left}px`;
		span.style.top  = `${rect.top}px`;

		// Sets the text.
		span.textContent = letter.charAt(0);

		// Begins fade out from the next frame.
		setTimeout(() => {
			span.style.transition = 'opacity 1s ease-out';
			span.style.opacity = 0;
		}, 17);	// For some reason, requestAnimationFrame only works after clicking the client area of browser.
	};
})();

/**
 *	Handles key events and stops its propagation to the Dictation app if necessary.
 */
window.addEventListener('keydown', (() => {
	const keyHandlers = {
		' ': () => {
			VoicePlayer.play();
		},
		'ArrowLeft': () => {
			VoicePlayer.rewind(1.0);
		},
		'ArrowRight': () => {
			VoicePlayer.forward(1.0);
		},
		'Backspace': () => {
			// TODO: implement
		},
		'Enter': () => {},
	};

	function handleSpecialKeys(event) {
		if (keyHandlers[event.key]) {
			keyHandlers[event.key]();
			return true;
		} else {
			return false;
		}
	}

	return (event) => {
		if (!isTypingMode()) {
			return;
		}

		if (handleSpecialKeys(event)) {
			event.preventDefault();
			event.stopPropagation();
			return;
		}

		if (!/^[a-zA-Z]$/.test(event.key)) {
			return;
		}

		if (!contents || contents.length === 0) {
			console.log('ERROR: Cannot get contents');
			return;
		}

		const index = getCurrentSetenceIndex();
		if (index < 0) {
			console.log('ERROR: Cannot get index');
			return;
		}

		const cursorPos = getCurrentCursorPos();
		if (cursorPos < 0) {
			console.log('ERROR: Cannot get cursorPos');
			return;
		}

		const sentence = contents[index].sentence;
		if (event.key.toLowerCase() !== sentence.charAt(cursorPos).toLowerCase()) {
			playIncorrect();
			displayIncorrect(event.key);

			event.preventDefault();
			event.stopPropagation();
			return;

		} else {
			displayIncorrect('');
		}

		setTimeout(() => {
			if (isSentenceCompleted()) {
				clickEnter();
			}
		}, 100);
	};
})(), true);

/**
 *	Clicks the "Enter" button in the Dictation app.
 */
function clickEnter() {
	document.getElementById('nav_enter').click();
}

/**
 *	Returns whether the Dictation app is in typing mode or not.
 */
function isTypingMode() {
	return (document.getElementById('dictation_quiz_screen').offsetHeight > 0 && !document.querySelector('.paused'));
}

/**
 *	Gets the position of the cursor in the Dictation app.
 */
function getCurrentCursorPos() {
	const spans = document.querySelectorAll('#dictation_quiz_screen .word, #dictation_quiz_screen .letter, #dictation_quiz_screen .space, #dictation_quiz_screen .excluded, #dictation_quiz_screen .punctuation');
	let pos = 0;
	for (let i = 0; i < spans.length; i++) {
		if (spans[i].classList.contains('cursor')) {
			return pos;
		}
		pos += spans[i].textContent.length;
	}
	return -1;
}

/**
 *	Gets the current index of the sentences in the Dictation app.
 */
function getCurrentSetenceIndex() {
	return document.querySelectorAll('#top-panel ul.steps li.step-filled').length - 1;
}

/**
 *	Gets the current sentence in the Dictation app.
 */
function getCurrentSentence() {
	return Array.prototype.slice.call(document.querySelectorAll('#dictation_quiz_screen .letter, #dictation_quiz_screen .space')).map(function(e) {return (e.textContent) ? e.textContent : ' ';}).join('');
}

/**
 *	Returns whether the sentence in the Dictation app has been input or not.
 */
function isSentenceCompleted() {
	return !Array.prototype.slice.call(document.querySelectorAll('#dictation_quiz_screen .typeable')).some((span) => !span.textContent);
}
<|endoftext|>