diff .cms/lib/codemirror/src/line/highlight.js @ 0:78edf6b517a0 draft

24.10
author Coffee CMS <info@coffee-cms.ru>
date Fri, 11 Oct 2024 22:40:23 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.cms/lib/codemirror/src/line/highlight.js	Fri Oct 11 22:40:23 2024 +0000
@@ -0,0 +1,284 @@
+import { countColumn } from "../util/misc.js"
+import { copyState, innerMode, startState } from "../modes.js"
+import StringStream from "../util/StringStream.js"
+
+import { getLine, lineNo } from "./utils_line.js"
+import { clipPos } from "./pos.js"
+
+class SavedContext {
+  constructor(state, lookAhead) {
+    this.state = state
+    this.lookAhead = lookAhead
+  }
+}
+
+class Context {
+  constructor(doc, state, line, lookAhead) {
+    this.state = state
+    this.doc = doc
+    this.line = line
+    this.maxLookAhead = lookAhead || 0
+    this.baseTokens = null
+    this.baseTokenPos = 1
+  }
+
+  lookAhead(n) {
+    let line = this.doc.getLine(this.line + n)
+    if (line != null && n > this.maxLookAhead) this.maxLookAhead = n
+    return line
+  }
+
+  baseToken(n) {
+    if (!this.baseTokens) return null
+    while (this.baseTokens[this.baseTokenPos] <= n)
+      this.baseTokenPos += 2
+    let type = this.baseTokens[this.baseTokenPos + 1]
+    return {type: type && type.replace(/( |^)overlay .*/, ""),
+            size: this.baseTokens[this.baseTokenPos] - n}
+  }
+
+  nextLine() {
+    this.line++
+    if (this.maxLookAhead > 0) this.maxLookAhead--
+  }
+
+  static fromSaved(doc, saved, line) {
+    if (saved instanceof SavedContext)
+      return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead)
+    else
+      return new Context(doc, copyState(doc.mode, saved), line)
+  }
+
+  save(copy) {
+    let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
+    return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
+  }
+}
+
+
+// Compute a style array (an array starting with a mode generation
+// -- for invalidation -- followed by pairs of end positions and
+// style strings), which is used to highlight the tokens on the
+// line.
+export function highlightLine(cm, line, context, forceToEnd) {
+  // A styles array always starts with a number identifying the
+  // mode/overlays that it is based on (for easy invalidation).
+  let st = [cm.state.modeGen], lineClasses = {}
+  // Compute the base array of styles
+  runMode(cm, line.text, cm.doc.mode, context, (end, style) => st.push(end, style),
+          lineClasses, forceToEnd)
+  let state = context.state
+
+  // Run overlays, adjust style array.
+  for (let o = 0; o < cm.state.overlays.length; ++o) {
+    context.baseTokens = st
+    let overlay = cm.state.overlays[o], i = 1, at = 0
+    context.state = true
+    runMode(cm, line.text, overlay.mode, context, (end, style) => {
+      let start = i
+      // Ensure there's a token end at the current position, and that i points at it
+      while (at < end) {
+        let i_end = st[i]
+        if (i_end > end)
+          st.splice(i, 1, end, st[i+1], i_end)
+        i += 2
+        at = Math.min(end, i_end)
+      }
+      if (!style) return
+      if (overlay.opaque) {
+        st.splice(start, i - start, end, "overlay " + style)
+        i = start + 2
+      } else {
+        for (; start < i; start += 2) {
+          let cur = st[start+1]
+          st[start+1] = (cur ? cur + " " : "") + "overlay " + style
+        }
+      }
+    }, lineClasses)
+    context.state = state
+    context.baseTokens = null
+    context.baseTokenPos = 1
+  }
+
+  return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
+}
+
+export function getLineStyles(cm, line, updateFrontier) {
+  if (!line.styles || line.styles[0] != cm.state.modeGen) {
+    let context = getContextBefore(cm, lineNo(line))
+    let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
+    let result = highlightLine(cm, line, context)
+    if (resetState) context.state = resetState
+    line.stateAfter = context.save(!resetState)
+    line.styles = result.styles
+    if (result.classes) line.styleClasses = result.classes
+    else if (line.styleClasses) line.styleClasses = null
+    if (updateFrontier === cm.doc.highlightFrontier)
+      cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier)
+  }
+  return line.styles
+}
+
+export function getContextBefore(cm, n, precise) {
+  let doc = cm.doc, display = cm.display
+  if (!doc.mode.startState) return new Context(doc, true, n)
+  let start = findStartLine(cm, n, precise)
+  let saved = start > doc.first && getLine(doc, start - 1).stateAfter
+  let context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)
+
+  doc.iter(start, n, line => {
+    processLine(cm, line.text, context)
+    let pos = context.line
+    line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
+    context.nextLine()
+  })
+  if (precise) doc.modeFrontier = context.line
+  return context
+}
+
+// Lightweight form of highlight -- proceed over this line and
+// update state, but don't save a style array. Used for lines that
+// aren't currently visible.
+export function processLine(cm, text, context, startAt) {
+  let mode = cm.doc.mode
+  let stream = new StringStream(text, cm.options.tabSize, context)
+  stream.start = stream.pos = startAt || 0
+  if (text == "") callBlankLine(mode, context.state)
+  while (!stream.eol()) {
+    readToken(mode, stream, context.state)
+    stream.start = stream.pos
+  }
+}
+
+function callBlankLine(mode, state) {
+  if (mode.blankLine) return mode.blankLine(state)
+  if (!mode.innerMode) return
+  let inner = innerMode(mode, state)
+  if (inner.mode.blankLine) return inner.mode.blankLine(inner.state)
+}
+
+function readToken(mode, stream, state, inner) {
+  for (let i = 0; i < 10; i++) {
+    if (inner) inner[0] = innerMode(mode, state).mode
+    let style = mode.token(stream, state)
+    if (stream.pos > stream.start) return style
+  }
+  throw new Error("Mode " + mode.name + " failed to advance stream.")
+}
+
+class Token {
+  constructor(stream, type, state) {
+    this.start = stream.start; this.end = stream.pos
+    this.string = stream.current()
+    this.type = type || null
+    this.state = state
+  }
+}
+
+// Utility for getTokenAt and getLineTokens
+export function takeToken(cm, pos, precise, asArray) {
+  let doc = cm.doc, mode = doc.mode, style
+  pos = clipPos(doc, pos)
+  let line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
+  let stream = new StringStream(line.text, cm.options.tabSize, context), tokens
+  if (asArray) tokens = []
+  while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+    stream.start = stream.pos
+    style = readToken(mode, stream, context.state)
+    if (asArray) tokens.push(new Token(stream, style, copyState(doc.mode, context.state)))
+  }
+  return asArray ? tokens : new Token(stream, style, context.state)
+}
+
+function extractLineClasses(type, output) {
+  if (type) for (;;) {
+    let lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
+    if (!lineClass) break
+    type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
+    let prop = lineClass[1] ? "bgClass" : "textClass"
+    if (output[prop] == null)
+      output[prop] = lineClass[2]
+    else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop]))
+      output[prop] += " " + lineClass[2]
+  }
+  return type
+}
+
+// Run the given mode's parser over a line, calling f for each token.
+function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
+  let flattenSpans = mode.flattenSpans
+  if (flattenSpans == null) flattenSpans = cm.options.flattenSpans
+  let curStart = 0, curStyle = null
+  let stream = new StringStream(text, cm.options.tabSize, context), style
+  let inner = cm.options.addModeClass && [null]
+  if (text == "") extractLineClasses(callBlankLine(mode, context.state), lineClasses)
+  while (!stream.eol()) {
+    if (stream.pos > cm.options.maxHighlightLength) {
+      flattenSpans = false
+      if (forceToEnd) processLine(cm, text, context, stream.pos)
+      stream.pos = text.length
+      style = null
+    } else {
+      style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
+    }
+    if (inner) {
+      let mName = inner[0].name
+      if (mName) style = "m-" + (style ? mName + " " + style : mName)
+    }
+    if (!flattenSpans || curStyle != style) {
+      while (curStart < stream.start) {
+        curStart = Math.min(stream.start, curStart + 5000)
+        f(curStart, curStyle)
+      }
+      curStyle = style
+    }
+    stream.start = stream.pos
+  }
+  while (curStart < stream.pos) {
+    // Webkit seems to refuse to render text nodes longer than 57444
+    // characters, and returns inaccurate measurements in nodes
+    // starting around 5000 chars.
+    let pos = Math.min(stream.pos, curStart + 5000)
+    f(pos, curStyle)
+    curStart = pos
+  }
+}
+
+// Finds the line to start with when starting a parse. Tries to
+// find a line with a stateAfter, so that it can start with a
+// valid state. If that fails, it returns the line with the
+// smallest indentation, which tends to need the least context to
+// parse correctly.
+function findStartLine(cm, n, precise) {
+  let minindent, minline, doc = cm.doc
+  let lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
+  for (let search = n; search > lim; --search) {
+    if (search <= doc.first) return doc.first
+    let line = getLine(doc, search - 1), after = line.stateAfter
+    if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
+      return search
+    let indented = countColumn(line.text, null, cm.options.tabSize)
+    if (minline == null || minindent > indented) {
+      minline = search - 1
+      minindent = indented
+    }
+  }
+  return minline
+}
+
+export function retreatFrontier(doc, n) {
+  doc.modeFrontier = Math.min(doc.modeFrontier, n)
+  if (doc.highlightFrontier < n - 10) return
+  let start = doc.first
+  for (let line = n - 1; line > start; line--) {
+    let saved = getLine(doc, line).stateAfter
+    // change is on 3
+    // state on line 1 looked ahead 2 -- so saw 3
+    // test 1 + 2 < 3 should cover this
+    if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) {
+      start = line + 1
+      break
+    }
+  }
+  doc.highlightFrontier = Math.min(doc.highlightFrontier, start)
+}