openrat-cms

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

line_data.js (13670B)


      1 import { getOrder } from "../util/bidi.js"
      2 import { ie, ie_version, webkit } from "../util/browser.js"
      3 import { elt, eltP, joinClasses } from "../util/dom.js"
      4 import { eventMixin, signal } from "../util/event.js"
      5 import { hasBadBidiRects, zeroWidthElement } from "../util/feature_detection.js"
      6 import { lst, spaceStr } from "../util/misc.js"
      7 
      8 import { getLineStyles } from "./highlight.js"
      9 import { attachMarkedSpans, compareCollapsedMarkers, detachMarkedSpans, lineIsHidden, visualLineContinued } from "./spans.js"
     10 import { getLine, lineNo, updateLineHeight } from "./utils_line.js"
     11 
     12 // LINE DATA STRUCTURE
     13 
     14 // Line objects. These hold state related to a line, including
     15 // highlighting info (the styles array).
     16 export class Line {
     17   constructor(text, markedSpans, estimateHeight) {
     18     this.text = text
     19     attachMarkedSpans(this, markedSpans)
     20     this.height = estimateHeight ? estimateHeight(this) : 1
     21   }
     22 
     23   lineNo() { return lineNo(this) }
     24 }
     25 eventMixin(Line)
     26 
     27 // Change the content (text, markers) of a line. Automatically
     28 // invalidates cached information and tries to re-estimate the
     29 // line's height.
     30 export function updateLine(line, text, markedSpans, estimateHeight) {
     31   line.text = text
     32   if (line.stateAfter) line.stateAfter = null
     33   if (line.styles) line.styles = null
     34   if (line.order != null) line.order = null
     35   detachMarkedSpans(line)
     36   attachMarkedSpans(line, markedSpans)
     37   let estHeight = estimateHeight ? estimateHeight(line) : 1
     38   if (estHeight != line.height) updateLineHeight(line, estHeight)
     39 }
     40 
     41 // Detach a line from the document tree and its markers.
     42 export function cleanUpLine(line) {
     43   line.parent = null
     44   detachMarkedSpans(line)
     45 }
     46 
     47 // Convert a style as returned by a mode (either null, or a string
     48 // containing one or more styles) to a CSS style. This is cached,
     49 // and also looks for line-wide styles.
     50 let styleToClassCache = {}, styleToClassCacheWithMode = {}
     51 function interpretTokenStyle(style, options) {
     52   if (!style || /^\s*$/.test(style)) return null
     53   let cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache
     54   return cache[style] ||
     55     (cache[style] = style.replace(/\S+/g, "cm-$&"))
     56 }
     57 
     58 // Render the DOM representation of the text of a line. Also builds
     59 // up a 'line map', which points at the DOM nodes that represent
     60 // specific stretches of text, and is used by the measuring code.
     61 // The returned object contains the DOM node, this map, and
     62 // information about line-wide styles that were set by the mode.
     63 export function buildLineContent(cm, lineView) {
     64   // The padding-right forces the element to have a 'border', which
     65   // is needed on Webkit to be able to get line-level bounding
     66   // rectangles for it (in measureChar).
     67   let content = eltP("span", null, null, webkit ? "padding-right: .1px" : null)
     68   let builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content,
     69                  col: 0, pos: 0, cm: cm,
     70                  trailingSpace: false,
     71                  splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}
     72   lineView.measure = {}
     73 
     74   // Iterate over the logical lines that make up this visual line.
     75   for (let i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {
     76     let line = i ? lineView.rest[i - 1] : lineView.line, order
     77     builder.pos = 0
     78     builder.addToken = buildToken
     79     // Optionally wire in some hacks into the token-rendering
     80     // algorithm, to deal with browser quirks.
     81     if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction)))
     82       builder.addToken = buildTokenBadBidi(builder.addToken, order)
     83     builder.map = []
     84     let allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line)
     85     insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate))
     86     if (line.styleClasses) {
     87       if (line.styleClasses.bgClass)
     88         builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "")
     89       if (line.styleClasses.textClass)
     90         builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "")
     91     }
     92 
     93     // Ensure at least a single node is present, for measuring.
     94     if (builder.map.length == 0)
     95       builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure)))
     96 
     97     // Store the map and a cache object for the current logical line
     98     if (i == 0) {
     99       lineView.measure.map = builder.map
    100       lineView.measure.cache = {}
    101     } else {
    102       ;(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map)
    103       ;(lineView.measure.caches || (lineView.measure.caches = [])).push({})
    104     }
    105   }
    106 
    107   // See issue #2901
    108   if (webkit) {
    109     let last = builder.content.lastChild
    110     if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
    111       builder.content.className = "cm-tab-wrap-hack"
    112   }
    113 
    114   signal(cm, "renderLine", cm, lineView.line, builder.pre)
    115   if (builder.pre.className)
    116     builder.textClass = joinClasses(builder.pre.className, builder.textClass || "")
    117 
    118   return builder
    119 }
    120 
    121 export function defaultSpecialCharPlaceholder(ch) {
    122   let token = elt("span", "\u2022", "cm-invalidchar")
    123   token.title = "\\u" + ch.charCodeAt(0).toString(16)
    124   token.setAttribute("aria-label", token.title)
    125   return token
    126 }
    127 
    128 // Build up the DOM representation for a single token, and add it to
    129 // the line map. Takes care to render special characters separately.
    130 function buildToken(builder, text, style, startStyle, endStyle, title, css) {
    131   if (!text) return
    132   let displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text
    133   let special = builder.cm.state.specialChars, mustWrap = false
    134   let content
    135   if (!special.test(text)) {
    136     builder.col += text.length
    137     content = document.createTextNode(displayText)
    138     builder.map.push(builder.pos, builder.pos + text.length, content)
    139     if (ie && ie_version < 9) mustWrap = true
    140     builder.pos += text.length
    141   } else {
    142     content = document.createDocumentFragment()
    143     let pos = 0
    144     while (true) {
    145       special.lastIndex = pos
    146       let m = special.exec(text)
    147       let skipped = m ? m.index - pos : text.length - pos
    148       if (skipped) {
    149         let txt = document.createTextNode(displayText.slice(pos, pos + skipped))
    150         if (ie && ie_version < 9) content.appendChild(elt("span", [txt]))
    151         else content.appendChild(txt)
    152         builder.map.push(builder.pos, builder.pos + skipped, txt)
    153         builder.col += skipped
    154         builder.pos += skipped
    155       }
    156       if (!m) break
    157       pos += skipped + 1
    158       let txt
    159       if (m[0] == "\t") {
    160         let tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize
    161         txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"))
    162         txt.setAttribute("role", "presentation")
    163         txt.setAttribute("cm-text", "\t")
    164         builder.col += tabWidth
    165       } else if (m[0] == "\r" || m[0] == "\n") {
    166         txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"))
    167         txt.setAttribute("cm-text", m[0])
    168         builder.col += 1
    169       } else {
    170         txt = builder.cm.options.specialCharPlaceholder(m[0])
    171         txt.setAttribute("cm-text", m[0])
    172         if (ie && ie_version < 9) content.appendChild(elt("span", [txt]))
    173         else content.appendChild(txt)
    174         builder.col += 1
    175       }
    176       builder.map.push(builder.pos, builder.pos + 1, txt)
    177       builder.pos++
    178     }
    179   }
    180   builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32
    181   if (style || startStyle || endStyle || mustWrap || css) {
    182     let fullStyle = style || ""
    183     if (startStyle) fullStyle += startStyle
    184     if (endStyle) fullStyle += endStyle
    185     let token = elt("span", [content], fullStyle, css)
    186     if (title) token.title = title
    187     return builder.content.appendChild(token)
    188   }
    189   builder.content.appendChild(content)
    190 }
    191 
    192 function splitSpaces(text, trailingBefore) {
    193   if (text.length > 1 && !/  /.test(text)) return text
    194   let spaceBefore = trailingBefore, result = ""
    195   for (let i = 0; i < text.length; i++) {
    196     let ch = text.charAt(i)
    197     if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32))
    198       ch = "\u00a0"
    199     result += ch
    200     spaceBefore = ch == " "
    201   }
    202   return result
    203 }
    204 
    205 // Work around nonsense dimensions being reported for stretches of
    206 // right-to-left text.
    207 function buildTokenBadBidi(inner, order) {
    208   return (builder, text, style, startStyle, endStyle, title, css) => {
    209     style = style ? style + " cm-force-border" : "cm-force-border"
    210     let start = builder.pos, end = start + text.length
    211     for (;;) {
    212       // Find the part that overlaps with the start of this text
    213       let part
    214       for (let i = 0; i < order.length; i++) {
    215         part = order[i]
    216         if (part.to > start && part.from <= start) break
    217       }
    218       if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css)
    219       inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css)
    220       startStyle = null
    221       text = text.slice(part.to - start)
    222       start = part.to
    223     }
    224   }
    225 }
    226 
    227 function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
    228   let widget = !ignoreWidget && marker.widgetNode
    229   if (widget) builder.map.push(builder.pos, builder.pos + size, widget)
    230   if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
    231     if (!widget)
    232       widget = builder.content.appendChild(document.createElement("span"))
    233     widget.setAttribute("cm-marker", marker.id)
    234   }
    235   if (widget) {
    236     builder.cm.display.input.setUneditable(widget)
    237     builder.content.appendChild(widget)
    238   }
    239   builder.pos += size
    240   builder.trailingSpace = false
    241 }
    242 
    243 // Outputs a number of spans to make up a line, taking highlighting
    244 // and marked text into account.
    245 function insertLineContent(line, builder, styles) {
    246   let spans = line.markedSpans, allText = line.text, at = 0
    247   if (!spans) {
    248     for (let i = 1; i < styles.length; i+=2)
    249       builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options))
    250     return
    251   }
    252 
    253   let len = allText.length, pos = 0, i = 1, text = "", style, css
    254   let nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed
    255   for (;;) {
    256     if (nextChange == pos) { // Update current marker set
    257       spanStyle = spanEndStyle = spanStartStyle = title = css = ""
    258       collapsed = null; nextChange = Infinity
    259       let foundBookmarks = [], endStyles
    260       for (let j = 0; j < spans.length; ++j) {
    261         let sp = spans[j], m = sp.marker
    262         if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
    263           foundBookmarks.push(m)
    264         } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
    265           if (sp.to != null && sp.to != pos && nextChange > sp.to) {
    266             nextChange = sp.to
    267             spanEndStyle = ""
    268           }
    269           if (m.className) spanStyle += " " + m.className
    270           if (m.css) css = (css ? css + ";" : "") + m.css
    271           if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle
    272           if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to)
    273           if (m.title && !title) title = m.title
    274           if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
    275             collapsed = sp
    276         } else if (sp.from > pos && nextChange > sp.from) {
    277           nextChange = sp.from
    278         }
    279       }
    280       if (endStyles) for (let j = 0; j < endStyles.length; j += 2)
    281         if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j]
    282 
    283       if (!collapsed || collapsed.from == pos) for (let j = 0; j < foundBookmarks.length; ++j)
    284         buildCollapsedSpan(builder, 0, foundBookmarks[j])
    285       if (collapsed && (collapsed.from || 0) == pos) {
    286         buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
    287                            collapsed.marker, collapsed.from == null)
    288         if (collapsed.to == null) return
    289         if (collapsed.to == pos) collapsed = false
    290       }
    291     }
    292     if (pos >= len) break
    293 
    294     let upto = Math.min(len, nextChange)
    295     while (true) {
    296       if (text) {
    297         let end = pos + text.length
    298         if (!collapsed) {
    299           let tokenText = end > upto ? text.slice(0, upto - pos) : text
    300           builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
    301                            spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css)
    302         }
    303         if (end >= upto) {text = text.slice(upto - pos); pos = upto; break}
    304         pos = end
    305         spanStartStyle = ""
    306       }
    307       text = allText.slice(at, at = styles[i++])
    308       style = interpretTokenStyle(styles[i++], builder.cm.options)
    309     }
    310   }
    311 }
    312 
    313 
    314 // These objects are used to represent the visible (currently drawn)
    315 // part of the document. A LineView may correspond to multiple
    316 // logical lines, if those are connected by collapsed ranges.
    317 export function LineView(doc, line, lineN) {
    318   // The starting line
    319   this.line = line
    320   // Continuing lines, if any
    321   this.rest = visualLineContinued(line)
    322   // Number of logical lines in this visual line
    323   this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1
    324   this.node = this.text = null
    325   this.hidden = lineIsHidden(doc, line)
    326 }
    327 
    328 // Create a range of LineView objects for the given lines.
    329 export function buildViewArray(cm, from, to) {
    330   let array = [], nextPos
    331   for (let pos = from; pos < to; pos = nextPos) {
    332     let view = new LineView(cm.doc, getLine(cm.doc, pos), pos)
    333     nextPos = pos + view.size
    334     array.push(view)
    335   }
    336   return array
    337 }