├── .npmignore ├── .gitignore ├── src ├── README.md └── highlight.ts ├── tsconfig.json ├── README.md ├── package.json ├── LICENSE └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/* 3 | .tern-* 4 | .rpt2_cache 5 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | This package provides a vocabulary for syntax-highlighting code based 2 | on a Lezer syntax tree. 3 | 4 | @Tag 5 | 6 | @tags 7 | 8 | @styleTags 9 | 10 | @getStyleTags 11 | 12 | @Highlighter 13 | 14 | @tagHighlighter 15 | 16 | @highlightCode 17 | 18 | @highlightTree 19 | 20 | @classHighlighter 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2017"], 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "strict": true, 7 | "target": "es2015", 8 | "module": "es2015", 9 | "newLine": "lf", 10 | "stripInternal": true, 11 | "moduleResolution": "node" 12 | }, 13 | "include": ["src/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @lezer/highlight 2 | 3 | [ [**WEBSITE**](http://lezer.codemirror.net) | [**ISSUES**](https://github.com/lezer-parser/lezer/issues) | [**FORUM**](https://discuss.codemirror.net/c/lezer) | [**CHANGELOG**](https://github.com/lezer-parser/highlight/blob/master/CHANGELOG.md) ] 4 | 5 | [Lezer](https://lezer.codemirror.net/) is an incremental parser system 6 | intended for use in an editor or similar system. 7 | 8 | @lezer/highlight provides a syntax highlighting framework for Lezer 9 | parse trees. 10 | 11 | Its programming interface is documented on [the 12 | website](https://lezer.codemirror.net/docs/ref/#highlight). 13 | 14 | This code is licensed under an MIT license. 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lezer/highlight", 3 | "version": "1.2.3", 4 | "description": "Highlighting system for Lezer parse trees", 5 | "main": "dist/index.cjs", 6 | "type": "module", 7 | "exports": { 8 | "import": "./dist/index.js", 9 | "require": "./dist/index.cjs" 10 | }, 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "author": "Marijn Haverbeke ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@marijn/buildtool": "0.1.3", 17 | "typescript": "^5.0.0" 18 | }, 19 | "dependencies": { 20 | "@lezer/common": "^1.3.0" 21 | }, 22 | "files": ["dist"], 23 | "repository": { 24 | "type" : "git", 25 | "url" : "https://github.com/lezer-parser/highlight.git" 26 | }, 27 | "scripts": { 28 | "watch": "node build.js --watch", 29 | "prepare": "node build.js" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2018 by Marijn Haverbeke and others 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.3 (2025-10-26) 2 | 3 | ### Bug fixes 4 | 5 | Fix a regression in 1.2.2 when assigning new highlight tags to nodes. 6 | 7 | ## 1.2.2 (2025-10-17) 8 | 9 | ### Bug fixes 10 | 11 | Fix an issue where adding additional highlighting info for a node that already had some rule would drop the old info. 12 | 13 | ## 1.2.1 (2024-08-13) 14 | 15 | ### Bug fixes 16 | 17 | Give `Tag` objects an optional string name for debugging, and use it in their `toString` method. 18 | 19 | ## 1.2.0 (2023-11-12) 20 | 21 | ### New features 22 | 23 | The new `highlightCode` function provides a higher-level interface for emitting highlighted code. 24 | 25 | ## 1.1.6 (2023-05-26) 26 | 27 | ### Bug fixes 28 | 29 | Fix a bug in that could cause it to add random highlighting to the text between parts of an overlaid tree. 30 | 31 | ## 1.1.5 (2023-05-24) 32 | 33 | ### Bug fixes 34 | 35 | Inheritable (`/...`) styles no longer apply across mounted subtrees. 36 | 37 | ## 1.1.4 (2023-03-24) 38 | 39 | ### Bug fixes 40 | 41 | Make sure TypeScript declaration file has the same name as the ES module to avoid some TypeScript resolution issues. 42 | 43 | ## 1.1.3 (2022-11-25) 44 | 45 | ### Bug fixes 46 | 47 | Fix a bug where the highlighting over overlaid ranges falling within a highlighted node in the parent tree was sometimes broken. 48 | 49 | ## 1.1.2 (2022-10-18) 50 | 51 | ### Bug fixes 52 | 53 | Fix an issue where unmodified tags were treated as having higher specificity than modified ones when computing precedence for tags with multiple modifiers. 54 | 55 | ## 1.1.1 (2022-09-23) 56 | 57 | ### Bug fixes 58 | 59 | Make sure `all` highlighting is applied even to nodes with no associated highlight tags. 60 | 61 | ## 1.1.0 (2022-09-20) 62 | 63 | ### New features 64 | 65 | The new `getStyleTags` function can be used to query the style tags that match a given syntax nodes. 66 | 67 | ## 1.0.0 (2022-06-06) 68 | 69 | ### New features 70 | 71 | First stable version. 72 | 73 | ## 0.16.0 (2022-04-20) 74 | 75 | ### Breaking changes 76 | 77 | First numbered release. 78 | -------------------------------------------------------------------------------- /src/highlight.ts: -------------------------------------------------------------------------------- 1 | import {Tree, NodeType, NodeProp, TreeCursor, SyntaxNodeRef} from "@lezer/common" 2 | 3 | let nextTagID = 0 4 | 5 | /// Highlighting tags are markers that denote a highlighting category. 6 | /// They are [associated](#highlight.styleTags) with parts of a syntax 7 | /// tree by a language mode, and then mapped to an actual CSS style by 8 | /// a [highlighter](#highlight.Highlighter). 9 | /// 10 | /// Because syntax tree node types and highlight styles have to be 11 | /// able to talk the same language, CodeMirror uses a mostly _closed_ 12 | /// [vocabulary](#highlight.tags) of syntax tags (as opposed to 13 | /// traditional open string-based systems, which make it hard for 14 | /// highlighting themes to cover all the tokens produced by the 15 | /// various languages). 16 | /// 17 | /// It _is_ possible to [define](#highlight.Tag^define) your own 18 | /// highlighting tags for system-internal use (where you control both 19 | /// the language package and the highlighter), but such tags will not 20 | /// be picked up by regular highlighters (though you can derive them 21 | /// from standard tags to allow highlighters to fall back to those). 22 | export class Tag { 23 | /// @internal 24 | id = nextTagID++ 25 | 26 | /// @internal 27 | constructor( 28 | /// The optional name of the base tag @internal 29 | readonly name: string, 30 | /// The set of this tag and all its parent tags, starting with 31 | /// this one itself and sorted in order of decreasing specificity. 32 | readonly set: Tag[], 33 | /// The base unmodified tag that this one is based on, if it's 34 | /// modified @internal 35 | readonly base: Tag | null, 36 | /// The modifiers applied to this.base @internal 37 | readonly modified: readonly Modifier[] 38 | ) {} 39 | 40 | toString() { 41 | let {name} = this 42 | for (let mod of this.modified) if (mod.name) name = `${mod.name}(${name})` 43 | return name 44 | } 45 | 46 | /// Define a new tag. If `parent` is given, the tag is treated as a 47 | /// sub-tag of that parent, and 48 | /// [highlighters](#highlight.tagHighlighter) that don't mention 49 | /// this tag will try to fall back to the parent tag (or grandparent 50 | /// tag, etc). 51 | static define(name?: string, parent?: Tag): Tag 52 | static define(parent?: Tag): Tag 53 | static define(nameOrParent?: string | Tag, parent?: Tag): Tag { 54 | let name = typeof nameOrParent == "string" ? nameOrParent : "?" 55 | if (nameOrParent instanceof Tag) parent = nameOrParent 56 | if (parent?.base) throw new Error("Can not derive from a modified tag") 57 | let tag = new Tag(name, [], null, []) 58 | tag.set.push(tag) 59 | if (parent) for (let t of parent.set) tag.set.push(t) 60 | return tag 61 | } 62 | 63 | /// Define a tag _modifier_, which is a function that, given a tag, 64 | /// will return a tag that is a subtag of the original. Applying the 65 | /// same modifier to a twice tag will return the same value (`m1(t1) 66 | /// == m1(t1)`) and applying multiple modifiers will, regardless or 67 | /// order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`). 68 | /// 69 | /// When multiple modifiers are applied to a given base tag, each 70 | /// smaller set of modifiers is registered as a parent, so that for 71 | /// example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`, 72 | /// `m1(m3(t1)`, and so on. 73 | static defineModifier(name?: string): (tag: Tag) => Tag { 74 | let mod = new Modifier(name) 75 | return (tag: Tag) => { 76 | if (tag.modified.indexOf(mod) > -1) return tag 77 | return Modifier.get(tag.base || tag, tag.modified.concat(mod).sort((a, b) => a.id - b.id)) 78 | } 79 | } 80 | } 81 | 82 | let nextModifierID = 0 83 | 84 | class Modifier { 85 | instances: Tag[] = [] 86 | id = nextModifierID++ 87 | 88 | constructor(readonly name?: string) {} 89 | 90 | static get(base: Tag, mods: readonly Modifier[]) { 91 | if (!mods.length) return base 92 | let exists = mods[0].instances.find(t => t.base == base && sameArray(mods, t.modified)) 93 | if (exists) return exists 94 | let set: Tag[] = [], tag = new Tag(base.name, set, base, mods) 95 | for (let m of mods) m.instances.push(tag) 96 | let configs = powerSet(mods) 97 | for (let parent of base.set) if (!parent.modified.length) for (let config of configs) 98 | set.push(Modifier.get(parent, config)) 99 | return tag 100 | } 101 | } 102 | 103 | function sameArray(a: readonly T[], b: readonly T[]) { 104 | return a.length == b.length && a.every((x, i) => x == b[i]) 105 | } 106 | 107 | function powerSet(array: readonly T[]): (readonly T[])[] { 108 | let sets: T[][] = [[]] 109 | for (let i = 0; i < array.length; i++) { 110 | for (let j = 0, e = sets.length; j < e; j++) { 111 | sets.push(sets[j].concat(array[i])) 112 | } 113 | } 114 | return sets.sort((a, b) => b.length - a.length) 115 | } 116 | 117 | /// This function is used to add a set of tags to a language syntax 118 | /// via [`NodeSet.extend`](#common.NodeSet.extend) or 119 | /// [`LRParser.configure`](#lr.LRParser.configure). 120 | /// 121 | /// The argument object maps node selectors to [highlighting 122 | /// tags](#highlight.Tag) or arrays of tags. 123 | /// 124 | /// Node selectors may hold one or more (space-separated) node paths. 125 | /// Such a path can be a [node name](#common.NodeType.name), or 126 | /// multiple node names (or `*` wildcards) separated by slash 127 | /// characters, as in `"Block/Declaration/VariableName"`. Such a path 128 | /// matches the final node but only if its direct parent nodes are the 129 | /// other nodes mentioned. A `*` in such a path matches any parent, 130 | /// but only a single level—wildcards that match multiple parents 131 | /// aren't supported, both for efficiency reasons and because Lezer 132 | /// trees make it rather hard to reason about what they would match.) 133 | /// 134 | /// A path can be ended with `/...` to indicate that the tag assigned 135 | /// to the node should also apply to all child nodes, even if they 136 | /// match their own style (by default, only the innermost style is 137 | /// used). 138 | /// 139 | /// When a path ends in `!`, as in `Attribute!`, no further matching 140 | /// happens for the node's child nodes, and the entire node gets the 141 | /// given style. 142 | /// 143 | /// In this notation, node names that contain `/`, `!`, `*`, or `...` 144 | /// must be quoted as JSON strings. 145 | /// 146 | /// For example: 147 | /// 148 | /// ```javascript 149 | /// parser.configure({props: [ 150 | /// styleTags({ 151 | /// // Style Number and BigNumber nodes 152 | /// "Number BigNumber": tags.number, 153 | /// // Style Escape nodes whose parent is String 154 | /// "String/Escape": tags.escape, 155 | /// // Style anything inside Attributes nodes 156 | /// "Attributes!": tags.meta, 157 | /// // Add a style to all content inside Italic nodes 158 | /// "Italic/...": tags.emphasis, 159 | /// // Style InvalidString nodes as both `string` and `invalid` 160 | /// "InvalidString": [tags.string, tags.invalid], 161 | /// // Style the node named "/" as punctuation 162 | /// '"/"': tags.punctuation 163 | /// }) 164 | /// ]}) 165 | /// ``` 166 | export function styleTags(spec: {[selector: string]: Tag | readonly Tag[]}) { 167 | let byName: {[name: string]: Rule} = Object.create(null) 168 | for (let prop in spec) { 169 | let tags = spec[prop] 170 | if (!Array.isArray(tags)) tags = [tags as Tag] 171 | for (let part of prop.split(" ")) if (part) { 172 | let pieces: string[] = [], mode = Mode.Normal, rest = part 173 | for (let pos = 0;;) { 174 | if (rest == "..." && pos > 0 && pos + 3 == part.length) { mode = Mode.Inherit; break } 175 | let m = /^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(rest) 176 | if (!m) throw new RangeError("Invalid path: " + part) 177 | pieces.push(m[0] == "*" ? "" : m[0][0] == '"' ? JSON.parse(m[0]) : m[0]) 178 | pos += m[0].length 179 | if (pos == part.length) break 180 | let next = part[pos++] 181 | if (pos == part.length && next == "!") { mode = Mode.Opaque; break } 182 | if (next != "/") throw new RangeError("Invalid path: " + part) 183 | rest = part.slice(pos) 184 | } 185 | let last = pieces.length - 1, inner = pieces[last] 186 | if (!inner) throw new RangeError("Invalid path: " + part) 187 | let rule = new Rule(tags, mode, last > 0 ? pieces.slice(0, last) : null) 188 | byName[inner] = rule.sort(byName[inner]) 189 | } 190 | } 191 | return ruleNodeProp.add(byName) 192 | } 193 | 194 | const ruleNodeProp = new NodeProp({ 195 | combine(a: Rule | undefined, b: Rule | undefined) { 196 | let cur: Rule | undefined, root: Rule, take: Rule 197 | while (a || b) { 198 | if (!a || b && a.depth >= b!.depth) { take = b!; b = b!.next } 199 | else { take = a!; a = a.next } 200 | if (cur && cur.mode == take.mode && !take.context && !cur.context) continue 201 | let copy = new Rule(take.tags, take.mode, take.context) 202 | if (cur) cur.next = copy 203 | else root = copy 204 | cur = copy 205 | } 206 | return root! 207 | } 208 | }) 209 | 210 | const enum Mode { Opaque, Inherit, Normal } 211 | 212 | class Rule { 213 | constructor(readonly tags: readonly Tag[], 214 | readonly mode: Mode, 215 | readonly context: readonly string[] | null, 216 | public next?: Rule) {} 217 | 218 | get opaque() { return this.mode == Mode.Opaque } 219 | get inherit() { return this.mode == Mode.Inherit } 220 | 221 | sort(other: Rule | undefined) { 222 | if (!other || other.depth < this.depth) { 223 | this.next = other 224 | return this 225 | } 226 | other.next = this.sort(other.next) 227 | return other 228 | } 229 | 230 | get depth() { return this.context ? this.context.length : 0 } 231 | 232 | static empty = new Rule([], Mode.Normal, null) 233 | } 234 | 235 | /// A highlighter defines a mapping from highlighting tags and 236 | /// language scopes to CSS class names. They are usually defined via 237 | /// [`tagHighlighter`](#highlight.tagHighlighter) or some wrapper 238 | /// around that, but it is also possible to implement them from 239 | /// scratch. 240 | export interface Highlighter { 241 | /// Get the set of classes that should be applied to the given set 242 | /// of highlighting tags, or null if this highlighter doesn't assign 243 | /// a style to the tags. 244 | style(tags: readonly Tag[]): string | null 245 | /// When given, the highlighter will only be applied to trees on 246 | /// whose [top](#common.NodeType.isTop) node this predicate returns 247 | /// true. 248 | scope?(node: NodeType): boolean 249 | } 250 | 251 | /// Define a [highlighter](#highlight.Highlighter) from an array of 252 | /// tag/class pairs. Classes associated with more specific tags will 253 | /// take precedence. 254 | export function tagHighlighter(tags: readonly {tag: Tag | readonly Tag[], class: string}[], options?: { 255 | /// By default, highlighters apply to the entire document. You can 256 | /// scope them to a single language by providing the tree's 257 | /// [top](#common.NodeType.isTop) node type here. 258 | scope?: (node: NodeType) => boolean, 259 | /// Add a style to _all_ tokens. Probably only useful in combination 260 | /// with `scope`. 261 | all?: string 262 | }): Highlighter { 263 | let map: {[tagID: number]: string | null} = Object.create(null) 264 | for (let style of tags) { 265 | if (!Array.isArray(style.tag)) map[(style.tag as Tag).id] = style.class 266 | else for (let tag of style.tag) map[tag.id] = style.class 267 | } 268 | let {scope, all = null} = options || {} 269 | return { 270 | style: (tags) => { 271 | let cls = all 272 | for (let tag of tags) { 273 | for (let sub of tag.set) { 274 | let tagClass = map[sub.id] 275 | if (tagClass) { 276 | cls = cls ? cls + " " + tagClass : tagClass 277 | break 278 | } 279 | } 280 | } 281 | return cls 282 | }, 283 | scope 284 | } 285 | } 286 | 287 | function highlightTags(highlighters: readonly Highlighter[], tags: readonly Tag[]): string | null { 288 | let result = null 289 | for (let highlighter of highlighters) { 290 | let value = highlighter.style(tags) 291 | if (value) result = result ? result + " " + value : value 292 | } 293 | return result 294 | } 295 | 296 | /// Highlight the given [tree](#common.Tree) with the given 297 | /// [highlighter](#highlight.Highlighter). Often, the higher-level 298 | /// [`highlightCode`](#highlight.highlightCode) function is easier to 299 | /// use. 300 | export function highlightTree( 301 | tree: Tree, 302 | highlighter: Highlighter | readonly Highlighter[], 303 | /// Assign styling to a region of the text. Will be called, in order 304 | /// of position, for any ranges where more than zero classes apply. 305 | /// `classes` is a space separated string of CSS classes. 306 | putStyle: (from: number, to: number, classes: string) => void, 307 | /// The start of the range to highlight. 308 | from = 0, 309 | /// The end of the range. 310 | to = tree.length, 311 | ) { 312 | let builder = new HighlightBuilder(from, Array.isArray(highlighter) ? highlighter : [highlighter], putStyle) 313 | builder.highlightRange(tree.cursor(), from, to, "", builder.highlighters) 314 | builder.flush(to) 315 | } 316 | 317 | /// Highlight the given tree with the given highlighter, calling 318 | /// `putText` for every piece of text, either with a set of classes or 319 | /// with the empty string when unstyled, and `putBreak` for every line 320 | /// break. 321 | export function highlightCode(code: string, tree: Tree, highlighter: Highlighter | readonly Highlighter[], 322 | putText: (code: string, classes: string) => void, 323 | putBreak: () => void, 324 | from = 0, to = code.length) { 325 | let pos = from 326 | function writeTo(p: number, classes: string) { 327 | if (p <= pos) return 328 | for (let text = code.slice(pos, p), i = 0;;) { 329 | let nextBreak = text.indexOf("\n", i) 330 | let upto = nextBreak < 0 ? text.length : nextBreak 331 | if (upto > i) putText(text.slice(i, upto), classes) 332 | if (nextBreak < 0) break 333 | putBreak() 334 | i = nextBreak + 1 335 | } 336 | pos = p 337 | } 338 | 339 | highlightTree(tree, highlighter, (from, to, classes) => { 340 | writeTo(from, "") 341 | writeTo(to, classes) 342 | }, from, to) 343 | writeTo(to, "") 344 | } 345 | 346 | class HighlightBuilder { 347 | class = "" 348 | constructor( 349 | public at: number, 350 | readonly highlighters: readonly Highlighter[], 351 | readonly span: (from: number, to: number, cls: string) => void 352 | ) {} 353 | 354 | startSpan(at: number, cls: string) { 355 | if (cls != this.class) { 356 | this.flush(at) 357 | if (at > this.at) this.at = at 358 | this.class = cls 359 | } 360 | } 361 | 362 | flush(to: number) { 363 | if (to > this.at && this.class) this.span(this.at, to, this.class) 364 | } 365 | 366 | highlightRange(cursor: TreeCursor, from: number, to: number, inheritedClass: string, highlighters: readonly Highlighter[]) { 367 | let {type, from: start, to: end} = cursor 368 | if (start >= to || end <= from) return 369 | if (type.isTop) highlighters = this.highlighters.filter(h => !h.scope || h.scope(type)) 370 | 371 | let cls = inheritedClass 372 | let rule = getStyleTags(cursor) as Rule || Rule.empty 373 | let tagCls = highlightTags(highlighters, rule.tags) 374 | if (tagCls) { 375 | if (cls) cls += " " 376 | cls += tagCls 377 | if (rule.mode == Mode.Inherit) inheritedClass += (inheritedClass ? " " : "") + tagCls 378 | } 379 | 380 | this.startSpan(Math.max(from, start), cls) 381 | if (rule.opaque) return 382 | 383 | let mounted = cursor.tree && cursor.tree.prop(NodeProp.mounted) 384 | if (mounted && mounted.overlay) { 385 | let inner = cursor.node.enter(mounted.overlay[0].from + start, 1)! 386 | let innerHighlighters = this.highlighters.filter(h => !h.scope || h.scope(mounted!.tree.type)) 387 | let hasChild = cursor.firstChild() 388 | for (let i = 0, pos = start;; i++) { 389 | let next = i < mounted.overlay.length ? mounted.overlay[i] : null 390 | let nextPos = next ? next.from + start : end 391 | let rangeFrom = Math.max(from, pos), rangeTo = Math.min(to, nextPos) 392 | if (rangeFrom < rangeTo && hasChild) { 393 | while (cursor.from < rangeTo) { 394 | this.highlightRange(cursor, rangeFrom, rangeTo, inheritedClass, highlighters) 395 | this.startSpan(Math.min(rangeTo, cursor.to), cls) 396 | if (cursor.to >= nextPos || !cursor.nextSibling()) break 397 | } 398 | } 399 | if (!next || nextPos > to) break 400 | pos = next.to + start 401 | if (pos > from) { 402 | this.highlightRange(inner.cursor(), Math.max(from, next.from + start), Math.min(to, pos), 403 | "", innerHighlighters) 404 | this.startSpan(Math.min(to, pos), cls) 405 | } 406 | } 407 | if (hasChild) cursor.parent() 408 | } else if (cursor.firstChild()) { 409 | if (mounted) inheritedClass = "" 410 | do { 411 | if (cursor.to <= from) continue 412 | if (cursor.from >= to) break 413 | this.highlightRange(cursor, from, to, inheritedClass, highlighters) 414 | this.startSpan(Math.min(to, cursor.to), cls) 415 | } while (cursor.nextSibling()) 416 | cursor.parent() 417 | } 418 | } 419 | } 420 | 421 | /// Match a syntax node's [highlight rules](#highlight.styleTags). If 422 | /// there's a match, return its set of tags, and whether it is 423 | /// opaque (uses a `!`) or applies to all child nodes (`/...`). 424 | export function getStyleTags(node: SyntaxNodeRef): {tags: readonly Tag[], opaque: boolean, inherit: boolean} | null { 425 | let rule = node.type.prop(ruleNodeProp) 426 | while (rule && rule.context && !node.matchContext(rule.context)) rule = rule.next 427 | return rule || null 428 | } 429 | 430 | const t = Tag.define 431 | 432 | const comment = t(), name = t(), typeName = t(name), propertyName = t(name), 433 | literal = t(), string = t(literal), number = t(literal), 434 | content = t(), heading = t(content), keyword = t(), operator = t(), 435 | punctuation = t(), bracket = t(punctuation), meta = t() 436 | 437 | /// The default set of highlighting [tags](#highlight.Tag). 438 | /// 439 | /// This collection is heavily biased towards programming languages, 440 | /// and necessarily incomplete. A full ontology of syntactic 441 | /// constructs would fill a stack of books, and be impractical to 442 | /// write themes for. So try to make do with this set. If all else 443 | /// fails, [open an 444 | /// issue](https://github.com/codemirror/codemirror.next) to propose a 445 | /// new tag, or [define](#highlight.Tag^define) a local custom tag for 446 | /// your use case. 447 | /// 448 | /// Note that it is not obligatory to always attach the most specific 449 | /// tag possible to an element—if your grammar can't easily 450 | /// distinguish a certain type of element (such as a local variable), 451 | /// it is okay to style it as its more general variant (a variable). 452 | /// 453 | /// For tags that extend some parent tag, the documentation links to 454 | /// the parent. 455 | export const tags = { 456 | /// A comment. 457 | comment, 458 | /// A line [comment](#highlight.tags.comment). 459 | lineComment: t(comment), 460 | /// A block [comment](#highlight.tags.comment). 461 | blockComment: t(comment), 462 | /// A documentation [comment](#highlight.tags.comment). 463 | docComment: t(comment), 464 | 465 | /// Any kind of identifier. 466 | name, 467 | /// The [name](#highlight.tags.name) of a variable. 468 | variableName: t(name), 469 | /// A type [name](#highlight.tags.name). 470 | typeName: typeName, 471 | /// A tag name (subtag of [`typeName`](#highlight.tags.typeName)). 472 | tagName: t(typeName), 473 | /// A property or field [name](#highlight.tags.name). 474 | propertyName: propertyName, 475 | /// An attribute name (subtag of [`propertyName`](#highlight.tags.propertyName)). 476 | attributeName: t(propertyName), 477 | /// The [name](#highlight.tags.name) of a class. 478 | className: t(name), 479 | /// A label [name](#highlight.tags.name). 480 | labelName: t(name), 481 | /// A namespace [name](#highlight.tags.name). 482 | namespace: t(name), 483 | /// The [name](#highlight.tags.name) of a macro. 484 | macroName: t(name), 485 | 486 | /// A literal value. 487 | literal, 488 | /// A string [literal](#highlight.tags.literal). 489 | string, 490 | /// A documentation [string](#highlight.tags.string). 491 | docString: t(string), 492 | /// A character literal (subtag of [string](#highlight.tags.string)). 493 | character: t(string), 494 | /// An attribute value (subtag of [string](#highlight.tags.string)). 495 | attributeValue: t(string), 496 | /// A number [literal](#highlight.tags.literal). 497 | number, 498 | /// An integer [number](#highlight.tags.number) literal. 499 | integer: t(number), 500 | /// A floating-point [number](#highlight.tags.number) literal. 501 | float: t(number), 502 | /// A boolean [literal](#highlight.tags.literal). 503 | bool: t(literal), 504 | /// Regular expression [literal](#highlight.tags.literal). 505 | regexp: t(literal), 506 | /// An escape [literal](#highlight.tags.literal), for example a 507 | /// backslash escape in a string. 508 | escape: t(literal), 509 | /// A color [literal](#highlight.tags.literal). 510 | color: t(literal), 511 | /// A URL [literal](#highlight.tags.literal). 512 | url: t(literal), 513 | 514 | /// A language keyword. 515 | keyword, 516 | /// The [keyword](#highlight.tags.keyword) for the self or this 517 | /// object. 518 | self: t(keyword), 519 | /// The [keyword](#highlight.tags.keyword) for null. 520 | null: t(keyword), 521 | /// A [keyword](#highlight.tags.keyword) denoting some atomic value. 522 | atom: t(keyword), 523 | /// A [keyword](#highlight.tags.keyword) that represents a unit. 524 | unit: t(keyword), 525 | /// A modifier [keyword](#highlight.tags.keyword). 526 | modifier: t(keyword), 527 | /// A [keyword](#highlight.tags.keyword) that acts as an operator. 528 | operatorKeyword: t(keyword), 529 | /// A control-flow related [keyword](#highlight.tags.keyword). 530 | controlKeyword: t(keyword), 531 | /// A [keyword](#highlight.tags.keyword) that defines something. 532 | definitionKeyword: t(keyword), 533 | /// A [keyword](#highlight.tags.keyword) related to defining or 534 | /// interfacing with modules. 535 | moduleKeyword: t(keyword), 536 | 537 | /// An operator. 538 | operator, 539 | /// An [operator](#highlight.tags.operator) that dereferences something. 540 | derefOperator: t(operator), 541 | /// Arithmetic-related [operator](#highlight.tags.operator). 542 | arithmeticOperator: t(operator), 543 | /// Logical [operator](#highlight.tags.operator). 544 | logicOperator: t(operator), 545 | /// Bit [operator](#highlight.tags.operator). 546 | bitwiseOperator: t(operator), 547 | /// Comparison [operator](#highlight.tags.operator). 548 | compareOperator: t(operator), 549 | /// [Operator](#highlight.tags.operator) that updates its operand. 550 | updateOperator: t(operator), 551 | /// [Operator](#highlight.tags.operator) that defines something. 552 | definitionOperator: t(operator), 553 | /// Type-related [operator](#highlight.tags.operator). 554 | typeOperator: t(operator), 555 | /// Control-flow [operator](#highlight.tags.operator). 556 | controlOperator: t(operator), 557 | 558 | /// Program or markup punctuation. 559 | punctuation, 560 | /// [Punctuation](#highlight.tags.punctuation) that separates 561 | /// things. 562 | separator: t(punctuation), 563 | /// Bracket-style [punctuation](#highlight.tags.punctuation). 564 | bracket, 565 | /// Angle [brackets](#highlight.tags.bracket) (usually `<` and `>` 566 | /// tokens). 567 | angleBracket: t(bracket), 568 | /// Square [brackets](#highlight.tags.bracket) (usually `[` and `]` 569 | /// tokens). 570 | squareBracket: t(bracket), 571 | /// Parentheses (usually `(` and `)` tokens). Subtag of 572 | /// [bracket](#highlight.tags.bracket). 573 | paren: t(bracket), 574 | /// Braces (usually `{` and `}` tokens). Subtag of 575 | /// [bracket](#highlight.tags.bracket). 576 | brace: t(bracket), 577 | 578 | /// Content, for example plain text in XML or markup documents. 579 | content, 580 | /// [Content](#highlight.tags.content) that represents a heading. 581 | heading, 582 | /// A level 1 [heading](#highlight.tags.heading). 583 | heading1: t(heading), 584 | /// A level 2 [heading](#highlight.tags.heading). 585 | heading2: t(heading), 586 | /// A level 3 [heading](#highlight.tags.heading). 587 | heading3: t(heading), 588 | /// A level 4 [heading](#highlight.tags.heading). 589 | heading4: t(heading), 590 | /// A level 5 [heading](#highlight.tags.heading). 591 | heading5: t(heading), 592 | /// A level 6 [heading](#highlight.tags.heading). 593 | heading6: t(heading), 594 | /// A prose [content](#highlight.tags.content) separator (such as a horizontal rule). 595 | contentSeparator: t(content), 596 | /// [Content](#highlight.tags.content) that represents a list. 597 | list: t(content), 598 | /// [Content](#highlight.tags.content) that represents a quote. 599 | quote: t(content), 600 | /// [Content](#highlight.tags.content) that is emphasized. 601 | emphasis: t(content), 602 | /// [Content](#highlight.tags.content) that is styled strong. 603 | strong: t(content), 604 | /// [Content](#highlight.tags.content) that is part of a link. 605 | link: t(content), 606 | /// [Content](#highlight.tags.content) that is styled as code or 607 | /// monospace. 608 | monospace: t(content), 609 | /// [Content](#highlight.tags.content) that has a strike-through 610 | /// style. 611 | strikethrough: t(content), 612 | 613 | /// Inserted text in a change-tracking format. 614 | inserted: t(), 615 | /// Deleted text. 616 | deleted: t(), 617 | /// Changed text. 618 | changed: t(), 619 | 620 | /// An invalid or unsyntactic element. 621 | invalid: t(), 622 | 623 | /// Metadata or meta-instruction. 624 | meta, 625 | /// [Metadata](#highlight.tags.meta) that applies to the entire 626 | /// document. 627 | documentMeta: t(meta), 628 | /// [Metadata](#highlight.tags.meta) that annotates or adds 629 | /// attributes to a given syntactic element. 630 | annotation: t(meta), 631 | /// Processing instruction or preprocessor directive. Subtag of 632 | /// [meta](#highlight.tags.meta). 633 | processingInstruction: t(meta), 634 | 635 | /// [Modifier](#highlight.Tag^defineModifier) that indicates that a 636 | /// given element is being defined. Expected to be used with the 637 | /// various [name](#highlight.tags.name) tags. 638 | definition: Tag.defineModifier("definition"), 639 | /// [Modifier](#highlight.Tag^defineModifier) that indicates that 640 | /// something is constant. Mostly expected to be used with 641 | /// [variable names](#highlight.tags.variableName). 642 | constant: Tag.defineModifier("constant"), 643 | /// [Modifier](#highlight.Tag^defineModifier) used to indicate that 644 | /// a [variable](#highlight.tags.variableName) or [property 645 | /// name](#highlight.tags.propertyName) is being called or defined 646 | /// as a function. 647 | function: Tag.defineModifier("function"), 648 | /// [Modifier](#highlight.Tag^defineModifier) that can be applied to 649 | /// [names](#highlight.tags.name) to indicate that they belong to 650 | /// the language's standard environment. 651 | standard: Tag.defineModifier("standard"), 652 | /// [Modifier](#highlight.Tag^defineModifier) that indicates a given 653 | /// [names](#highlight.tags.name) is local to some scope. 654 | local: Tag.defineModifier("local"), 655 | 656 | /// A generic variant [modifier](#highlight.Tag^defineModifier) that 657 | /// can be used to tag language-specific alternative variants of 658 | /// some common tag. It is recommended for themes to define special 659 | /// forms of at least the [string](#highlight.tags.string) and 660 | /// [variable name](#highlight.tags.variableName) tags, since those 661 | /// come up a lot. 662 | special: Tag.defineModifier("special") 663 | } 664 | 665 | for (let name in tags) { 666 | let val = (tags as any)[name] 667 | if (val instanceof Tag) (val as any).name = name 668 | } 669 | 670 | /// This is a highlighter that adds stable, predictable classes to 671 | /// tokens, for styling with external CSS. 672 | /// 673 | /// The following tags are mapped to their name prefixed with `"tok-"` 674 | /// (for example `"tok-comment"`): 675 | /// 676 | /// * [`link`](#highlight.tags.link) 677 | /// * [`heading`](#highlight.tags.heading) 678 | /// * [`emphasis`](#highlight.tags.emphasis) 679 | /// * [`strong`](#highlight.tags.strong) 680 | /// * [`keyword`](#highlight.tags.keyword) 681 | /// * [`atom`](#highlight.tags.atom) 682 | /// * [`bool`](#highlight.tags.bool) 683 | /// * [`url`](#highlight.tags.url) 684 | /// * [`labelName`](#highlight.tags.labelName) 685 | /// * [`inserted`](#highlight.tags.inserted) 686 | /// * [`deleted`](#highlight.tags.deleted) 687 | /// * [`literal`](#highlight.tags.literal) 688 | /// * [`string`](#highlight.tags.string) 689 | /// * [`number`](#highlight.tags.number) 690 | /// * [`variableName`](#highlight.tags.variableName) 691 | /// * [`typeName`](#highlight.tags.typeName) 692 | /// * [`namespace`](#highlight.tags.namespace) 693 | /// * [`className`](#highlight.tags.className) 694 | /// * [`macroName`](#highlight.tags.macroName) 695 | /// * [`propertyName`](#highlight.tags.propertyName) 696 | /// * [`operator`](#highlight.tags.operator) 697 | /// * [`comment`](#highlight.tags.comment) 698 | /// * [`meta`](#highlight.tags.meta) 699 | /// * [`punctuation`](#highlight.tags.punctuation) 700 | /// * [`invalid`](#highlight.tags.invalid) 701 | /// 702 | /// In addition, these mappings are provided: 703 | /// 704 | /// * [`regexp`](#highlight.tags.regexp), 705 | /// [`escape`](#highlight.tags.escape), and 706 | /// [`special`](#highlight.tags.special)[`(string)`](#highlight.tags.string) 707 | /// are mapped to `"tok-string2"` 708 | /// * [`special`](#highlight.tags.special)[`(variableName)`](#highlight.tags.variableName) 709 | /// to `"tok-variableName2"` 710 | /// * [`local`](#highlight.tags.local)[`(variableName)`](#highlight.tags.variableName) 711 | /// to `"tok-variableName tok-local"` 712 | /// * [`definition`](#highlight.tags.definition)[`(variableName)`](#highlight.tags.variableName) 713 | /// to `"tok-variableName tok-definition"` 714 | /// * [`definition`](#highlight.tags.definition)[`(propertyName)`](#highlight.tags.propertyName) 715 | /// to `"tok-propertyName tok-definition"` 716 | export const classHighlighter = tagHighlighter([ 717 | {tag: tags.link, class: "tok-link"}, 718 | {tag: tags.heading, class: "tok-heading"}, 719 | {tag: tags.emphasis, class: "tok-emphasis"}, 720 | {tag: tags.strong, class: "tok-strong"}, 721 | {tag: tags.keyword, class: "tok-keyword"}, 722 | {tag: tags.atom, class: "tok-atom"}, 723 | {tag: tags.bool, class: "tok-bool"}, 724 | {tag: tags.url, class: "tok-url"}, 725 | {tag: tags.labelName, class: "tok-labelName"}, 726 | {tag: tags.inserted, class: "tok-inserted"}, 727 | {tag: tags.deleted, class: "tok-deleted"}, 728 | {tag: tags.literal, class: "tok-literal"}, 729 | {tag: tags.string, class: "tok-string"}, 730 | {tag: tags.number, class: "tok-number"}, 731 | {tag: [tags.regexp, tags.escape, tags.special(tags.string)], class: "tok-string2"}, 732 | {tag: tags.variableName, class: "tok-variableName"}, 733 | {tag: tags.local(tags.variableName), class: "tok-variableName tok-local"}, 734 | {tag: tags.definition(tags.variableName), class: "tok-variableName tok-definition"}, 735 | {tag: tags.special(tags.variableName), class: "tok-variableName2"}, 736 | {tag: tags.definition(tags.propertyName), class: "tok-propertyName tok-definition"}, 737 | {tag: tags.typeName, class: "tok-typeName"}, 738 | {tag: tags.namespace, class: "tok-namespace"}, 739 | {tag: tags.className, class: "tok-className"}, 740 | {tag: tags.macroName, class: "tok-macroName"}, 741 | {tag: tags.propertyName, class: "tok-propertyName"}, 742 | {tag: tags.operator, class: "tok-operator"}, 743 | {tag: tags.comment, class: "tok-comment"}, 744 | {tag: tags.meta, class: "tok-meta"}, 745 | {tag: tags.invalid, class: "tok-invalid"}, 746 | {tag: tags.punctuation, class: "tok-punctuation"} 747 | ]) 748 | --------------------------------------------------------------------------------