8ebdf64a0882d09636e1dd0781fb49766df9700e0b99084e084c44033c6ceee2941f0c4b4fcb2569ae87f5e7fc4bb940329d072e65f8fa157d793b91c41754 14 KB

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