static/js/helpers.js
function(lineNum){
        var Changeset = require('ep_etherpad-lite/static/js/Changeset');
        var ChangesetUtils = require('ep_etherpad-lite/static/js/ChangesetUtils');
        var _ = require('ep_etherpad-lite/static/js/underscore');

        var aline = Cristo.rep.alines[lineNum];
        if(aline){
            var entry = Cristo.rep.lines.atIndex(lineNum);
            var text = entry.text;
            var opIter = Changeset.opIterator(aline);
            var ci = -1;
            var bci = -1;
            var attributes = [];
            while (opIter.hasNext()) {
                var op = opIter.next();
                bci = ci + 1;
                ci += op.chars;
                Changeset.eachAttribNumber(op.attribs, function (n) {
                    if(bci > text.length){ return; }
                    attributes.push({
                        name: Cristo.rep.apool.getAttribKey(n),
                        value: Cristo.rep.apool.getAttribValue(n),
                        offset: bci,
                        limit: ci - bci + 1,
                        charStart: text[bci],
                        charEnd: text[Math.min(ci, text.length - 1)],
                        remove: function(withChar, limit){
                            limit = Math.min(this.offset + ( limit>0? limit:this.limit ), entry.width);
                            var builder = Changeset.builder(Cristo.rep.lines.totalWidth());
                            if(withChar){
                                ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                                ChangesetUtils.buildRemoveRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit]);
                            } else{
                                var attribs = _replaceAttrib(this);
                                //console.log('rm', attribs, this.name, this.value, 'ch: '+this.char, 'l', lineNum, this.offset, limit);
                                ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                                ChangesetUtils.buildKeepRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit], attribs, Cristo.rep.apool);
                            }
                            return Cristo.documentAttributeManager.applyChangeset(builder);
                        },
                        changeValue:function(newValue, limit){
                            limit = Math.min(this.offset + ( limit>0? limit:this.limit ), entry.width);
                            var builder = Changeset.builder(Cristo.rep.lines.totalWidth());
                            var attribs = _replaceAttrib(this, newValue);
                            ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                            ChangesetUtils.buildKeepRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit], attribs, Cristo.rep.apool);
                            return Cristo.documentAttributeManager.applyChangeset(builder);
                        }
                    });
                });
            }

            var _replaceAttrib = function(cont, value){
                value = value || '';
                return _.chain(attributes).map(function(a){
                    if(a.offset === cont.offset && a.char === cont.char){
                        if(a.name === cont.name && a.value === cont.value){
                            return [a.name, value];
                        }
                    }
                }).filter(function(v){return v;}).value();
            };

            return attributes;
        }
        return [];
    }
function(lineNum){
        var Changeset = require('ep_etherpad-lite/static/js/Changeset');
        var ChangesetUtils = require('ep_etherpad-lite/static/js/ChangesetUtils');
        var _ = require('ep_etherpad-lite/static/js/underscore');

        var aline = Cristo.rep.alines[lineNum];
        if(aline){
            var entry = Cristo.rep.lines.atIndex(lineNum);
            var text = entry.text;
            var opIter = Changeset.opIterator(aline);
            var ci = -1;
            var bci = -1;
            var attributes = [];
            while (opIter.hasNext()) {
                var op = opIter.next();
                bci = ci + 1;
                ci += op.chars;
                Changeset.eachAttribNumber(op.attribs, function (n) {
                    if(bci > text.length){ return; }
                    attributes.push({
                        name: Cristo.rep.apool.getAttribKey(n),
                        value: Cristo.rep.apool.getAttribValue(n),
                        offset: bci,
                        limit: ci - bci + 1,
                        charStart: text[bci],
                        charEnd: text[Math.min(ci, text.length - 1)],
                        remove: function(withChar, limit){
                            limit = Math.min(this.offset + ( limit>0? limit:this.limit ), entry.width);
                            var builder = Changeset.builder(Cristo.rep.lines.totalWidth());
                            if(withChar){
                                ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                                ChangesetUtils.buildRemoveRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit]);
                            } else{
                                var attribs = _replaceAttrib(this);
                                //console.log('rm', attribs, this.name, this.value, 'ch: '+this.char, 'l', lineNum, this.offset, limit);
                                ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                                ChangesetUtils.buildKeepRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit], attribs, Cristo.rep.apool);
                            }
                            return Cristo.documentAttributeManager.applyChangeset(builder);
                        },
                        changeValue:function(newValue, limit){
                            limit = Math.min(this.offset + ( limit>0? limit:this.limit ), entry.width);
                            var builder = Changeset.builder(Cristo.rep.lines.totalWidth());
                            var attribs = _replaceAttrib(this, newValue);
                            ChangesetUtils.buildKeepToStartOfRange(Cristo.rep, builder, [lineNum, this.offset]);
                            ChangesetUtils.buildKeepRange(Cristo.rep, builder, [lineNum, this.offset], [lineNum, limit], attribs, Cristo.rep.apool);
                            return Cristo.documentAttributeManager.applyChangeset(builder);
                        }
                    });
                });
            }

            var _replaceAttrib = function(cont, value){
                value = value || '';
                return _.chain(attributes).map(function(a){
                    if(a.offset === cont.offset && a.char === cont.char){
                        if(a.name === cont.name && a.value === cont.value){
                            return [a.name, value];
                        }
                    }
                }).filter(function(v){return v;}).value();
            };

            return attributes;
        }
        return [];
    }
Gets all attributes on line. Any attribute is nice described, and provides functions such remove, or changeValue.
attribute.remove(withChar, limit) - remove attribute to the limit, if withChar == true then characters will be removed too.
attribute.changeValue(newValue, limit) - replace current value on new to the limit
- default limit is a count of following chars, which have the same attribute.
TIP: It should be called from stack callWithAce (inside)
@example: You can use Cristo.callWithAce(function(ace){Cristo.getAllAttributesOnLine()},'myCall')
@param lineNum {Number}
@returns {Array} of objects like this one {name, value, offset, limit, charStart, charEnd, remove(), changeValue()}