import {Tree, SyntaxNode, ChangedRange, TreeFragment, NodeProp, NodeType, Input, PartialParse, Parser, IterMode} from "@lezer/common" import type {LRParser, ParserConfig} from "@lezer/lr" import {EditorState, StateField, Transaction, Extension, StateEffect, Facet, ChangeDesc, Text, TextIterator} from "@codemirror/state" import {ViewPlugin, ViewUpdate, EditorView, logException} from "@codemirror/view" /// Node prop stored in a parser's top syntax node to provide the /// facet that stores language-specific data for that language. export const languageDataProp = new NodeProp>() /// Helper function to define a facet (to be added to the top syntax /// node(s) for a language via /// [`languageDataProp`](#language.languageDataProp)), that will be /// used to associate language data with the language. You /// probably only need this when subclassing /// [`Language`](#language.Language). export function defineLanguageFacet(baseData?: {[name: string]: any}) { return Facet.define<{[name: string]: any}>({ combine: baseData ? values => values.concat(baseData!) : undefined }) } /// Some languages need to return different [language /// data](#state.EditorState.languageDataAt) for some parts of their /// tree. Sublanguages, registered by adding a [node /// prop](#language.sublanguageProp) to the language's top syntax /// node, provide a mechanism to do this. /// /// (Note that when using nested parsing, where nested syntax is /// parsed by a different parser and has its own top node type, you /// don't need a sublanguage.) export interface Sublanguage { /// Determines whether the data provided by this sublanguage should /// completely replace the regular data or be added to it (with /// higher-precedence). The default is `"extend"`. type?: "replace" | "extend", /// A predicate that returns whether the node at the queried /// position is part of the sublanguage. test: (node: SyntaxNode, state: EditorState) => boolean, /// The language data facet that holds the sublanguage's data. /// You'll want to use /// [`defineLanguageFacet`](#language.defineLanguageFacet) to create /// this. facet: Facet<{[name: string]: any}> } /// Syntax node prop used to register sublanguages. Should be added to /// the top level node type for the language. export const sublanguageProp = new NodeProp() /// A language object manages parsing and per-language /// [metadata](#state.EditorState.languageDataAt). Parse data is /// managed as a [Lezer](https://lezer.codemirror.net) tree. The class /// can be used directly, via the [`LRLanguage`](#language.LRLanguage) /// subclass for [Lezer](https://lezer.codemirror.net/) LR parsers, or /// via the [`StreamLanguage`](#language.StreamLanguage) subclass /// for stream parsers. export class Language { /// The extension value to install this as the document language. readonly extension: Extension /// The parser object. Can be useful when using this as a [nested /// parser](https://lezer.codemirror.net/docs/ref#common.Parser). parser: Parser /// Construct a language object. If you need to invoke this /// directly, first define a data facet with /// [`defineLanguageFacet`](#language.defineLanguageFacet), and then /// configure your parser to [attach](#language.languageDataProp) it /// to the language's outer syntax node. constructor( /// The [language data](#state.EditorState.languageDataAt) facet /// used for this language. readonly data: Facet<{[name: string]: any}>, parser: Parser, extraExtensions: Extension[] = [], /// A language name. readonly name: string = "" ) { // Kludge to define EditorState.tree as a debugging helper, // without the EditorState package actually knowing about // languages and lezer trees. if (!EditorState.prototype.hasOwnProperty("tree")) Object.defineProperty(EditorState.prototype, "tree", {get() { return syntaxTree(this) }}) this.parser = parser this.extension = [ language.of(this), EditorState.languageData.of((state, pos, side) => { let top = topNodeAt(state, pos, side), data = top.type.prop(languageDataProp) if (!data) return [] let base = state.facet(data), sub = top.type.prop(sublanguageProp) if (sub) { let innerNode = top.resolve(pos - top.from, side) for (let sublang of sub) if (sublang.test(innerNode, state)) { let data = state.facet(sublang.facet) return sublang.type == "replace" ? data : data.concat(base) } } return base }) ].concat(extraExtensions) } /// Query whether this language is active at the given position. isActiveAt(state: EditorState, pos: number, side: -1 | 0 | 1 = -1) { return topNodeAt(state, pos, side).type.prop(languageDataProp) == this.data } /// Find the document regions that were parsed using this language. /// The returned regions will _include_ any nested languages rooted /// in this language, when those exist. findRegions(state: EditorState) { let lang = state.facet(language) if (lang?.data == this.data) return [{from: 0, to: state.doc.length}] if (!lang || !lang.allowsNesting) return [] let result: {from: number, to: number}[] = [] let explore = (tree: Tree, from: number) => { if (tree.prop(languageDataProp) == this.data) { result.push({from, to: from + tree.length}) return } let mount = tree.prop(NodeProp.mounted) if (mount) { if (mount.tree.prop(languageDataProp) == this.data) { if (mount.overlay) for (let r of mount.overlay) result.push({from: r.from + from, to: r.to + from}) else result.push({from: from, to: from + tree.length}) return } else if (mount.overlay) { let size = result.length explore(mount.tree, mount.overlay[0].from + from) if (result.length > size) return } } for (let i = 0; i < tree.children.length; i++) { let ch = tree.children[i] if (ch instanceof Tree) explore(ch, tree.positions[i] + from) } } explore(syntaxTree(state), 0) return result } /// Indicates whether this language allows nested languages. The /// default implementation returns true. get allowsNesting() { return true } /// @internal static state: StateField /// @internal static setState = StateEffect.define() } function topNodeAt(state: EditorState, pos: number, side: -1 | 0 | 1) { let topLang = state.facet(language), tree = syntaxTree(state).topNode if (!topLang || topLang.allowsNesting) { for (let node: SyntaxNode | null = tree; node; node = node.enter(pos, side, IterMode.ExcludeBuffers | IterMode.EnterBracketed)) if (node.type.isTop) tree = node } return tree } /// A subclass of [`Language`](#language.Language) for use with Lezer /// [LR parsers](https://lezer.codemirror.net/docs/ref#lr.LRParser). export class LRLanguage extends Language { private constructor(data: Facet<{[name: string]: any}>, readonly parser: LRParser, name?: string) { super(data, parser, [], name) } /// Define a language from a parser. static define(spec: { /// The [name](#Language.name) of the language. name?: string, /// The parser to use. Should already have added editor-relevant /// node props (and optionally things like dialect and top rule) /// configured. parser: LRParser, /// [Language data](#state.EditorState.languageDataAt) /// to register for this language. languageData?: {[name: string]: any} }) { let data = defineLanguageFacet(spec.languageData) return new LRLanguage(data, spec.parser.configure({ props: [languageDataProp.add(type => type.isTop ? data : undefined)] }), spec.name) } /// Create a new instance of this language with a reconfigured /// version of its parser and optionally a new name. configure(options: ParserConfig, name?: string): LRLanguage { return new LRLanguage(this.data, this.parser.configure(options), name || this.name) } get allowsNesting() { return this.parser.hasWrappers() } } /// Get the syntax tree for a state, which is the current (possibly /// incomplete) parse tree of the active /// [language](#language.Language), or the empty tree if there is no /// language available. export function syntaxTree(state: EditorState): Tree { let field = state.field(Language.state, false) return field ? field.tree : Tree.empty } /// Try to get a parse tree that spans at least up to `upto`. The /// method will do at most `timeout` milliseconds of work to parse /// up to that point if the tree isn't already available. export function ensureSyntaxTree(state: EditorState, upto: number, timeout = 50): Tree | null { let parse = state.field(Language.state, false)?.context if (!parse) return null let oldVieport = parse.viewport parse.updateViewport({from: 0, to: upto}) let result = parse.isDone(upto) || parse.work(timeout, upto) ? parse.tree : null parse.updateViewport(oldVieport) return result } /// Queries whether there is a full syntax tree available up to the /// given document position. If there isn't, the background parse /// process _might_ still be working and update the tree further, but /// there is no guarantee of that—the parser will [stop /// working](#language.syntaxParserRunning) when it has spent a /// certain amount of time or has moved beyond the visible viewport. /// Always returns false if no language has been enabled. export function syntaxTreeAvailable(state: EditorState, upto = state.doc.length) { return state.field(Language.state, false)?.context.isDone(upto) || false } /// Move parsing forward, and update the editor state afterwards to /// reflect the new tree. Will work for at most `timeout` /// milliseconds. Returns true if the parser managed get to the given /// position in that time. export function forceParsing(view: EditorView, upto = view.viewport.to, timeout = 100): boolean { let success = ensureSyntaxTree(view.state, upto, timeout) if (success != syntaxTree(view.state)) view.dispatch({}) return !!success } /// Tells you whether the language parser is planning to do more /// parsing work (in a `requestIdleCallback` pseudo-thread) or has /// stopped running, either because it parsed the entire document, /// because it spent too much time and was cut off, or because there /// is no language parser enabled. export function syntaxParserRunning(view: EditorView) { return view.plugin(parseWorker)?.isWorking() || false } /// Lezer-style /// [`Input`](https://lezer.codemirror.net/docs/ref#common.Input) /// object for a [`Text`](#state.Text) object. export class DocInput implements Input { private cursor: TextIterator private cursorPos = 0 private string = "" /// Create an input object for the given document. constructor(readonly doc: Text) { this.cursor = doc.iter() } get length() { return this.doc.length } private syncTo(pos: number) { this.string = this.cursor.next(pos - this.cursorPos).value this.cursorPos = pos + this.string.length return this.cursorPos - this.string.length } chunk(pos: number) { this.syncTo(pos) return this.string } get lineChunks() { return true } read(from: number, to: number) { let stringStart = this.cursorPos - this.string.length if (from < stringStart || to >= this.cursorPos) return this.doc.sliceString(from, to) else return this.string.slice(from - stringStart, to - stringStart) } } const enum Work { // Milliseconds of work time to perform immediately for a state doc change Apply = 20, // Minimum amount of work time to perform in an idle callback MinSlice = 25, // Amount of work time to perform in pseudo-thread when idle callbacks aren't supported Slice = 100, // Minimum pause between pseudo-thread slices MinPause = 100, // Maximum pause (timeout) for the pseudo-thread MaxPause = 500, // Parse time budgets are assigned per chunk—the parser can run for // ChunkBudget milliseconds at most during ChunkTime milliseconds. // After that, no further background parsing is scheduled until the // next chunk in which the editor is active. ChunkBudget = 3000, ChunkTime = 30000, // For every change the editor receives while focused, it gets a // small bonus to its parsing budget (as a way to allow active // editors to continue doing work). ChangeBonus = 50, // Don't eagerly parse this far beyond the end of the viewport MaxParseAhead = 1e5, // When initializing the state field (before viewport info is // available), pretend the viewport goes from 0 to here. InitViewport = 3000, } let currentContext: ParseContext | null = null /// A parse context provided to parsers working on the editor content. export class ParseContext { private parse: PartialParse | null = null /// @internal tempSkipped: {from: number, to: number}[] = [] private constructor( private parser: Parser, /// The current editor state. readonly state: EditorState, /// Tree fragments that can be reused by incremental re-parses. public fragments: readonly TreeFragment[] = [], /// @internal public tree: Tree, /// @internal public treeLen: number, /// The current editor viewport (or some overapproximation /// thereof). Intended to be used for opportunistically avoiding /// work (in which case /// [`skipUntilInView`](#language.ParseContext.skipUntilInView) /// should be called to make sure the parser is restarted when the /// skipped region becomes visible). public viewport: {from: number, to: number}, /// @internal public skipped: {from: number, to: number}[], /// This is where skipping parsers can register a promise that, /// when resolved, will schedule a new parse. It is cleared when /// the parse worker picks up the promise. @internal public scheduleOn: Promise | null ) {} /// @internal static create(parser: Parser, state: EditorState, viewport: {from: number, to: number}) { return new ParseContext(parser, state, [], Tree.empty, 0, viewport, [], null) } private startParse() { return this.parser.startParse(new DocInput(this.state.doc), this.fragments) } /// @internal work(until: number | (() => boolean), upto?: number) { if (upto != null && upto >= this.state.doc.length) upto = undefined if (this.tree != Tree.empty && this.isDone(upto ?? this.state.doc.length)) { this.takeTree() return true } return this.withContext(() => { if (typeof until == "number") { let endTime = Date.now() + until until = () => Date.now() > endTime } if (!this.parse) this.parse = this.startParse() if (upto != null && (this.parse.stoppedAt == null || this.parse.stoppedAt > upto) && upto < this.state.doc.length) this.parse.stopAt(upto) for (;;) { let done = this.parse.advance() if (done) { this.fragments = this.withoutTempSkipped(TreeFragment.addTree(done, this.fragments, this.parse.stoppedAt != null)) this.treeLen = this.parse.stoppedAt ?? this.state.doc.length this.tree = done this.parse = null if (this.treeLen < (upto ?? this.state.doc.length)) this.parse = this.startParse() else return true } if (until()) return false } }) } /// @internal takeTree() { let pos, tree: Tree | undefined | null if (this.parse && (pos = this.parse.parsedPos) >= this.treeLen) { if (this.parse.stoppedAt == null || this.parse.stoppedAt > pos) this.parse.stopAt(pos) this.withContext(() => { while (!(tree = this.parse!.advance())) {} }) this.treeLen = pos this.tree = tree! this.fragments = this.withoutTempSkipped(TreeFragment.addTree(this.tree, this.fragments, true)) this.parse = null } } private withContext(f: () => T): T { let prev = currentContext currentContext = this try { return f() } finally { currentContext = prev } } private withoutTempSkipped(fragments: readonly TreeFragment[]) { for (let r; r = this.tempSkipped.pop();) fragments = cutFragments(fragments, r.from, r.to) return fragments } /// @internal changes(changes: ChangeDesc, newState: EditorState) { let {fragments, tree, treeLen, viewport, skipped} = this this.takeTree() if (!changes.empty) { let ranges: ChangedRange[] = [] changes.iterChangedRanges((fromA, toA, fromB, toB) => ranges.push({fromA, toA, fromB, toB})) fragments = TreeFragment.applyChanges(fragments, ranges) tree = Tree.empty treeLen = 0 viewport = {from: changes.mapPos(viewport.from, -1), to: changes.mapPos(viewport.to, 1)} if (this.skipped.length) { skipped = [] for (let r of this.skipped) { let from = changes.mapPos(r.from, 1), to = changes.mapPos(r.to, -1) if (from < to) skipped.push({from, to}) } } } return new ParseContext(this.parser, newState, fragments, tree, treeLen, viewport, skipped, this.scheduleOn) } /// @internal updateViewport(viewport: {from: number, to: number}) { if (this.viewport.from == viewport.from && this.viewport.to == viewport.to) return false this.viewport = viewport let startLen = this.skipped.length for (let i = 0; i < this.skipped.length; i++) { let {from, to} = this.skipped[i] if (from < viewport.to && to > viewport.from) { this.fragments = cutFragments(this.fragments, from, to) this.skipped.splice(i--, 1) } } if (this.skipped.length >= startLen) return false this.reset() return true } /// @internal reset() { if (this.parse) { this.takeTree() this.parse = null } } /// Notify the parse scheduler that the given region was skipped /// because it wasn't in view, and the parse should be restarted /// when it comes into view. skipUntilInView(from: number, to: number) { this.skipped.push({from, to}) } /// Returns a parser intended to be used as placeholder when /// asynchronously loading a nested parser. It'll skip its input and /// mark it as not-really-parsed, so that the next update will parse /// it again. /// /// When `until` is given, a reparse will be scheduled when that /// promise resolves. static getSkippingParser(until?: Promise): Parser { return new class extends Parser { createParse( input: Input, fragments: readonly TreeFragment[], ranges: readonly {from: number, to: number}[] ): PartialParse { let from = ranges[0].from, to = ranges[ranges.length - 1].to let parser = { parsedPos: from, advance() { let cx = currentContext if (cx) { for (let r of ranges) cx.tempSkipped.push(r) if (until) cx.scheduleOn = cx.scheduleOn ? Promise.all([cx.scheduleOn, until]) : until } this.parsedPos = to return new Tree(NodeType.none, [], [], to - from) }, stoppedAt: null, stopAt() {} } return parser } } } /// @internal isDone(upto: number) { upto = Math.min(upto, this.state.doc.length) let frags = this.fragments return this.treeLen >= upto && frags.length && frags[0].from == 0 && frags[0].to >= upto } /// Get the context for the current parse, or `null` if no editor /// parse is in progress. static get() { return currentContext } } function cutFragments(fragments: readonly TreeFragment[], from: number, to: number) { return TreeFragment.applyChanges(fragments, [{fromA: from, toA: to, fromB: from, toB: to}]) } class LanguageState { // The current tree. Immutable, because directly accessible from // the editor state. readonly tree: Tree constructor( // A mutable parse state that is used to preserve work done during // the lifetime of a state when moving to the next state. readonly context: ParseContext ) { this.tree = context.tree } apply(tr: Transaction) { if (!tr.docChanged && this.tree == this.context.tree) return this let newCx = this.context.changes(tr.changes, tr.state) // If the previous parse wasn't done, go forward only up to its // end position or the end of the viewport, to avoid slowing down // state updates with parse work beyond the viewport. let upto = this.context.treeLen == tr.startState.doc.length ? undefined : Math.max(tr.changes.mapPos(this.context.treeLen), newCx.viewport.to) if (!newCx.work(Work.Apply, upto)) newCx.takeTree() return new LanguageState(newCx) } static init(state: EditorState) { let vpTo = Math.min(Work.InitViewport, state.doc.length) let parseState = ParseContext.create(state.facet(language)!.parser, state, {from: 0, to: vpTo}) if (!parseState.work(Work.Apply, vpTo)) parseState.takeTree() return new LanguageState(parseState) } } Language.state = StateField.define({ create: LanguageState.init, update(value, tr) { for (let e of tr.effects) if (e.is(Language.setState)) return e.value if (tr.startState.facet(language) != tr.state.facet(language)) return LanguageState.init(tr.state) return value.apply(tr) } }) let requestIdle = (callback: (deadline?: IdleDeadline) => void) => { let timeout = setTimeout(() => callback(), Work.MaxPause) return () => clearTimeout(timeout) } if (typeof requestIdleCallback != "undefined") requestIdle = (callback: (deadline?: IdleDeadline) => void) => { let idle = -1, timeout = setTimeout(() => { idle = requestIdleCallback(callback, {timeout: Work.MaxPause - Work.MinPause}) }, Work.MinPause) return () => idle < 0 ? clearTimeout(timeout) : cancelIdleCallback(idle) } const isInputPending = typeof navigator != "undefined" && (navigator as any).scheduling?.isInputPending ? () => (navigator as any).scheduling.isInputPending() : null const parseWorker = ViewPlugin.fromClass(class ParseWorker { working: (() => void) | null = null workScheduled = 0 // End of the current time chunk chunkEnd = -1 // Milliseconds of budget left for this chunk chunkBudget = -1 constructor(readonly view: EditorView) { this.work = this.work.bind(this) this.scheduleWork() } update(update: ViewUpdate) { let cx = this.view.state.field(Language.state).context if (cx.updateViewport(update.view.viewport) || this.view.viewport.to > cx.treeLen) this.scheduleWork() if (update.docChanged || update.selectionSet) { if (this.view.hasFocus) this.chunkBudget += Work.ChangeBonus this.scheduleWork() } this.checkAsyncSchedule(cx) } scheduleWork() { if (this.working) return let {state} = this.view, field = state.field(Language.state) if (field.tree != field.context.tree || !field.context.isDone(state.doc.length)) this.working = requestIdle(this.work) } work(deadline?: IdleDeadline) { this.working = null let now = Date.now() if (this.chunkEnd < now && (this.chunkEnd < 0 || this.view.hasFocus)) { // Start a new chunk this.chunkEnd = now + Work.ChunkTime this.chunkBudget = Work.ChunkBudget } if (this.chunkBudget <= 0) return // No more budget let {state, viewport: {to: vpTo}} = this.view, field = state.field(Language.state) if (field.tree == field.context.tree && field.context.isDone(vpTo + Work.MaxParseAhead)) return let endTime = Date.now() + Math.min( this.chunkBudget, Work.Slice, deadline && !isInputPending ? Math.max(Work.MinSlice, deadline.timeRemaining() - 5) : 1e9) let viewportFirst = field.context.treeLen < vpTo && state.doc.length > vpTo + 1000 let done = field.context.work(() => { return isInputPending && isInputPending() || Date.now() > endTime } , vpTo + (viewportFirst ? 0 : Work.MaxParseAhead)) this.chunkBudget -= Date.now() - now if (done || this.chunkBudget <= 0) { field.context.takeTree() this.view.dispatch({effects: Language.setState.of(new LanguageState(field.context))}) } if (this.chunkBudget > 0 && !(done && !viewportFirst)) this.scheduleWork() this.checkAsyncSchedule(field.context) } checkAsyncSchedule(cx: ParseContext) { if (cx.scheduleOn) { this.workScheduled++ cx.scheduleOn .then(() => this.scheduleWork()) .catch(err => logException(this.view.state, err)) .then(() => this.workScheduled--) cx.scheduleOn = null } } destroy() { if (this.working) this.working() } isWorking() { return !!(this.working || this.workScheduled > 0) } }, { eventHandlers: {focus() { this.scheduleWork() }} }) /// The facet used to associate a language with an editor state. Used /// by `Language` object's `extension` property (so you don't need to /// manually wrap your languages in this). Can be used to access the /// current language on a state. export const language = Facet.define({ combine(languages) { return languages.length ? languages[0] : null }, enables: language => [ Language.state, parseWorker, EditorView.contentAttributes.compute([language], state => { let lang = state.facet(language) return lang && lang.name ? {"data-language": lang.name} : {} as {} }) ] }) /// This class bundles a [language](#language.Language) with an /// optional set of supporting extensions. Language packages are /// encouraged to export a function that optionally takes a /// configuration object and returns a `LanguageSupport` instance, as /// the main way for client code to use the package. export class LanguageSupport { /// An extension including both the language and its support /// extensions. (Allowing the object to be used as an extension /// value itself.) extension: Extension /// Create a language support object. constructor( /// The language object. readonly language: Language, /// An optional set of supporting extensions. When nesting a /// language in another language, the outer language is encouraged /// to include the supporting extensions for its inner languages /// in its own set of support extensions. readonly support: Extension = [] ) { this.extension = [language, support] } } /// Language descriptions are used to store metadata about languages /// and to dynamically load them. Their main role is finding the /// appropriate language for a filename or dynamically loading nested /// parsers. export class LanguageDescription { private loading: Promise | null = null private constructor( /// The name of this language. readonly name: string, /// Alternative names for the mode (lowercased, includes `this.name`). readonly alias: readonly string[], /// File extensions associated with this language. readonly extensions: readonly string[], /// Optional filename pattern that should be associated with this /// language. readonly filename: RegExp | undefined, private loadFunc: () => Promise, /// If the language has been loaded, this will hold its value. public support: LanguageSupport | undefined = undefined ) {} /// Start loading the the language. Will return a promise that /// resolves to a [`LanguageSupport`](#language.LanguageSupport) /// object when the language successfully loads. load(): Promise { return this.loading || (this.loading = this.loadFunc().then( support => this.support = support, err => { this.loading = null; throw err } )) } /// Create a language description. static of(spec: { /// The language's name. name: string, /// An optional array of alternative names. alias?: readonly string[], /// An optional array of filename extensions associated with this /// language. extensions?: readonly string[], /// An optional filename pattern associated with this language. filename?: RegExp, /// A function that will asynchronously load the language. load?: () => Promise, /// Alternatively to `load`, you can provide an already loaded /// support object. Either this or `load` should be provided. support?: LanguageSupport }) { let {load, support} = spec if (!load) { if (!support) throw new RangeError("Must pass either 'load' or 'support' to LanguageDescription.of") load = () => Promise.resolve(support!) } return new LanguageDescription(spec.name, (spec.alias || []).concat(spec.name).map(s => s.toLowerCase()), spec.extensions || [], spec.filename, load, support) } /// Look for a language in the given array of descriptions that /// matches the filename. Will first match /// [`filename`](#language.LanguageDescription.filename) patterns, /// and then [extensions](#language.LanguageDescription.extensions), /// and return the first language that matches. static matchFilename(descs: readonly LanguageDescription[], filename: string) { for (let d of descs) if (d.filename && d.filename.test(filename)) return d let ext = /\.([^.]+)$/.exec(filename) if (ext) for (let d of descs) if (d.extensions.indexOf(ext[1]) > -1) return d return null } /// Look for a language whose name or alias matches the the given /// name (case-insensitively). If `fuzzy` is true, and no direct /// matchs is found, this'll also search for a language whose name /// or alias occurs in the string (for names shorter than three /// characters, only when surrounded by non-word characters). static matchLanguageName(descs: readonly LanguageDescription[], name: string, fuzzy = true) { name = name.toLowerCase() for (let d of descs) if (d.alias.some(a => a == name)) return d if (fuzzy) for (let d of descs) for (let a of d.alias) { let found = name.indexOf(a) if (found > -1 && (a.length > 2 || !/\w/.test(name[found - 1]) && !/\w/.test(name[found + a.length]))) return d } return null } }