From 3eee7b93fc75b61b290fcce0f7af24bcfffa00e3 Mon Sep 17 00:00:00 2001 From: Nickolay Ponomarev Date: Tue, 8 Mar 2016 18:23:10 +0300 Subject: [PATCH] Split the code doing highlighting from the rest. --- src/highlights.coffee | 58 ++------------------------------ src/tokens-to-html.coffee | 70 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 56 deletions(-) create mode 100644 src/tokens-to-html.coffee diff --git a/src/highlights.coffee b/src/highlights.coffee index 5767988..e25cb89 100644 --- a/src/highlights.coffee +++ b/src/highlights.coffee @@ -1,11 +1,11 @@ path = require 'path' -_ = require 'underscore-plus' fs = require 'fs-plus' CSON = require 'season' once = require 'once' {GrammarRegistry} = require 'first-mate' Selector = require 'first-mate-select-grammar' selector = Selector() +TokensToHTML = require './tokens-to-html' module.exports = @@ -152,26 +152,7 @@ class Highlights grammar ?= selector.selectGrammar(@registry, filePath, fileContents) lineTokens = grammar.tokenizeLines(fileContents) - - # Remove trailing newline - if lineTokens.length > 0 - lastLineTokens = lineTokens[lineTokens.length - 1] - - if lastLineTokens.length is 1 and lastLineTokens[0].value is '' - lineTokens.pop() - - html = '
'
-    for tokens in lineTokens
-      scopeStack = []
-      html += '
' - for {value, scopes} in tokens - value = ' ' unless value - html = @updateScopeStack(scopeStack, scopes, html) - html += "#{@escapeString(value)}" - html = @popScope(scopeStack, html) while scopeStack.length > 0 - html += '
' - html += '
' - html + TokensToHTML.highlightFromTokens(lineTokens) loadGrammarsSync: -> return if @registry.grammars.length > 1 @@ -269,38 +250,3 @@ class Highlights cb(false, JSON.parse(contents)) catch err return cb(err) - - escapeString: (string) -> - string.replace /[&"'<> ]/g, (match) -> - switch match - when '&' then '&' - when '"' then '"' - when "'" then ''' - when '<' then '<' - when '>' then '>' - when ' ' then ' ' - else match - - updateScopeStack: (scopeStack, desiredScopes, html) -> - excessScopes = scopeStack.length - desiredScopes.length - if excessScopes > 0 - html = @popScope(scopeStack, html) while excessScopes-- - - # pop until common prefix - for i in [scopeStack.length..0] - break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) - html = @popScope(scopeStack, html) - - # push on top of common prefix until scopeStack is desiredScopes - for j in [i...desiredScopes.length] - html = @pushScope(scopeStack, desiredScopes[j], html) - - html - - pushScope: (scopeStack, scope, html) -> - scopeStack.push(scope) - html += "" - - popScope: (scopeStack, html) -> - scopeStack.pop() - html += '' diff --git a/src/tokens-to-html.coffee b/src/tokens-to-html.coffee new file mode 100644 index 0000000..afa62c1 --- /dev/null +++ b/src/tokens-to-html.coffee @@ -0,0 +1,70 @@ +_ = require 'underscore-plus' + +module.exports = +class TokensToHTML + # Returns HTML string ready for syntax highlighting with CSS, given an Array + # of tokenized lines. Each tokenized line is itself an Array of tokens. + # Each token is an Object with two keys: + # :value - the substring of the source text corresponding to the token + # :scopes - an Array of CSS class names that apply to the token, in the + # order from the least to the most specific, e.g.: + # ['source.python', 'keyword.other.python'] + # Essentially the output HTML for each token is: + # ${escape(value)} + # ...but we try to minimize the number of CSS classes on each span, by nesting + # consecutive spans with a common scope in a parent . + @highlightFromTokens: (lineTokens) -> + # Remove trailing newline + if lineTokens.length > 0 + lastLineTokens = lineTokens[lineTokens.length - 1] + + if lastLineTokens.length is 1 and lastLineTokens[0].value is '' + lineTokens.pop() + + html = '
'
+    for tokens in lineTokens
+      scopeStack = []
+      html += '
' + for {value, scopes} in tokens + value = ' ' unless value + html = @updateScopeStack(scopeStack, scopes, html) + html += "#{@escapeString(value)}" + html = @popScope(scopeStack, html) while scopeStack.length > 0 + html += '
' + html += '
' + html + + @escapeString: (string) -> + string.replace /[&"'<> ]/g, (match) -> + switch match + when '&' then '&' + when '"' then '"' + when "'" then ''' + when '<' then '<' + when '>' then '>' + when ' ' then ' ' + else match + + @updateScopeStack: (scopeStack, desiredScopes, html) -> + excessScopes = scopeStack.length - desiredScopes.length + if excessScopes > 0 + html = @popScope(scopeStack, html) while excessScopes-- + + # pop until common prefix + for i in [scopeStack.length..0] + break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) + html = @popScope(scopeStack, html) + + # push on top of common prefix until scopeStack is desiredScopes + for j in [i...desiredScopes.length] + html = @pushScope(scopeStack, desiredScopes[j], html) + + html + + @pushScope: (scopeStack, scope, html) -> + scopeStack.push(scope) + html += "" + + @popScope: (scopeStack, html) -> + scopeStack.pop() + html += ''