openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

highlight.js (10140B)


      1 import { countColumn } from "../util/misc.js"
      2 import { copyState, innerMode, startState } from "../modes.js"
      3 import StringStream from "../util/StringStream.js"
      4 
      5 import { getLine, lineNo } from "./utils_line.js"
      6 import { clipPos } from "./pos.js"
      7 
      8 class SavedContext {
      9   constructor(state, lookAhead) {
     10     this.state = state
     11     this.lookAhead = lookAhead
     12   }
     13 }
     14 
     15 class Context {
     16   constructor(doc, state, line, lookAhead) {
     17     this.state = state
     18     this.doc = doc
     19     this.line = line
     20     this.maxLookAhead = lookAhead || 0
     21     this.baseTokens = null
     22     this.baseTokenPos = 1
     23   }
     24 
     25   lookAhead(n) {
     26     let line = this.doc.getLine(this.line + n)
     27     if (line != null && n > this.maxLookAhead) this.maxLookAhead = n
     28     return line
     29   }
     30 
     31   baseToken(n) {
     32     if (!this.baseTokens) return null
     33     while (this.baseTokens[this.baseTokenPos] <= n)
     34       this.baseTokenPos += 2
     35     let type = this.baseTokens[this.baseTokenPos + 1]
     36     return {type: type && type.replace(/( |^)overlay .*/, ""),
     37             size: this.baseTokens[this.baseTokenPos] - n}
     38   }
     39 
     40   nextLine() {
     41     this.line++
     42     if (this.maxLookAhead > 0) this.maxLookAhead--
     43   }
     44 
     45   static fromSaved(doc, saved, line) {
     46     if (saved instanceof SavedContext)
     47       return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead)
     48     else
     49       return new Context(doc, copyState(doc.mode, saved), line)
     50   }
     51 
     52   save(copy) {
     53     let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
     54     return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
     55   }
     56 }
     57 
     58 
     59 // Compute a style array (an array starting with a mode generation
     60 // -- for invalidation -- followed by pairs of end positions and
     61 // style strings), which is used to highlight the tokens on the
     62 // line.
     63 export function highlightLine(cm, line, context, forceToEnd) {
     64   // A styles array always starts with a number identifying the
     65   // mode/overlays that it is based on (for easy invalidation).
     66   let st = [cm.state.modeGen], lineClasses = {}
     67   // Compute the base array of styles
     68   runMode(cm, line.text, cm.doc.mode, context, (end, style) => st.push(end, style),
     69           lineClasses, forceToEnd)
     70   let state = context.state
     71 
     72   // Run overlays, adjust style array.
     73   for (let o = 0; o < cm.state.overlays.length; ++o) {
     74     context.baseTokens = st
     75     let overlay = cm.state.overlays[o], i = 1, at = 0
     76     context.state = true
     77     runMode(cm, line.text, overlay.mode, context, (end, style) => {
     78       let start = i
     79       // Ensure there's a token end at the current position, and that i points at it
     80       while (at < end) {
     81         let i_end = st[i]
     82         if (i_end > end)
     83           st.splice(i, 1, end, st[i+1], i_end)
     84         i += 2
     85         at = Math.min(end, i_end)
     86       }
     87       if (!style) return
     88       if (overlay.opaque) {
     89         st.splice(start, i - start, end, "overlay " + style)
     90         i = start + 2
     91       } else {
     92         for (; start < i; start += 2) {
     93           let cur = st[start+1]
     94           st[start+1] = (cur ? cur + " " : "") + "overlay " + style
     95         }
     96       }
     97     }, lineClasses)
     98     context.state = state
     99     context.baseTokens = null
    100     context.baseTokenPos = 1
    101   }
    102 
    103   return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
    104 }
    105 
    106 export function getLineStyles(cm, line, updateFrontier) {
    107   if (!line.styles || line.styles[0] != cm.state.modeGen) {
    108     let context = getContextBefore(cm, lineNo(line))
    109     let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
    110     let result = highlightLine(cm, line, context)
    111     if (resetState) context.state = resetState
    112     line.stateAfter = context.save(!resetState)
    113     line.styles = result.styles
    114     if (result.classes) line.styleClasses = result.classes
    115     else if (line.styleClasses) line.styleClasses = null
    116     if (updateFrontier === cm.doc.highlightFrontier)
    117       cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier)
    118   }
    119   return line.styles
    120 }
    121 
    122 export function getContextBefore(cm, n, precise) {
    123   let doc = cm.doc, display = cm.display
    124   if (!doc.mode.startState) return new Context(doc, true, n)
    125   let start = findStartLine(cm, n, precise)
    126   let saved = start > doc.first && getLine(doc, start - 1).stateAfter
    127   let context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)
    128 
    129   doc.iter(start, n, line => {
    130     processLine(cm, line.text, context)
    131     let pos = context.line
    132     line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
    133     context.nextLine()
    134   })
    135   if (precise) doc.modeFrontier = context.line
    136   return context
    137 }
    138 
    139 // Lightweight form of highlight -- proceed over this line and
    140 // update state, but don't save a style array. Used for lines that
    141 // aren't currently visible.
    142 export function processLine(cm, text, context, startAt) {
    143   let mode = cm.doc.mode
    144   let stream = new StringStream(text, cm.options.tabSize, context)
    145   stream.start = stream.pos = startAt || 0
    146   if (text == "") callBlankLine(mode, context.state)
    147   while (!stream.eol()) {
    148     readToken(mode, stream, context.state)
    149     stream.start = stream.pos
    150   }
    151 }
    152 
    153 function callBlankLine(mode, state) {
    154   if (mode.blankLine) return mode.blankLine(state)
    155   if (!mode.innerMode) return
    156   let inner = innerMode(mode, state)
    157   if (inner.mode.blankLine) return inner.mode.blankLine(inner.state)
    158 }
    159 
    160 export function readToken(mode, stream, state, inner) {
    161   for (let i = 0; i < 10; i++) {
    162     if (inner) inner[0] = innerMode(mode, state).mode
    163     let style = mode.token(stream, state)
    164     if (stream.pos > stream.start) return style
    165   }
    166   throw new Error("Mode " + mode.name + " failed to advance stream.")
    167 }
    168 
    169 class Token {
    170   constructor(stream, type, state) {
    171     this.start = stream.start; this.end = stream.pos
    172     this.string = stream.current()
    173     this.type = type || null
    174     this.state = state
    175   }
    176 }
    177 
    178 // Utility for getTokenAt and getLineTokens
    179 export function takeToken(cm, pos, precise, asArray) {
    180   let doc = cm.doc, mode = doc.mode, style
    181   pos = clipPos(doc, pos)
    182   let line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
    183   let stream = new StringStream(line.text, cm.options.tabSize, context), tokens
    184   if (asArray) tokens = []
    185   while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
    186     stream.start = stream.pos
    187     style = readToken(mode, stream, context.state)
    188     if (asArray) tokens.push(new Token(stream, style, copyState(doc.mode, context.state)))
    189   }
    190   return asArray ? tokens : new Token(stream, style, context.state)
    191 }
    192 
    193 function extractLineClasses(type, output) {
    194   if (type) for (;;) {
    195     let lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
    196     if (!lineClass) break
    197     type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
    198     let prop = lineClass[1] ? "bgClass" : "textClass"
    199     if (output[prop] == null)
    200       output[prop] = lineClass[2]
    201     else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
    202       output[prop] += " " + lineClass[2]
    203   }
    204   return type
    205 }
    206 
    207 // Run the given mode's parser over a line, calling f for each token.
    208 function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
    209   let flattenSpans = mode.flattenSpans
    210   if (flattenSpans == null) flattenSpans = cm.options.flattenSpans
    211   let curStart = 0, curStyle = null
    212   let stream = new StringStream(text, cm.options.tabSize, context), style
    213   let inner = cm.options.addModeClass && [null]
    214   if (text == "") extractLineClasses(callBlankLine(mode, context.state), lineClasses)
    215   while (!stream.eol()) {
    216     if (stream.pos > cm.options.maxHighlightLength) {
    217       flattenSpans = false
    218       if (forceToEnd) processLine(cm, text, context, stream.pos)
    219       stream.pos = text.length
    220       style = null
    221     } else {
    222       style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
    223     }
    224     if (inner) {
    225       let mName = inner[0].name
    226       if (mName) style = "m-" + (style ? mName + " " + style : mName)
    227     }
    228     if (!flattenSpans || curStyle != style) {
    229       while (curStart < stream.start) {
    230         curStart = Math.min(stream.start, curStart + 5000)
    231         f(curStart, curStyle)
    232       }
    233       curStyle = style
    234     }
    235     stream.start = stream.pos
    236   }
    237   while (curStart < stream.pos) {
    238     // Webkit seems to refuse to render text nodes longer than 57444
    239     // characters, and returns inaccurate measurements in nodes
    240     // starting around 5000 chars.
    241     let pos = Math.min(stream.pos, curStart + 5000)
    242     f(pos, curStyle)
    243     curStart = pos
    244   }
    245 }
    246 
    247 // Finds the line to start with when starting a parse. Tries to
    248 // find a line with a stateAfter, so that it can start with a
    249 // valid state. If that fails, it returns the line with the
    250 // smallest indentation, which tends to need the least context to
    251 // parse correctly.
    252 function findStartLine(cm, n, precise) {
    253   let minindent, minline, doc = cm.doc
    254   let lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
    255   for (let search = n; search > lim; --search) {
    256     if (search <= doc.first) return doc.first
    257     let line = getLine(doc, search - 1), after = line.stateAfter
    258     if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
    259       return search
    260     let indented = countColumn(line.text, null, cm.options.tabSize)
    261     if (minline == null || minindent > indented) {
    262       minline = search - 1
    263       minindent = indented
    264     }
    265   }
    266   return minline
    267 }
    268 
    269 export function retreatFrontier(doc, n) {
    270   doc.modeFrontier = Math.min(doc.modeFrontier, n)
    271   if (doc.highlightFrontier < n - 10) return
    272   let start = doc.first
    273   for (let line = n - 1; line > start; line--) {
    274     let saved = getLine(doc, line).stateAfter
    275     // change is on 3
    276     // state on line 1 looked ahead 2 -- so saw 3
    277     // test 1 + 2 < 3 should cover this
    278     if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) {
    279       start = line + 1
    280       break
    281     }
    282   }
    283   doc.highlightFrontier = Math.min(doc.highlightFrontier, start)
    284 }