diff --git a/assets/scripts/app/helpers/helpers.coffee b/assets/scripts/app/helpers/helpers.coffee index c3f51333..2c4ed12e 100644 --- a/assets/scripts/app/helpers/helpers.coffee +++ b/assets/scripts/app/helpers/helpers.coffee @@ -1,4 +1,3 @@ -require 'travis/log' require 'config/emoij' @Travis.Helpers = diff --git a/assets/scripts/app/models.coffee b/assets/scripts/app/models.coffee index 28064194..8eab6753 100644 --- a/assets/scripts/app/models.coffee +++ b/assets/scripts/app/models.coffee @@ -1,6 +1,5 @@ require 'models/extensions' require 'models/account' -require 'models/artifact' require 'models/broadcast' require 'models/branch' require 'models/build' @@ -8,6 +7,7 @@ require 'models/commit' require 'models/event' require 'models/hook' require 'models/job' +require 'models/log' require 'models/repo' require 'models/sponsor' require 'models/user' diff --git a/assets/scripts/app/models/artifact.coffee b/assets/scripts/app/models/artifact.coffee deleted file mode 100644 index 26b87921..00000000 --- a/assets/scripts/app/models/artifact.coffee +++ /dev/null @@ -1,89 +0,0 @@ -require 'travis/model' - -@Travis.Artifact = Em.Object.extend - version: 1 # used to refresh log on requeue - body: null - isLoaded: false - - init: -> - @_super.apply this, arguments - - @addObserver 'job.id', @fetchBody - @fetchBody() - - @set 'queue', Ember.A([]) - @set 'parts', Ember.ArrayProxy.create(content: []) - - @addObserver 'body', @fetchWorker - @fetchWorker() - - id: (-> - @get('job.id') - ).property('job.id') - - clear: -> - @set('body', '') - @incrementProperty('version') - - fetchBody: -> - if jobId = @get('job.id') - @removeObserver 'job.id', @fetchBody - - self = this - Travis.ajax.ajax "/jobs/#{jobId}/log.txt?cors_hax=true", 'GET', - dataType: 'text' - contentType: 'text/plain' - success: (data, textStatus, xhr) -> - if xhr.status == 204 - logUrl = xhr.getResponseHeader('X-Log-Location') - - # For some reason not all browsers can fetch this header - unless logUrl - logUrl = self.s3Url("/jobs/#{jobId}/log.txt") - - $.ajax - url: logUrl - type: 'GET' - success: (data) -> - self.fetchedBody(data) - else - self.fetchedBody(data) - - s3Url: (path) -> - endpoint = Travis.config.api_endpoint - staging = if endpoint.match(/-staging/) then '-staging' else '' - host = Travis.config.api_endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.') - "https://s3.amazonaws.com/archive#{staging}.#{host}#{path}" - - - fetchedBody: (body) -> - @set 'body', body - @set 'isLoaded', true - - append: (body) -> - if @get('isInitialized') - @get('parts').pushObject body - @set('body', @get('body') + body) - else - @get('queue').pushObject(body) - - recordDidLoad: (-> - if @get('isLoaded') - if (body = @get 'body') && @get('parts.length') == 0 - @get('parts').pushObject body - - @set 'isInitialized', true - - queue = @get('queue') - if queue.get('length') > 0 - @append queue.toArray().join('') - ).observes('isLoaded') - - fetchWorker: -> - if !@get('workerName') && (body = @get('body')) - line = body.split("\n")[0] - if line && (match = line.match /Using worker: (.*)/) - if worker = match[1] - worker = worker.trim().split(':')[0] - @set('workerName', worker) - @removeObserver 'body', @fetchWorker diff --git a/assets/scripts/app/models/job.coffee b/assets/scripts/app/models/job.coffee index 8d56b5b3..bd6e9cfc 100644 --- a/assets/scripts/app/models/job.coffee +++ b/assets/scripts/app/models/job.coffee @@ -18,8 +18,9 @@ require 'travis/model' build: DS.belongsTo('Travis.Build') commit: DS.belongsTo('Travis.Commit') commits: DS.belongsTo('Travis.Commit') + log: ( -> - Travis.Artifact.create(job: this) + Travis.Log.create(job: this) ).property() repoSlug: (-> @@ -39,7 +40,7 @@ require 'travis/model' ).property('state') clearLog: -> - @get('log').clear() if @get('log.isLoaded') + @get('log').clear() sponsor: (-> worker = @get('log.workerName') @@ -71,13 +72,13 @@ require 'travis/model' requeue: -> Travis.ajax.post '/requests', job_id: @get('id') - appendLog: (text) -> - if log = @get('log') - log.append(text) + appendLog: (part) -> + @get('log').append part subscribe: -> - if id = @get('id') - Travis.pusher.subscribe "job-#{id}" + return if @get('subscribed') + @set('subscribed', true) + Travis.pusher.subscribe "job-#{@get('id')}" onStateChange: (-> if @get('state') == 'finished' && Travis.pusher diff --git a/assets/scripts/app/models/log.coffee b/assets/scripts/app/models/log.coffee new file mode 100644 index 00000000..1cd72187 --- /dev/null +++ b/assets/scripts/app/models/log.coffee @@ -0,0 +1,68 @@ +require 'travis/model' + +@Travis.Log = Em.Object.extend + version: 0 # used to refresh log on requeue + isLoaded: false + length: 0 + + init: -> + @set('parts', Ember.ArrayProxy.create(content: [])) + @fetch() + + fetch: -> + console.log 'fetch' + handlers = + json: (json) => @loadParts(json['log']['parts']) + text: (text) => @loadText(text) + Travis.Log.Request.create(id: id, handlers: handlers).run() if id = @get('job.id') + + clear: -> + @set('parts', Ember.ArrayProxy.create(content: [])) + @incrementProperty('version') + + append: (part) -> + @get('parts').pushObject(part) + + loadParts: (parts) -> + @append(part) for part in parts + @set('isLoaded', true) + + loadText: (text) -> + number = -1 + @append(number: 0, content: text) + @set('isLoaded', true) + +Travis.Log.Request = Em.Object.extend + HEADERS: + accept: 'application/vnd.travis-ci.2+json; chunked=true; version=2, text/plain; version=2' + + run: -> + Travis.ajax.ajax "/jobs/#{@id}/log?cors_hax=true", 'GET', + dataType: 'text' + headers: @HEADERS + success: (body, status, xhr) => @handle(body, status, xhr) + + handle: (body, status, xhr) -> + if xhr.status == 204 + $.ajax(url: @redirectTo(xhr), type: 'GET', success: @handlers.text) + else if @isJson(xhr, body) + @handlers.json(JSON.parse(body)) + else + @handlers.text(body) + + redirectTo: (xhr) -> + # Firefox can't see the Location header on the xhr response due to the wrong + # status code 204. Should be some redirect code but that doesn't work with CORS. + xhr.getResponseHeader('Location') || @s3Url() + + s3Url: -> + endpoint = Travis.config.api_endpoint + staging = if endpoint.match(/-staging/) then '-staging' else '' + host = endpoint.replace(/^https?:\/\//, '').split('.').slice(-2).join('.') + "https://s3.amazonaws.com/archive#{staging}.#{host}#{path}/jobs/#{@id}/log.txt" + + isJson: (xhr, body) -> + # Firefox can't see the Content-Type header on the xhr response due to the wrong + # status code 204. Should be some redirect code but that doesn't work with CORS. + type = xhr.getResponseHeader('Content-Type') || '' + type.indexOf('json') > -1 || body.slice(0, 8) == '{"log":{' diff --git a/assets/scripts/app/store.coffee b/assets/scripts/app/store.coffee index e3ec1074..e26cfd63 100644 --- a/assets/scripts/app/store.coffee +++ b/assets/scripts/app/store.coffee @@ -52,7 +52,7 @@ Travis.Store = DS.Store.extend !!@typeMapFor(type).idToCid[id] receive: (event, data) -> - console.log event, data + #console.log event, data [name, type] = event.split(':') mappings = @adapter.get('mappings') @@ -79,8 +79,9 @@ Travis.Store = DS.Store.extend if event == 'job:log' - if job = @find(Travis.Job, data['job']['id']) - job.appendLog(data['job']['_log']) + data = data.job + job = @find(Travis.Job, data.id) + job.appendLog(number: parseInt(data.number), content: data._log) else if data[type.singularName()] @_loadOne(this, type, data) else if data[type.pluralName()] diff --git a/assets/scripts/app/templates/builds/show.hbs b/assets/scripts/app/templates/builds/show.hbs index 8a0e2697..2b3f7802 100644 --- a/assets/scripts/app/templates/builds/show.hbs +++ b/assets/scripts/app/templates/builds/show.hbs @@ -46,6 +46,6 @@ {{view Travis.JobsView jobsBinding="build.requiredJobs" required="true"}} {{view Travis.JobsView jobsBinding="build.allowedFailureJobs"}} {{else}} - {{view Travis.LogView contextBinding="build.jobs.firstObject"}} + {{view Travis.LogView jobBinding="build.jobs.firstObject"}} {{/if}} {{/if}} diff --git a/assets/scripts/app/templates/jobs/log.hbs b/assets/scripts/app/templates/jobs/log.hbs index 36979890..fe64e071 100644 --- a/assets/scripts/app/templates/jobs/log.hbs +++ b/assets/scripts/app/templates/jobs/log.hbs @@ -1,13 +1,7 @@ -{{view.logSubscriber}} +{{#if view.log.isLoaded}} + {{view Travis.PreView logBinding="view.log" logUrlBinding="view.logUrl"}} -{{#if view.job.log.isLoaded}} - {{! this #with + #if is needed because I want to rerender 'pre' when log changes to properly clean it up, - this should probably be refactored to use container view}} - {{#with view.job.log}} - {{view Travis.PreView logBinding="view.context.log" logUrlBinding="view.logUrl"}} - {{/with}} - - {{#if sponsor.name}} + {{#if view.job.sponsor.name}}
{{formatConfig job.config}}
- {{view Travis.LogView contextBinding="job"}}} + {{view Travis.LogView jobBinding="job"}} {{else}}
diff --git a/assets/scripts/app/views.coffee b/assets/scripts/app/views.coffee index cb59e169..7dce5884 100644 --- a/assets/scripts/app/views.coffee +++ b/assets/scripts/app/views.coffee @@ -21,6 +21,7 @@ require 'views/build' require 'views/events' require 'views/flash' require 'views/job' +require 'views/log' require 'views/repo' require 'views/profile' require 'views/sidebar' diff --git a/assets/scripts/app/views/job.coffee b/assets/scripts/app/views/job.coffee index e78ca0f5..9027ebf7 100644 --- a/assets/scripts/app/views/job.coffee +++ b/assets/scripts/app/views/job.coffee @@ -1,4 +1,4 @@ -@Travis.reopen +Travis.reopen JobsView: Travis.View.extend templateName: 'jobs/list' buildBinding: 'controller.build' @@ -37,204 +37,3 @@ urlCommitter: (-> Travis.Urls.email(@get('commit.committerEmail')) ).property('commit.committerEmail') - - LogView: Travis.View.extend - templateName: 'jobs/log' - logBinding: 'job.log' - - plainTextLogUrl: (-> - if id = @get('job.log.id') - Travis.Urls.plainTextLog(id) - ).property('job.log') - - didInsertElement: -> - @_super.apply this, arguments - @tryScrollingToHashLineNumber() - - scrollTo: (hash) -> - # and this is even more weird, when changing hash in URL in firefox - # to other value, for example #L10, it actually scrolls just #main - # element... this is probably some CSS issue, I don't have time to - # investigate at the moment - # TODO: fix this - $('#main').scrollTop 0 - - # weird, html works in chrome, body in firefox - $('html,body').scrollTop $(hash).offset().top - - @set 'controller.lineNumberHash', null - - lineNumberHashDidChange: (-> - @tryScrollingToHashLineNumber() - ).observes('controller.lineNumberHash') - - tryScrollingToHashLineNumber: -> - if hash = @get 'controller.lineNumberHash' - self = this - - checker = -> - return if self.get('isDestroyed') - - if $(hash).length - self.scrollTo(hash) - else - setTimeout checker, 100 - - checker() - - click: -> - target = $(event.target) - - target.closest('.fold').toggleClass('open') - - if target.is('a') && target.attr('id') && target.attr('id').match(/^L\d+$/) - path = target.attr 'href' - Travis.get('router').route(path) - event.stopPropagation() - return false - - toTop: () -> - $(window).scrollTop(0) - - jobBinding: 'context' - - logSubscriber: (-> - # for some reason observing context does not work, - # TODO: find out why - job = @get('job') - job.subscribe() if job && !job.get('isFinished') - null - ).property('job', 'job.state') - - logUrl: (-> - repo = @get('job.repo') - item = @get('parentView.currentItem') - - if repo && item - event = if item.constructor == Travis.Build - 'showBuild' - else - 'showJob' - - #Travis.get('router').urlForEvent(event, repo, item) - ).property('job.repo', 'parentView.currentItem') - - PreView: Em.View.extend - templateName: 'jobs/pre' - init: -> - @_super.apply this, arguments - @set 'logManager', Travis.Log.create(target: this) - - toggleTailing: -> - Travis.tailing.toggle() - event.preventDefault() - - didInsertElement: -> - @_super.apply this, arguments - - Ember.run.next this, -> - if @get 'log.isInitialized' - @logDidChange() - - willDestroy: -> - @get('logManager').destroy() - @get('log.parts').removeArrayObserver this, - didChange: 'logContentsDidChange' - willChange: 'logContentsWillChange' - - version: (-> - @rerender() - @set 'logManager', Travis.Log.create(target: this) - ).observes('log.version') - - logDidChange: (-> - if @get('log.isInitialized') && @state == 'inDOM' - @attachLogObservers() - ).observes('log', 'log.isInitialized') - - attachLogObservers: -> - return if @get('logPartsObserversAttached') == Ember.guidFor(@get('log')) - @set 'logPartsObserversAttached', Ember.guidFor(@get('log')) - - Ember.run.next this, -> - @get('logManager').append @get('log.parts') - - @get('log.parts').addArrayObserver this, - didChange: 'logContentsDidChange' - willChange: 'logContentsWillChange' - - logContentsDidChange: (lines, index, removedCount, addedCount) -> - addedLines = lines.slice(index, index + addedCount) - @get('logManager').append addedLines - - logContentsWillChange: (-> ) - - appendLog: (payloads) -> - url = @get('logUrl') - - leftOut = [] - cut = false - fragment = document.createDocumentFragment() - - # TODO: refactor this loop, it's getting messy - for payload in payloads - line = payload.content - number = payload.number - - if payload.logWasCut - cut = true - else - unless payload.append - pathWithNumber = "#{url}#L#{number}" - p = document.createElement('p') - p.innerHTML = "#{number}#{line}" - line = p - - if payload.fold && !payload.foldContinuation - div = document.createElement('div') - div.appendChild line - div.className = "fold #{payload.fold} show-first-line" - line = div - - if payload.replace - if link = fragment.querySelector("#L#{number}") - link.parentElement.innerHTML = line.innerHTML - else - this.$("#L#{number}").parent().replaceWith line - else if payload.append - if link = fragment.querySelector("#L#{number}") - link.parentElement.innerHTML += line - else - this.$("#L#{number}").parent().append line - else if payload.foldContinuation - folds = fragment.querySelectorAll(".fold.#{payload.fold}") - if fold = folds[folds.length - 1] - fold.appendChild line - else - this.$("#log .fold.#{payload.fold}:last").append line - else - fragment.appendChild(line) - - if payload.openFold - folds = fragment.querySelectorAll(".fold.#{payload.openFold}") - if fold = folds[folds.length - 1] - fold = $(fold) - else - fold = this.$(".fold.#{payload.openFold}:last") - - fold.removeClass('show-first-line').addClass('open') - - if payload.foldEnd - folds = fragment.querySelectorAll(".fold.#{payload.fold}") - if fold = folds[folds.length - 1] - fold = $(fold) - else - fold = this.$(".fold.#{payload.fold}:last") - - fold.removeClass('show-first-line') - - this.$('#log')[0].appendChild fragment - if cut - url = Travis.Urls.plainTextLog(@get('log.id')) - this.$("#log").append $("

Log was too long to display. Download the the raw version to get the full log.

") - diff --git a/assets/scripts/app/views/log.coffee b/assets/scripts/app/views/log.coffee new file mode 100644 index 00000000..fd99e324 --- /dev/null +++ b/assets/scripts/app/views/log.coffee @@ -0,0 +1,190 @@ +require 'log' + +Travis.reopen + LogView: Travis.View.extend + templateName: 'jobs/log' + logBinding: 'job.log' + contextBinding: 'job' + + init: -> + @_super.apply this, arguments + + plainTextLogUrl: (-> + if id = @get('job.log.id') + Travis.Urls.plainTextLog(id) + ).property('log') + + didInsertElement: -> + job = @get('job') + job.subscribe() if job && !job.get('isFinished') + + @_super.apply this, arguments + #@tryScrollingToHashLineNumber() + + scrollTo: (hash) -> + # and this is even more weird, when changing hash in URL in firefox + # to other value, for example #L10, it actually scrolls just #main + # element... this is probably some CSS issue, I don't have time to + # investigate at the moment + # TODO: fix this + $('#main').scrollTop 0 + + # weird, html works in chrome, body in firefox + $('html,body').scrollTop $(hash).offset().top + + @set 'controller.lineNumberHash', null + + lineNumberHashDidChange: (-> + @tryScrollingToHashLineNumber() + ).observes('controller.lineNumberHash') + + tryScrollingToHashLineNumber: -> + if hash = @get 'controller.lineNumberHash' + self = this + + checker = -> + return if self.get('isDestroyed') + + if $(hash).length + self.scrollTo(hash) + else + setTimeout checker, 100 + + checker() + + click: -> + target = $(event.target) + + target.closest('.fold').toggleClass('open') + + if target.is('a') && target.attr('id') && target.attr('id').match(/^L\d+$/) + path = target.attr 'href' + Travis.get('router').route(path) + event.stopPropagation() + return false + + toTop: () -> + $(window).scrollTop(0) + + logUrl: (-> + repo = @get('job.repo') + item = @get('parentView.currentItem') + + if repo && item + event = if item.constructor == Travis.Build + 'showBuild' + else + 'showJob' + + #Travis.get('router').urlForEvent(event, repo, item) + ).property('job.repo', 'parentView.currentItem') + + PreView: Em.View.extend + templateName: 'jobs/pre' + + createEngine: -> + limit = new Log.Limit + scroll = new Log.Scroll + engine = Log.create(listeners: [limit, new Log.FragmentRenderer, new Log.Folds, scroll]) + + @set('scroll', scroll) + @set('limit', limit) + @set('engine', engine) + + @observeParts() + + observeParts: -> + parts = @get('log.parts') + parts.addArrayObserver(@, didChange: 'partsDidChange', willChange: 'partsWillChange') + @partsDidChange(parts.slice(0)) + + didInsertElement: -> + @_super.apply this, arguments + @createEngine() + + willDestroyElement: -> + parts = @get('log.parts') + parts.removeArrayObserver(@, didChange: 'partsDidChange', willChange: 'partsWillChange') + + partsWillChange: -> + + partsDidChange: (parts, start, _, added) -> + unless @get('isLimited') + start ||= 0 + added ||= parts.length + @get('engine').set(part.number, part.content) for part, i in parts.slice(start, start + added) + #@propertyDidChange('isLimited') + + versionDidChange: (-> + @rerender() if @get('inDOM') + ).observes('log.version') + + logDidChange: (-> + @rerender() if @get('inDOM') + ).observes('log') + + isLimited: (-> + @limit && @limit.isLimited() + ).property() + + plainTextLogUrl: (-> + Travis.Urls.plainTextLog(id) if id = @get('log.job.id') + ).property('job.log.id') + + toggleTailing: (event) -> + Travis.app.tailing.toggle() + event.preventDefault() + + lineNumbers: -> + $('#log').on 'mouseenter', 'a', -> + $(this).attr('href', '#L' + ($(this.parentNode).prevAll('p').length + 1)) + + folds: -> + $('#log').on 'click', '.fold', -> + $(this).toggleClass('open') + + click: (event) -> + target = $(event.target) + target.closest('.fold').toggleClass('open') + if target.is('a') && matches = target.attr('href')?.match(/#L(\d+)$/) + Travis.app.get('router.location').setURL(target.attr('href')) + @set('controller.lineNumber', matches[1]) + event.stopPropagation() + return false + + lineNumberObserver: (-> + @scroll.set(number) if !@get('isDestroyed') && number = @get('controller.lineNumber') + ).observes('controller.lineNumber') + +Log.Scroll = -> +Log.Scroll.prototype = $.extend new Log.Listener, + set: (number) -> + return unless number + @number = number + @tryScroll() + + insert: (log, after, data) -> + @tryScroll() if @number + + tryScroll: -> + if element = $("#log p:nth-child(#{@number})") + $('#main').scrollTop(0) + $('html, body').scrollTop(element.offset()?.top) # weird, html works in chrome, body in firefox + @highlight(element) + @number = undefined + + highlight: (element) -> + $('#log p.highlight').removeClass('highlight') + $(element).addClass('highlight') + +Log.Limit = -> +Log.Limit.prototype = $.extend new Log.Listener, + MAX_LINES: 5000 + count: 0 + + insert: (log, after, lines) -> + @count += lines.length + lines.length = @MAX_LINES if lines.length > @MAX_LINES + + isLimited: -> + @count > @MAX_LINES diff --git a/assets/scripts/lib/travis/log.coffee b/assets/scripts/lib/travis/log.coffee deleted file mode 100644 index e4d5b88a..00000000 --- a/assets/scripts/lib/travis/log.coffee +++ /dev/null @@ -1,152 +0,0 @@ -# TODO: revisit those patterns -FOLDS = [ - Em.Object.create(name: 'schema', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:schema:load/, endPattern: /^(<\/span>)?\$/) - Em.Object.create(name: 'migrate', startPattern: /^\$ (?:bundle exec )?rake( db:create)? db:migrate/, endPattern: /^(<\/span>)?\$/) - Em.Object.create(name: 'bundle', startPattern: /^\$ bundle install/, endPattern: /^(<\/span>)?\$/) -] - -@Travis.Log = Em.Object.extend - init: -> - @set 'folds', [] - @set 'line', 1 - @set 'lineNumber', 1 - @initial = true - - for fold in FOLDS - @addFold fold - - append: (lines) -> - return unless lines - return if @get('lineNumber') > 5000 - - log = @join lines - log = @escape log - log = @deansi log - lines = @split log - - target = @get 'target' - index = 0 - currentFold = @currentFold - - result = [] - - for line in lines - if line == '\r' - @set 'replace', true - else if line == '\n' - @set 'newline', true - index += 1 - else - if currentFold && ( @isFoldEnding(currentFold, line) ) - # end of the fold, send fold to target - if result.length > 0 - result.slice(-1)[0].foldEnd = true - target.appendLog result - - @currentFold = currentFold = null - @set 'foldContinuation', false - result = [] - - if !currentFold && ( currentFold = @foldByStart(line) ) - # beginning new fold, send current lines to target - if result.length > 0 - target.appendLog result - - result = [] - start = index - - payload = { content: line } - - if currentFold - payload.fold = currentFold.get('name') - - if @get 'foldContinuation' - payload.foldContinuation = true - - payload.number = @get('lineNumber') + index - - if @get 'replace' - @set 'replace', false - payload.replace = true - else if @get 'newline' - @set 'newline', false - else if !@initial - payload.append = true - - @initial = false - - if payload.foldContinuation && payload.content.match(/Done. Build script exited|Your build has been stopped/) - # script ended, but fold is still closed, which most probably means - # error, end the fold and open it. - # TODO: we need log marks to make it easier - payload.foldContinuation = null - payload.openFold = payload.fold - payload.fold = null - - result.pushObject payload - - if currentFold - @set 'foldContinuation', true - - if @get('lineNumber') + index >= 5000 - result.pushObject logWasCut: true - break - - if result.length > 0 - if currentFold - @currentFold = currentFold - - target.appendLog result - - nextLineNumber = @get('lineNumber') + index - @set 'lineNumber', nextLineNumber - - join: (lines) -> - if typeof lines == 'string' - lines - else - lines.toArray().join '' - - split: (log) -> - log = log.replace /\r\n/g, '\n' - lines = log.split(/(\n)/) - - if lines.slice(-1)[0] == '' - lines.popObject() - - result = [] - for line in lines - result.pushObjects line.split(/(\r)/) - - result - - escape: (log) -> - Handlebars.Utils.escapeExpression log - - deansi: (log) -> - log = log.replace(/\r\r/g, '\r') - .replace(/\033\[K\r/g, '\r') - .replace(/\[2K/g, '') - .replace(/\033\(B/g, '') - .replace(/\033\[\d+G/g, '') - - ansi = ansiparse(log) - - text = '' - ansi.forEach (part) -> - classes = [] - part.foreground and classes.push(part.foreground) - part.background and classes.push('bg-' + part.background) - part.bold and classes.push('bold') - part.italic and classes.push('italic') - text += (if classes.length then ('' + part.text + '') else part.text) - text.replace /\033/g, '' - - addFold: (fold) -> - @get('folds').pushObject fold - - foldByStart: (line) -> - @get('folds').find (fold) -> line.match(fold.get('startPattern')) - - isFoldEnding: (fold, line) -> - line.match(fold.get('endPattern'))