diff --git a/.eslintrc.js b/.eslintrc.js index 930b631331..e531d00a73 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,6 +17,10 @@ module.exports = { "ecmaVersion": 2018, "sourceType": "module" }, + "parser": '@typescript-eslint/parser', + "plugins": [ + "@typescript-eslint" + ], "rules": { "array-callback-return": "error", "block-scoped-var": "error", @@ -27,7 +31,7 @@ module.exports = { // for now ignore diff between types of quoting "quotes": "off", // this is the style we are already using - "operator-linebreak": ["error","after", { "overrides": { "?": "after", ":": "after" } }], + "operator-linebreak": ["error","before", { "overrides": { "?": "after", ":": "after", "+": "after" } }], // sometimes we declare variables with extra spacing "indent": ["error", 2, {"VariableDeclarator":2}], // seems like a good idea not to use explicit undefined diff --git a/CHANGES.md b/CHANGES.md index 03e90acb8e..684bea0c5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,8 @@ Parser Engine: Deprecations: +- when using `highlightBlock` `result.re` deprecated. Use `result.relevance` instead. (#2552) [Josh Goebel][] +- ditto for `result.second_best.re` => `result.second_best.relevance` (#2552) - `lexemes` is now deprecated in favor of `keywords.$pattern` key (#2519) [Josh Goebel][] - `endSameAsBegin` is now deprecated. (#2261) [Josh Goebel][] @@ -34,6 +36,7 @@ Language Improvements: - fix(yaml) Fix tags to include non-word characters (#2486) [Peter Plantinga][] - fix(swift) `@objcMembers` was being partially highlighted (#2543) [Nick Randall][] - enh(dart) Add `late` and `required` keywords, and `Never` built-in type (#2550) [Sam Rawlins][] +- enh(erlang) Add underscore separators to numeric literals (#2554) [Sergey Prokhorov][] [Josh Goebel]: https://github.com/yyyc514 [Peter Plantinga]: https://github.com/pplantinga @@ -42,6 +45,7 @@ Language Improvements: [Hankun Lin]: https://github.com/Linhk1606 [Nick Randall]: https://github.com/nicked [Sam Rawlins]: https://github.com/srawlins +[Sergey Prokhorov]: https://github.com/seriyps ## Version 10.0.2 diff --git a/SUPPORTED_LANGUAGES.md b/SUPPORTED_LANGUAGES.md index ad4bbdc950..5d3d3ea152 100644 --- a/SUPPORTED_LANGUAGES.md +++ b/SUPPORTED_LANGUAGES.md @@ -92,6 +92,7 @@ Languages that listed a **Package** below are 3rd party languages and are not bu | JSON | json | | | Java | java, jsp | | | JavaScript | javascript, js, jsx | | +| Jolie | jolie, iol, ol | [highlightjs-jolie](https://github.com/xiroV/highlightjs-jolie) | | Kotlin | kotlin, kt | | | LaTeX | tex | | | Leaf | leaf | | diff --git a/docs/api.rst b/docs/api.rst index 41f7014ad6..8b5ee44540 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -101,13 +101,13 @@ Applies highlighting to all ``
...
`` blocks on a page. Attaches highlighting to the page load event. -``registerLanguage(name, language)`` +``registerLanguage(languageName, languageDefinition)`` ------------------------------------ Adds new language to the library under the specified name. Used mostly internally. -* ``name``: a string with the name of the language being registered -* ``language``: a function that returns an object which represents the +* ``languageName``: a string with the name of the language being registered +* ``languageDefinition``: a function that returns an object which represents the language definition. The function is passed the ``hljs`` object to be able to use common regular expressions defined within it. diff --git a/package-lock.json b/package-lock.json index 09fe06c654..318acfc140 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -79,6 +85,12 @@ "@types/node": "*" } }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -91,6 +103,98 @@ "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.32.0.tgz", + "integrity": "sha512-nb1kSUa8cd22hGgxpGdVT6/iyP7IKyrnyZEGYo+tN8iyDdXvXa+nfsX03tJVeFfhbkwR/0CDk910zPbqSflAsg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.32.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.32.0.tgz", + "integrity": "sha512-oDWuB2q5AXsQ/mLq2N4qtWiBASWXPf7KhqXgeGH4QsyVKx+km8F6Vfqd3bspJQyhyCqxcbLO/jKJuIV3DzHZ6A==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.32.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.32.0.tgz", + "integrity": "sha512-swRtH835fUfm2khchiOVNchU3gVNaZNj2pY92QSx4kXan+RzaGNrwIRaCyX8uqzmK0xNPzseaUYHP8CsmrsjFw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.32.0", + "@typescript-eslint/typescript-estree": "2.32.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.32.0.tgz", + "integrity": "sha512-hQpbWM/Y2iq6jB9FHYJBqa3h1R9IEGodOtajhb261cVHt9cz30AKjXM6WP7LxJdEPPlyJ9rPTZVgBUgZgiyPgw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", @@ -3719,6 +3823,15 @@ "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3749,6 +3862,12 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true }, + "typescript": { + "version": "4.0.0-dev.20200512", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.0-dev.20200512.tgz", + "integrity": "sha512-ZsVvhdxpQaA6KpjlT8wNNtweORzNsMtwgCo8viKWQmOvaU+BlMsd3MjD2LONQjFSiETCaw4uq0nNdyfKrCjjIw==", + "dev": true + }, "uglify-js": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.2.tgz", diff --git a/package.json b/package.json index f442149c96..c6f72791e3 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,13 @@ "url": "git://github.com/highlightjs/highlight.js.git" }, "main": "./lib/index.js", + "types": "./types/index.d.ts", "scripts": { "mocha": "mocha", - "build_and_test": "npm run build && npm run test", "build": "node ./tools/build.js -t node", "build-cdn": "node ./tools/build.js -t cdn", "build-browser": "node ./tools/build.js -t browser :common", - "test": "mocha --globals document test", "test-markup": "mocha --globals document test/markup", "test-detect": "mocha --globals document test/detect", @@ -38,6 +37,8 @@ "node": "*" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^2.32.0", + "@typescript-eslint/parser": "^2.32.0", "clean-css": "^4.2.1", "cli-table": "^0.3.1", "colors": "^1.1.2", @@ -62,7 +63,8 @@ "rollup-plugin-json": "^4.0.0", "should": "^13.2.3", "terser": "^4.3.9", - "tiny-worker": "^2.3.0" + "tiny-worker": "^2.3.0", + "typescript": "^4.0.0-dev.20200512" }, "dependencies": {} } diff --git a/src/highlight.js b/src/highlight.js index 2b3f776429..fbcca7a24d 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -18,26 +18,33 @@ const inherit = utils.inherit; const { nodeStream, mergeStreams } = utils; const NO_MATCH = Symbol("nomatch"); +/** + * @param {any} hljs - object that is extended (legacy) + */ const HLJS = function(hljs) { // Convenience variables for build-in objects + /** @type {unknown[]} */ var ArrayProto = []; // Global internal variables used within the highlight.js library. + /** @type {Record} */ var languages = {}; + /** @type {Record} */ var aliases = {}; + /** @type {HLJSPlugin[]} */ var plugins = []; // safe/production mode - swallows more errors, tries to keep running // even if a single syntax or parse hits a fatal error var SAFE_MODE = true; - - // Regular expressions used throughout the highlight.js library. var fixMarkupRe = /(^(<[^>]+>|\t|)+|\n)/gm; - var LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?"; + /** @type {Language} */ + const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] }; // Global options used when within external APIs. This is modified when // calling the `hljs.configure` function. + /** @type HLJSOptions */ var options = { noHighlightRe: /^(no-?highlight)$/i, languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, @@ -52,10 +59,17 @@ const HLJS = function(hljs) { /* Utility functions */ - function shouldNotHighlight(language) { - return options.noHighlightRe.test(language); + /** + * Tests a language name to see if highlighting should be skipped + * @param {string} languageName + */ + function shouldNotHighlight(languageName) { + return options.noHighlightRe.test(languageName); } + /** + * @param {HighlightedHTMLElement} block - the HTML element to determine language for + */ function blockLanguage(block) { var classes = block.className + ' '; @@ -82,18 +96,19 @@ const HLJS = function(hljs) { * * @param {string} languageName - the language to use for highlighting * @param {string} code - the code to highlight - * @param {boolean} ignoreIllegals - whether to ignore illegal matches, default is to bail - * @param {array} continuation - array of continuation modes + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {Mode} [continuation] - current continuation mode, if any * - * @returns an object that represents the result + * @returns {HighlightResult} Result - an object that represents the result * @property {string} language - the language name * @property {number} relevance - the relevance score * @property {string} value - the highlighted HTML code * @property {string} code - the original raw code - * @property {mode} top - top of the current mode stack + * @property {Mode} top - top of the current mode stack * @property {boolean} illegal - indicates whether any illegal matches were found */ function highlight(languageName, code, ignoreIllegals, continuation) { + /** @type {{ code: string, language: string, result?: any }} */ var context = { code, language: languageName @@ -115,10 +130,23 @@ const HLJS = function(hljs) { return result; } - // private highlight that's used internally and does not fire callbacks + /** + * private highlight that's used internally and does not fire callbacks + * + * @param {string} languageName - the language to use for highlighting + * @param {string} code - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {Mode} [continuation] - current continuation mode, if any + */ function _highlight(languageName, code, ignoreIllegals, continuation) { var codeToHighlight = code; + /** + * Return keyword data if a match is a keyword + * @param {CompiledMode} mode - current mode + * @param {RegExpMatchArray} match - regexp match data + * @returns {KeywordData | false} + */ function keywordData(mode, match) { var matchText = language.case_insensitive ? match[0].toLowerCase() : match[0]; return Object.prototype.hasOwnProperty.call(mode.keywords, matchText) && mode.keywords[matchText]; @@ -157,18 +185,20 @@ const HLJS = function(hljs) { function processSubLanguage() { if (mode_buffer === "") return; + /** @type HighlightResult */ + var result = null; - var explicit = typeof top.subLanguage === 'string'; - - if (explicit && !languages[top.subLanguage]) { - emitter.addText(mode_buffer); - return; + if (typeof top.subLanguage === 'string') { + if (!languages[top.subLanguage]) { + emitter.addText(mode_buffer); + return; + } + result = _highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]); + continuations[top.subLanguage] = result.top; + } else { + result = highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : null); } - var result = explicit ? - _highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : - highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : null); - // Counting embedded language score towards the host language may be disabled // with zeroing the containing mode relevance. Use case in point is Markdown that // allows XML everywhere and makes every XML snippet to have a much larger Markdown @@ -176,9 +206,6 @@ const HLJS = function(hljs) { if (top.relevance > 0) { relevance += result.relevance; } - if (explicit) { - continuations[top.subLanguage] = result.top; - } emitter.addSublanguage(result.emitter, result.language); } @@ -191,23 +218,31 @@ const HLJS = function(hljs) { mode_buffer = ''; } + /** + * @param {Mode} mode - new mode to start + */ function startNewMode(mode) { if (mode.className) { emitter.openNode(mode.className); } - top = Object.create(mode, {parent: {value: top}}); + top = Object.create(mode, { parent: { value: top } }); return top; } + /** + * @param {CompiledMode } mode - the mode to potentially end + * @param {RegExpMatchArray} match - the latest match + * @param {string} matchPlusRemainder - match plus remainder of content + * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode + */ function endOfMode(mode, match, matchPlusRemainder) { let matched = regex.startsWith(mode.endRe, matchPlusRemainder); if (matched) { if (mode["on:end"]) { - let resp = new Response(mode); + const resp = new Response(mode); mode["on:end"](match, resp); - if (resp.ignore) - matched = false; + if (resp.ignore) matched = false; } if (matched) { @@ -224,6 +259,11 @@ const HLJS = function(hljs) { } } + /** + * Handle matching but then ignoring a sequence of text + * + * @param {string} lexeme - string containing full match text + */ function doIgnore(lexeme) { if (top.matcher.regexIndex === 0) { // no more regexs to potentially match here, so we move the cursor forward one @@ -238,15 +278,20 @@ const HLJS = function(hljs) { } } + /** + * Handle the start of a new potential mode match + * + * @param {EnhancedMatch} match - the current match + * @returns {number} how far to advance the parse cursor + */ function doBeginMatch(match) { var lexeme = match[0]; var new_mode = match.rule; - var mode; - let resp = new Response(new_mode); + const resp = new Response(new_mode); // first internal before callbacks, then the public ones - let beforeCallbacks = [new_mode.__beforeBegin, new_mode["on:begin"]]; - for (let cb of beforeCallbacks) { + const beforeCallbacks = [new_mode.__beforeBegin, new_mode["on:begin"]]; + for (const cb of beforeCallbacks) { if (!cb) continue; cb(match, resp); if (resp.ignore) return doIgnore(lexeme); @@ -267,7 +312,7 @@ const HLJS = function(hljs) { mode_buffer = lexeme; } } - mode = startNewMode(new_mode); + startNewMode(new_mode); // if (mode["after:begin"]) { // let resp = new Response(mode); // mode["after:begin"](match, resp); @@ -275,6 +320,11 @@ const HLJS = function(hljs) { return new_mode.returnBegin ? 0 : lexeme.length; } + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ function doEndMatch(match) { var lexeme = match[0]; var matchPlusRemainder = codeToHighlight.substr(match.index); @@ -322,7 +372,15 @@ const HLJS = function(hljs) { list.forEach(item => emitter.openNode(item)); } + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ var lastMatch = {}; + + /** + * Process an individual match + * + * @param {string} textBeforeMatch - text preceeding the match (since the last match) + * @param {EnhancedMatch} [match] - the match itself + */ function processLexeme(textBeforeMatch, match) { var lexeme = match && match[0]; @@ -342,6 +400,7 @@ const HLJS = function(hljs) { // spit the "skipped" character that our regex choked on back into the output sequence mode_buffer += codeToHighlight.slice(match.index, match.index + 1); if (!SAFE_MODE) { + /** @type {AnnotatedError} */ const err = new Error('0 width match regex'); err.languageName = languageName; err.badRule = lastMatch.rule; @@ -355,6 +414,7 @@ const HLJS = function(hljs) { return doBeginMatch(match); } else if (match.type === "illegal" && !ignoreIllegals) { // illegal match, we do not continue processing + /** @type {AnnotatedError} */ const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); err.mode = top; throw err; @@ -404,9 +464,11 @@ const HLJS = function(hljs) { throw new Error('Unknown language: "' + languageName + '"'); } - compileLanguage(language); + var md = compileLanguage(language); var result = ''; - var top = continuation || language; + /** @type {CompiledMode} */ + var top = continuation || md; + /** @type Record */ var continuations = {}; // keep continuations for sub-languages var emitter = new options.__emitter(options); processContinuations(); @@ -422,9 +484,9 @@ const HLJS = function(hljs) { for (;;) { iterations++; if (continueScanAtSamePosition) { - continueScanAtSamePosition = false; // only regexes not matched previously will now be // considered for a potential match + continueScanAtSamePosition = false; } else { top.matcher.lastIndex = index; top.matcher.considerAll(); @@ -466,6 +528,7 @@ const HLJS = function(hljs) { }; } else if (SAFE_MODE) { return { + illegal: false, relevance: 0, value: escape(codeToHighlight), emitter: emitter, @@ -479,10 +542,13 @@ const HLJS = function(hljs) { } } - // returns a valid highlight result, without actually - // doing any actual work, auto highlight starts with - // this and it's possible for small snippets that - // auto-detection may not find a better match + /** + * returns a valid highlight result, without actually doing any actual work, + * auto highlight starts with this and it's possible for small snippets that + * auto-detection may not find a better match + * @param {string} code + * @returns {HighlightResult} + */ function justTextHighlightResult(code) { const result = { relevance: 0, @@ -491,11 +557,11 @@ const HLJS = function(hljs) { illegal: false, top: PLAINTEXT_LANGUAGE }; - result.emitter.addText(code) + result.emitter.addText(code); return result; } - /* + /** Highlighting with language detection. Accepts a string with the code to highlight. Returns an object with the following properties: @@ -505,10 +571,13 @@ const HLJS = function(hljs) { - second_best (object with the same structure for second-best heuristically detected language, may be absent) + @param {string} code + @param {Array} [languageSubset] + @returns {AutoHighlightResult} */ function highlightAuto(code, languageSubset) { languageSubset = languageSubset || options.languages || Object.keys(languages); - var result = justTextHighlightResult(code) + var result = justTextHighlightResult(code); var secondBest = result; languageSubset.filter(getLanguage).filter(autoDetection).forEach(function(name) { var current = _highlight(name, code, false); @@ -528,19 +597,21 @@ const HLJS = function(hljs) { return result; } - /* + /** Post-processing of the highlighted markup: - replace TABs with something more useful - replace real line-breaks with '
' for non-pre containers + @param {string} html + @returns {string} */ - function fixMarkup(value) { + function fixMarkup(html) { if (!(options.tabReplace || options.useBR)) { - return value; + return html; } - return value.replace(fixMarkupRe, match => { + return html.replace(fixMarkupRe, match => { if (match === '\n') { return options.useBR ? '
' : match; } else if (options.tabReplace) { @@ -550,6 +621,13 @@ const HLJS = function(hljs) { }); } + /** + * Builds new class name for block given the language name + * + * @param {string} prevClassName + * @param {string} [currentLang] + * @param {string} [resultLang] + */ function buildClassName(prevClassName, currentLang, resultLang) { var language = currentLang ? aliases[currentLang] : resultLang; var result = [prevClassName.trim()]; @@ -565,24 +643,27 @@ const HLJS = function(hljs) { return result.join(' ').trim(); } - /* - Applies highlighting to a DOM node containing code. Accepts a DOM node and - two optional parameters for fixMarkup. + /** + * Applies highlighting to a DOM node containing code. Accepts a DOM node and + * two optional parameters for fixMarkup. + * + * @param {HighlightedHTMLElement} element - the HTML element to highlight */ - function highlightBlock(block) { + function highlightBlock(element) { + /** @type HTMLElement */ let node = null; - const language = blockLanguage(block); + const language = blockLanguage(element); if (shouldNotHighlight(language)) return; fire("before:highlightBlock", - { block: block, language: language }); + { block: element, language: language }); if (options.useBR) { node = document.createElement('div'); - node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(//g, '\n'); + node.innerHTML = element.innerHTML.replace(/\n/g, '').replace(//g, '\n'); } else { - node = block; + node = element; } const text = node.textContent; const result = language ? highlight(language, text, true) : highlightAuto(text); @@ -595,55 +676,66 @@ const HLJS = function(hljs) { } result.value = fixMarkup(result.value); - fire("after:highlightBlock", { block: block, result: result }); + fire("after:highlightBlock", { block: element, result: result }); - block.innerHTML = result.value; - block.className = buildClassName(block.className, language, result.language); - block.result = { + element.innerHTML = result.value; + element.className = buildClassName(element.className, language, result.language); + element.result = { language: result.language, - re: result.relevance + // TODO: remove with version 11.0 + re: result.relevance, + relavance: result.relevance, }; if (result.second_best) { - block.second_best = { + element.second_best = { language: result.second_best.language, - re: result.second_best.relevance + // TODO: remove with version 11.0 + re: result.second_best.relevance, + relavance: result.second_best.relevance }; } } - /* - Updates highlight.js global options with values passed in the form of an object. - */ + /** + * Updates highlight.js global options with the passed options + * + * @param {{}} userOptions + */ function configure(userOptions) { options = inherit(options, userOptions); } - /* - Applies highlighting to all
..
blocks on a page. - */ - function initHighlighting() { + /** + * Highlights to all
 blocks on a page
+   *
+   * @type {Function & {called?: boolean}}
+   */
+  const initHighlighting = () => {
     if (initHighlighting.called) return;
     initHighlighting.called = true;
 
     var blocks = document.querySelectorAll('pre code');
     ArrayProto.forEach.call(blocks, highlightBlock);
-  }
+  };
 
-  /*
-  Attaches highlighting to the page load event.
-  */
+  // Higlights all when DOMContentLoaded fires
   function initHighlightingOnLoad() {
+    // @ts-ignore
     window.addEventListener('DOMContentLoaded', initHighlighting, false);
   }
 
-  const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text' };
-
-  function registerLanguage(name, language) {
+  /**
+   * Register a language grammar module
+   *
+   * @param {string} languageName
+   * @param {LanguageFn} languageDefinition
+   */
+  function registerLanguage(languageName, languageDefinition) {
     var lang = null;
     try {
-      lang = language(hljs);
+      lang = languageDefinition(hljs);
     } catch (error) {
-      console.error("Language definition for '{}' could not be registered.".replace("{}", name));
+      console.error("Language definition for '{}' could not be registered.".replace("{}", languageName));
       // hard or soft error
       if (!SAFE_MODE) { throw error; } else { console.error(error); }
       // languages that have serious errors are replaced with essentially a
@@ -653,24 +745,30 @@ const HLJS = function(hljs) {
       lang = PLAINTEXT_LANGUAGE;
     }
     // give it a temporary name if it doesn't have one in the meta-data
-    if (!lang.name) lang.name = name;
-    languages[name] = lang;
-    lang.rawDefinition = language.bind(null, hljs);
+    if (!lang.name) lang.name = languageName;
+    languages[languageName] = lang;
+    lang.rawDefinition = languageDefinition.bind(null, hljs);
 
     if (lang.aliases) {
-      registerAliases(lang.aliases, { languageName: name });
+      registerAliases(lang.aliases, { languageName });
     }
   }
 
+  /**
+   * @returns {string[]} List of language internal names
+   */
   function listLanguages() {
     return Object.keys(languages);
   }
 
-  /*
+  /**
     intended usage: When one language truly requires another
 
     Unlike `getLanguage`, this will throw when the requested language
     is not available.
+
+    @param {string} name - name of the language to fetch/require
+    @returns {Language | never}
   */
   function requireLanguage(name) {
     var lang = getLanguage(name);
@@ -680,27 +778,48 @@ const HLJS = function(hljs) {
     throw err;
   }
 
+  /**
+   * @param {string} name - name of the language to retrieve
+   * @returns {Language | undefined}
+   */
   function getLanguage(name) {
     name = (name || '').toLowerCase();
     return languages[name] || languages[aliases[name]];
   }
 
-  function registerAliases(aliasList, {languageName}) {
+  /**
+   *
+   * @param {string|string[]} aliasList - single alias or list of aliases
+   * @param {{languageName: string}} opts
+   */
+  function registerAliases(aliasList, { languageName }) {
     if (typeof aliasList === 'string') {
-      aliasList = [aliasList]
+      aliasList = [aliasList];
     }
-    aliasList.forEach(alias => aliases[alias] = languageName);
+    aliasList.forEach(alias => { aliases[alias] = languageName; });
   }
 
+  /**
+   * Determines if a given language has auto-detection enabled
+   * @param {string} name - name of the language
+   */
   function autoDetection(name) {
     var lang = getLanguage(name);
     return lang && !lang.disableAutodetect;
   }
 
+  /**
+   * @param {HLJSPlugin} plugin
+   */
   function addPlugin(plugin) {
     plugins.push(plugin);
   }
 
+  /**
+   *
+   * @param {PluginEvent} event
+   * @param {any} args
+   */
   function fire(event, args) {
     var cb = event;
     plugins.forEach(function(plugin) {
@@ -735,7 +854,9 @@ const HLJS = function(hljs) {
   hljs.versionString = packageJSON.version;
 
   for (const key in MODES) {
+    // @ts-ignore
     if (typeof MODES[key] === "object") {
+      // @ts-ignore
       deepFreeze(MODES[key]);
     }
   }
diff --git a/src/languages/abnf.js b/src/languages/abnf.js
index 3b45dff974..505a35426f 100644
--- a/src/languages/abnf.js
+++ b/src/languages/abnf.js
@@ -4,6 +4,7 @@ Author: Alex McKibben 
 Website: https://tools.ietf.org/html/rfc5234
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
     var regexes = {
         ruleDeclaration: "^[a-zA-Z][a-zA-Z0-9-]*",
diff --git a/src/languages/accesslog.js b/src/languages/accesslog.js
index 8d26334647..7826f8631e 100644
--- a/src/languages/accesslog.js
+++ b/src/languages/accesslog.js
@@ -5,6 +5,7 @@
  Website: https://httpd.apache.org/docs/2.4/logs.html#accesslog
  */
 
+ /** @type LanguageFn */
 export default function(hljs) {
   // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
   var HTTP_VERBS = [
diff --git a/src/languages/actionscript.js b/src/languages/actionscript.js
index 80e4f9b724..bd290ef823 100644
--- a/src/languages/actionscript.js
+++ b/src/languages/actionscript.js
@@ -4,6 +4,7 @@ Author: Alexander Myadzel 
 Category: scripting
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var IDENT_RE = '[a-zA-Z_$][a-zA-Z0-9_$]*';
   var IDENT_FUNC_RETURN_TYPE_RE = '([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)';
diff --git a/src/languages/ada.js b/src/languages/ada.js
index 64a96f6176..eba10a79d5 100644
--- a/src/languages/ada.js
+++ b/src/languages/ada.js
@@ -17,6 +17,7 @@ Description: Ada is a general-purpose programming language that has great suppor
 // xml (broken by Foo : Bar type), elm (broken by Foo : Bar type), vbscript-html (broken by body keyword)
 // sql (ada default.txt has a lot of sql keywords)
 
+/** @type LanguageFn */
 export default function(hljs) {
     // Regular expression for Ada numeric literals.
     // stolen form the VHDL highlighter
diff --git a/src/languages/angelscript.js b/src/languages/angelscript.js
index f192efa352..d04be25d50 100644
--- a/src/languages/angelscript.js
+++ b/src/languages/angelscript.js
@@ -5,6 +5,7 @@ Category: scripting
 Website: https://www.angelcode.com/angelscript/
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var builtInTypeMode = {
     className: 'built_in',
diff --git a/src/languages/apache.js b/src/languages/apache.js
index 2ee76a6861..9c8fd7db6c 100644
--- a/src/languages/apache.js
+++ b/src/languages/apache.js
@@ -7,6 +7,7 @@ Description: language definition for Apache configuration files (httpd.conf & .h
 Category: common, config
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var NUMBER_REF = {className: 'number', begin: '[\\$%]\\d+'};
   var NUMBER = {className: 'number', begin: '\\d+'};
diff --git a/src/languages/applescript.js b/src/languages/applescript.js
index 0fd9cc71c5..339f9b906d 100644
--- a/src/languages/applescript.js
+++ b/src/languages/applescript.js
@@ -5,6 +5,7 @@ Category: scripting
 Website: https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: ''});
   var PARAMS = {
diff --git a/src/languages/arcade.js b/src/languages/arcade.js
index 9485e20f91..8370158ebb 100644
--- a/src/languages/arcade.js
+++ b/src/languages/arcade.js
@@ -5,6 +5,8 @@
  Website: https://developers.arcgis.com/arcade/
  Description: ArcGIS Arcade is an expression language used in many Esri ArcGIS products such as Pro, Online, Server, Runtime, JavaScript, and Python
 */
+
+/** @type LanguageFn */
 export default function(hljs) {
   var IDENT_RE = '[A-Za-z_][0-9A-Za-z_]*';
   var KEYWORDS = {
@@ -25,7 +27,6 @@ export default function(hljs) {
       'TrackGeometryWindow TrackIndex TrackStartTime TrackWindow TypeOf Union UrlEncode Variance ' +
       'Weekday When Within Year '
   };
-  var EXPRESSIONS;
   var SYMBOL = {
     className: 'symbol',
     begin: '\\$[datastore|feature|layer|map|measure|sourcefeature|sourcelayer|targetfeature|targetlayer|value|view]+'
@@ -43,7 +44,7 @@ export default function(hljs) {
     className: 'subst',
     begin: '\\$\\{', end: '\\}',
     keywords: KEYWORDS,
-    contains: []  // defined later
+    contains: [] // defined later
   };
   var TEMPLATE_STRING = {
     className: 'string',
diff --git a/src/languages/arduino.js b/src/languages/arduino.js
index 8bd1098ce8..823e20395c 100644
--- a/src/languages/arduino.js
+++ b/src/languages/arduino.js
@@ -6,6 +6,7 @@ Requires: cpp.js
 Website: https://www.arduino.cc
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
 
 	var ARDUINO_KW = {
diff --git a/src/languages/armasm.js b/src/languages/armasm.js
index a8b993534a..eadcaf077d 100644
--- a/src/languages/armasm.js
+++ b/src/languages/armasm.js
@@ -5,6 +5,7 @@ Description: ARM Assembly including Thumb and Thumb2 instructions
 Category: assembler
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
     //local labels: %?[FB]?[AT]?\d{1,2}\w+
 
diff --git a/src/languages/asciidoc.js b/src/languages/asciidoc.js
index 84774a937f..419a2d551a 100644
--- a/src/languages/asciidoc.js
+++ b/src/languages/asciidoc.js
@@ -7,6 +7,7 @@ Description: A semantic, text-based document format that can be exported to HTML
 Category: markup
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'AsciiDoc',
diff --git a/src/languages/aspectj.js b/src/languages/aspectj.js
index f12c3c8ecb..5b5d5740da 100644
--- a/src/languages/aspectj.js
+++ b/src/languages/aspectj.js
@@ -4,7 +4,9 @@ Author: Hakan Ozler 
 Website: https://www.eclipse.org/aspectj/
 Description: Syntax Highlighting for the AspectJ Language which is a general-purpose aspect-oriented extension to the Java programming language.
  */
-export default function (hljs) {
+
+/** @type LanguageFn */
+export default function(hljs) {
   var KEYWORDS =
     'false synchronized int abstract float private char boolean static null if const ' +
     'for true while long throw strictfp finally protected import native final return void ' +
diff --git a/src/languages/autohotkey.js b/src/languages/autohotkey.js
index 030715c6fd..94c5d19ad6 100644
--- a/src/languages/autohotkey.js
+++ b/src/languages/autohotkey.js
@@ -5,6 +5,7 @@ Description: AutoHotkey language definition
 Category: scripting
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var BACKTICK_ESCAPE = {
     begin: '`[\\s\\S]'
diff --git a/src/languages/autoit.js b/src/languages/autoit.js
index 8a6f05439e..f5ecd57cf9 100644
--- a/src/languages/autoit.js
+++ b/src/languages/autoit.js
@@ -5,6 +5,7 @@ Description: AutoIt language definition
 Category: scripting
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
     var KEYWORDS = 'ByRef Case Const ContinueCase ContinueLoop ' +
         'Default Dim Do Else ElseIf EndFunc EndIf EndSelect ' +
diff --git a/src/languages/avrasm.js b/src/languages/avrasm.js
index 4d3c5add9c..de2c7a46d9 100644
--- a/src/languages/avrasm.js
+++ b/src/languages/avrasm.js
@@ -5,6 +5,7 @@ Category: assembler
 Website: https://www.microchip.com/webdoc/avrassembler/avrassembler.wb_instruction_list.html
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'AVR Assembly',
diff --git a/src/languages/awk.js b/src/languages/awk.js
index 48ef1de47b..0d61c54cc5 100644
--- a/src/languages/awk.js
+++ b/src/languages/awk.js
@@ -5,6 +5,7 @@ Website: https://www.gnu.org/software/gawk/manual/gawk.html
 Description: language definition for Awk scripts
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var VARIABLE = {
     className: 'variable',
diff --git a/src/languages/axapta.js b/src/languages/axapta.js
index 5f4a3633ea..e91207963b 100644
--- a/src/languages/axapta.js
+++ b/src/languages/axapta.js
@@ -5,6 +5,7 @@ Website: https://dynamics.microsoft.com/en-us/ax-overview/
 Category: enterprise
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Dynamics 365',
diff --git a/src/languages/bash.js b/src/languages/bash.js
index 0e52636449..e199f4f448 100644
--- a/src/languages/bash.js
+++ b/src/languages/bash.js
@@ -6,6 +6,7 @@ Website: https://www.gnu.org/software/bash/
 Category: common
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   const VAR = {};
   const BRACED_VAR = {
diff --git a/src/languages/basic.js b/src/languages/basic.js
index 6b5f57e14b..c37bedac71 100644
--- a/src/languages/basic.js
+++ b/src/languages/basic.js
@@ -5,6 +5,7 @@ Description: Based on the BASIC reference from the Tandy 1000 guide
 Website: https://en.wikipedia.org/wiki/Tandy_1000
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'BASIC',
diff --git a/src/languages/bnf.js b/src/languages/bnf.js
index 6c83904089..d4edc03bc6 100644
--- a/src/languages/bnf.js
+++ b/src/languages/bnf.js
@@ -4,6 +4,7 @@ Website: https://en.wikipedia.org/wiki/Backus–Naur_form
 Author: Oleg Efimov 
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Backus–Naur Form',
diff --git a/src/languages/brainfuck.js b/src/languages/brainfuck.js
index e5d813142b..957d19618b 100644
--- a/src/languages/brainfuck.js
+++ b/src/languages/brainfuck.js
@@ -4,6 +4,7 @@ Author: Evgeny Stepanischev 
 Website: https://esolangs.org/wiki/Brainfuck
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var LITERAL = {
     className: 'literal',
diff --git a/src/languages/c-like.js b/src/languages/c-like.js
index ef795072e9..11fad83a89 100644
--- a/src/languages/c-like.js
+++ b/src/languages/c-like.js
@@ -13,6 +13,7 @@ change in v10 and don't have to change the requirements again later.
 See: https://github.com/highlightjs/highlight.js/issues/2146
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   function optional(s) {
     return '(?:' + s + ')?';
diff --git a/src/languages/c.js b/src/languages/c.js
index 854c9da016..70f0afc9ba 100644
--- a/src/languages/c.js
+++ b/src/languages/c.js
@@ -5,6 +5,7 @@ Website: https://en.wikipedia.org/wiki/C_(programming_language)
 Requires: c-like.js
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
 
   var lang = hljs.getLanguage('c-like').rawDefinition();
diff --git a/src/languages/cal.js b/src/languages/cal.js
index bb00952506..ab7342e2b7 100644
--- a/src/languages/cal.js
+++ b/src/languages/cal.js
@@ -5,6 +5,7 @@ Description: Provides highlighting of Microsoft Dynamics NAV C/AL code files
 Website: https://docs.microsoft.com/en-us/dynamics-nav/programming-in-c-al
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var KEYWORDS =
     'div mod in and or not xor asserterror begin case do downto else end exit for if of repeat then to ' +
diff --git a/src/languages/capnproto.js b/src/languages/capnproto.js
index ecff298499..5fea8fc84f 100644
--- a/src/languages/capnproto.js
+++ b/src/languages/capnproto.js
@@ -6,6 +6,7 @@ Website: https://capnproto.org/capnp-tool.html
 Category: protocols
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Cap’n Proto',
diff --git a/src/languages/ceylon.js b/src/languages/ceylon.js
index 0f7eed3c96..f9b1b7e29c 100644
--- a/src/languages/ceylon.js
+++ b/src/languages/ceylon.js
@@ -3,6 +3,8 @@ Language: Ceylon
 Author: Lucas Werkmeister 
 Website: https://ceylon-lang.org
 */
+
+/** @type LanguageFn */
 export default function(hljs) {
   // 2.3. Identifiers and keywords
   var KEYWORDS =
diff --git a/src/languages/clean.js b/src/languages/clean.js
index a7ee3d96d8..f756675f02 100644
--- a/src/languages/clean.js
+++ b/src/languages/clean.js
@@ -5,6 +5,7 @@ Category: functional
 Website: http://clean.cs.ru.nl
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Clean',
diff --git a/src/languages/clojure-repl.js b/src/languages/clojure-repl.js
index a0fae9a918..6cc1146c6e 100644
--- a/src/languages/clojure-repl.js
+++ b/src/languages/clojure-repl.js
@@ -7,6 +7,7 @@ Website: https://clojure.org
 Category: lisp
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Clojure REPL',
diff --git a/src/languages/clojure.js b/src/languages/clojure.js
index cab6d2593c..bf0d877810 100644
--- a/src/languages/clojure.js
+++ b/src/languages/clojure.js
@@ -6,6 +6,7 @@ Website: https://clojure.org
 Category: lisp
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var SYMBOLSTART = 'a-zA-Z_\\-!.?+*=<>&#\'';
   var SYMBOL_RE = '[' + SYMBOLSTART + '][' + SYMBOLSTART + '0-9/;:]*';
diff --git a/src/languages/cmake.js b/src/languages/cmake.js
index 6fb70f3262..040769c610 100644
--- a/src/languages/cmake.js
+++ b/src/languages/cmake.js
@@ -5,6 +5,7 @@ Author: Igor Kalnitsky 
 Website: https://cmake.org
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'CMake',
diff --git a/src/languages/coffeescript.js b/src/languages/coffeescript.js
index 60c8bb5a4a..458ce3872a 100644
--- a/src/languages/coffeescript.js
+++ b/src/languages/coffeescript.js
@@ -9,6 +9,7 @@ Website: https://coffeescript.org
 
 import * as ECMAScript from "./lib/ecmascript";
 
+/** @type LanguageFn */
 export default function(hljs) {
   var COFFEE_BUILT_INS = [
     'npm',
diff --git a/src/languages/coq.js b/src/languages/coq.js
index 032c16988e..8f8a4e573d 100644
--- a/src/languages/coq.js
+++ b/src/languages/coq.js
@@ -5,6 +5,7 @@ Category: functional
 Website: https://coq.inria.fr
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Coq',
diff --git a/src/languages/cos.js b/src/languages/cos.js
index 18f1c8692a..48ae01b58e 100644
--- a/src/languages/cos.js
+++ b/src/languages/cos.js
@@ -4,6 +4,8 @@ Author: Nikita Savchenko 
 Category: enterprise, scripting
 Website: https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls
 */
+
+/** @type LanguageFn */
 export default function cos (hljs) {
 
   var STRINGS = {
diff --git a/src/languages/cpp.js b/src/languages/cpp.js
index aa41288a14..2f2bd748b6 100644
--- a/src/languages/cpp.js
+++ b/src/languages/cpp.js
@@ -5,8 +5,8 @@ Website: https://isocpp.org
 Requires: c-like.js
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
-
   var lang = hljs.getLanguage('c-like').rawDefinition();
   // return auto-detection back on
   lang.disableAutodetect = false;
diff --git a/src/languages/crmsh.js b/src/languages/crmsh.js
index 72165d1d12..ecbcefe836 100644
--- a/src/languages/crmsh.js
+++ b/src/languages/crmsh.js
@@ -6,6 +6,7 @@ Description: Syntax Highlighting for the crmsh DSL
 Category: config
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var RESOURCES = 'primitive rsc_template';
 
diff --git a/src/languages/crystal.js b/src/languages/crystal.js
index 6cd26ff1be..7230d6cb7b 100644
--- a/src/languages/crystal.js
+++ b/src/languages/crystal.js
@@ -4,6 +4,7 @@ Author: TSUYUSATO Kitsune 
 Website: https://crystal-lang.org
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var INT_SUFFIX = '(_*[ui](8|16|32|64|128))?';
   var FLOAT_SUFFIX = '(_*f(32|64))?';
diff --git a/src/languages/csharp.js b/src/languages/csharp.js
index 8294552c85..a68c265d17 100644
--- a/src/languages/csharp.js
+++ b/src/languages/csharp.js
@@ -6,6 +6,7 @@ Website: https://docs.microsoft.com/en-us/dotnet/csharp/
 Category: common
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var KEYWORDS = {
     keyword:
diff --git a/src/languages/csp.js b/src/languages/csp.js
index b024ec6de3..930c381f3c 100644
--- a/src/languages/csp.js
+++ b/src/languages/csp.js
@@ -7,6 +7,7 @@ Website: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
 vim: ts=2 sw=2 st=2
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'CSP',
diff --git a/src/languages/css.js b/src/languages/css.js
index 85e3dc1222..beabf84543 100644
--- a/src/languages/css.js
+++ b/src/languages/css.js
@@ -4,6 +4,7 @@ Category: common, css
 Website: https://developer.mozilla.org/en-US/docs/Web/CSS
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var FUNCTION_LIKE = {
     begin: /[\w-]+\(/, returnBegin: true,
diff --git a/src/languages/d.js b/src/languages/d.js
index b17be1696f..9f1d8be95e 100644
--- a/src/languages/d.js
+++ b/src/languages/d.js
@@ -23,6 +23,7 @@ Date: 2012-04-08
  *   up to the end of line is matched as special token sequence)
  */
 
+/** @type LanguageFn */
 export default function(hljs) {
   /**
    * Language keywords
diff --git a/src/languages/delphi.js b/src/languages/delphi.js
index b72d3bc2d2..334dddaae4 100644
--- a/src/languages/delphi.js
+++ b/src/languages/delphi.js
@@ -3,6 +3,7 @@ Language: Delphi
 Website: https://www.embarcadero.com/products/delphi
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   var KEYWORDS =
     'exports register file shl array record property for mod while set ally label uses raise not ' +
diff --git a/src/languages/diff.js b/src/languages/diff.js
index 213ff264ce..4bcbe27540 100644
--- a/src/languages/diff.js
+++ b/src/languages/diff.js
@@ -6,6 +6,7 @@ Website: https://www.gnu.org/software/diffutils/
 Category: common
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'Diff',
diff --git a/src/languages/dns.js b/src/languages/dns.js
index 0092c07c60..aa4f29a612 100644
--- a/src/languages/dns.js
+++ b/src/languages/dns.js
@@ -5,6 +5,7 @@ Category: config
 Website: https://en.wikipedia.org/wiki/Zone_file
 */
 
+/** @type LanguageFn */
 export default function(hljs) {
   return {
     name: 'DNS Zone',
diff --git a/src/languages/erlang-repl.js b/src/languages/erlang-repl.js
index 6ba6e0ed49..5d1e6679e9 100644
--- a/src/languages/erlang-repl.js
+++ b/src/languages/erlang-repl.js
@@ -23,7 +23,7 @@ export default function(hljs) {
       hljs.COMMENT('%', '$'),
       {
         className: 'number',
-        begin: '\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)',
+        begin: '\\b(\\d+(_\\d+)*#[a-fA-F0-9]+(_[a-fA-F0-9]+)*|\\d+(_\\d+)*(\\.\\d+(_\\d+)*)?([eE][-+]?\\d+)?)',
         relevance: 0
       },
       hljs.APOS_STRING_MODE,
diff --git a/src/languages/erlang.js b/src/languages/erlang.js
index 2cec6aecf5..e37b94ea53 100644
--- a/src/languages/erlang.js
+++ b/src/languages/erlang.js
@@ -20,7 +20,7 @@ export default function(hljs) {
   var COMMENT = hljs.COMMENT('%', '$');
   var NUMBER = {
     className: 'number',
-    begin: '\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)',
+    begin: '\\b(\\d+(_\\d+)*#[a-fA-F0-9]+(_[a-fA-F0-9]+)*|\\d+(_\\d+)*(\\.\\d+(_\\d+)*)?([eE][-+]?\\d+)?)',
     relevance: 0
   };
   var NAMED_FUN = {
diff --git a/src/lib/html_renderer.js b/src/lib/html_renderer.js
index bd6bb20307..9e98a07ab4 100644
--- a/src/lib/html_renderer.js
+++ b/src/lib/html_renderer.js
@@ -1,23 +1,53 @@
 import { escapeHTML } from './utils';
 
+/**
+ * @typedef {object} Renderer
+ * @property {(text: string) => void} addText
+ * @property {(node: Node) => void} openNode
+ * @property {(node: Node) => void} closeNode
+ * @property {() => string} value
+ */
+
+/** @typedef {{kind?: string, sublanguage?: boolean}} Node */
+/** @typedef {{walk: (r: Renderer) => void}} Tree */
+/** */
+
 const SPAN_CLOSE = '';
+
+/**
+ * Determines if a node needs to be wrapped in 
+ *
+ * @param {Node} node */
 const emitsWrappingTags = (node) => {
   return !!node.kind;
 };
 
+/** @type {Renderer} */
 export default class HTMLRenderer {
-  constructor(tree, options) {
+  /**
+   * Creates a new HTMLRenderer
+   *
+   * @param {Tree} parseTree - the parse tree (must support `walk` API)
+   * @param {{classPrefix: string}} options
+   */
+  constructor(parseTree, options) {
     this.buffer = "";
     this.classPrefix = options.classPrefix;
-    tree.walk(this);
+    parseTree.walk(this);
   }
 
-  // renderer API
-
+  /**
+   * Adds texts to the output stream
+   *
+   * @param {string} text */
   addText(text) {
     this.buffer += escapeHTML(text);
   }
 
+  /**
+   * Adds a node open to the output stream (if needed)
+   *
+   * @param {Node} node */
   openNode(node) {
     if (!emitsWrappingTags(node)) return;
 
@@ -28,19 +58,31 @@ export default class HTMLRenderer {
     this.span(className);
   }
 
+  /**
+   * Adds a node close to the output stream (if needed)
+   *
+   * @param {Node} node */
   closeNode(node) {
     if (!emitsWrappingTags(node)) return;
 
     this.buffer += SPAN_CLOSE;
   }
 
+  /**
+   * returns the accumulated buffer
+  */
+  value() {
+    return this.buffer;
+  }
+
   // helpers
 
+  /**
+   * Builds a span element
+   *
+   * @param {string} className */
   span(className) {
     this.buffer += ``;
   }
 
-  value() {
-    return this.buffer;
-  }
 }
diff --git a/src/lib/mode_compiler.js b/src/lib/mode_compiler.js
index 8e63eeba17..063283b173 100644
--- a/src/lib/mode_compiler.js
+++ b/src/lib/mode_compiler.js
@@ -6,8 +6,21 @@ var COMMON_KEYWORDS = 'of and for in not or if then'.split(' ');
 
 // compilation
 
+/**
+ * Compiles a language definition result
+ *
+ * Given the raw result of a language definition (Language), compiles this so
+ * that it is ready for highlighting code.
+ * @param {Language} language
+ * @returns {CompiledLanguage}
+ */
 export function compileLanguage(language) {
-
+  /**
+   * Builds a regex with the case sensativility of the current language
+   *
+   * @param {RegExp | string} value
+   * @param {boolean} [global]
+   */
   function langRe(value, global) {
     return new RegExp(
       regex.source(value),
@@ -31,13 +44,16 @@ export function compileLanguage(language) {
   class MultiRegex {
     constructor() {
       this.matchIndexes = {};
+      // @ts-ignore
       this.regexes = [];
       this.matchAt = 1;
       this.position = 0;
     }
 
+    // @ts-ignore
     addRule(re, opts) {
       opts.position = this.position++;
+      // @ts-ignore
       this.matchIndexes[this.matchAt] = opts;
       this.regexes.push([opts, re]);
       this.matchAt += regex.countMatchGroups(re) + 1;
@@ -46,13 +62,15 @@ export function compileLanguage(language) {
     compile() {
       if (this.regexes.length === 0) {
         // avoids the need to check length every time exec is called
+        // @ts-ignore
         this.exec = () => null;
       }
       const terminators = this.regexes.map(el => el[1]);
-      this.matcherRe = langRe(regex.join(terminators, '|'), true);
+      this.matcherRe = langRe(regex.join(terminators), true);
       this.lastIndex = 0;
     }
 
+    /** @param {string} s */
     exec(s) {
       this.matcherRe.lastIndex = this.lastIndex;
       const match = this.matcherRe.exec(s);
@@ -60,6 +78,7 @@ export function compileLanguage(language) {
 
       // eslint-disable-next-line no-undefined
       const i = match.findIndex((el, i) => i > 0 && el !== undefined);
+      // @ts-ignore
       const matchData = this.matchIndexes[i];
       // trim off any earlier non-relevant match groups (ie, the other regex
       // match groups that make up the multi-matcher)
@@ -102,7 +121,9 @@ export function compileLanguage(language) {
   */
   class ResumableMultiRegex {
     constructor() {
+      // @ts-ignore
       this.rules = [];
+      // @ts-ignore
       this.multiRegexes = [];
       this.count = 0;
 
@@ -110,6 +131,7 @@ export function compileLanguage(language) {
       this.regexIndex = 0;
     }
 
+    // @ts-ignore
     getMatcher(index) {
       if (this.multiRegexes[index]) return this.multiRegexes[index];
 
@@ -124,11 +146,13 @@ export function compileLanguage(language) {
       this.regexIndex = 0;
     }
 
+    // @ts-ignore
     addRule(re, opts) {
       this.rules.push([re, opts]);
       if (opts.type === "begin") this.count++;
     }
 
+    /** @param {string} s */
     exec(s) {
       const m = this.getMatcher(this.regexIndex);
       m.lastIndex = this.lastIndex;
@@ -145,6 +169,13 @@ export function compileLanguage(language) {
     }
   }
 
+  /**
+   * Given a mode, builds a huge ResumableMultiRegex that can be used to walk
+   * the content and find matches.
+   *
+   * @param {CompiledMode} mode
+   * @returns {ResumableMultiRegex}
+   */
   function buildModeRegex(mode) {
     const mm = new ResumableMultiRegex();
 
@@ -161,11 +192,20 @@ export function compileLanguage(language) {
   }
 
   // TODO: We need negative look-behind support to do this properly
-  function skipIfhasPrecedingOrTrailingDot(match, resp) {
+  /**
+   * Skip a match if it has a preceding or trailing dot
+   *
+   * This is used for `beginKeywords` to prevent matching expressions such as
+   * `bob.keyword.do()`. The mode compiler automatically wires this up as a
+   * special _internal_ 'on:begin' callback for modes with `beginKeywords`
+   * @param {RegExpMatchArray} match
+   * @param {CallbackResponse} response
+   */
+  function skipIfhasPrecedingOrTrailingDot(match, response) {
     const before = match.input[match.index - 1];
     const after = match.input[match.index + match[0].length];
     if (before === "." || after === ".") {
-      resp.ignoreMatch();
+      response.ignoreMatch();
     }
   }
 
@@ -199,8 +239,18 @@ export function compileLanguage(language) {
    *             - The parser cursor is not moved forward.
    */
 
+  /**
+   * Compiles an individual mode
+   *
+   * This can raise an error if the mode contains certain detectable known logic
+   * issues.
+   * @param {Mode} mode
+   * @param {CompiledMode | null} [parent]
+   * @returns {CompiledMode | never}
+   */
   function compileMode(mode, parent) {
-    if (mode.compiled) return;
+    const cmode = /** @type CompiledMode */ (mode);
+    if (mode.compiled) return cmode;
     mode.compiled = true;
 
     // __beforeBegin is considered private API, internal use only
@@ -225,7 +275,7 @@ export function compileLanguage(language) {
 
     // `mode.lexemes` was the old standard before we added and now recommend
     // using `keywords.$pattern` to pass the keyword pattern
-    mode.keywordPatternRe = langRe(mode.lexemes || kw_pattern || /\w+/, true);
+    cmode.keywordPatternRe = langRe(mode.lexemes || kw_pattern || /\w+/, true);
 
     if (parent) {
       if (mode.beginKeywords) {
@@ -237,51 +287,68 @@ export function compileLanguage(language) {
         mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?=\\b|\\s)';
         mode.__beforeBegin = skipIfhasPrecedingOrTrailingDot;
       }
-      if (!mode.begin)
-        mode.begin = /\B|\b/;
-      mode.beginRe = langRe(mode.begin);
-      if (mode.endSameAsBegin)
-        mode.end = mode.begin;
-      if (!mode.end && !mode.endsWithParent)
-        mode.end = /\B|\b/;
-      if (mode.end)
-        mode.endRe = langRe(mode.end);
-      mode.terminator_end = regex.source(mode.end) || '';
-      if (mode.endsWithParent && parent.terminator_end)
-        mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
-    }
-    if (mode.illegal)
-      mode.illegalRe = langRe(mode.illegal);
-    if (mode.relevance == null)
-      mode.relevance = 1;
-    if (!mode.contains) {
-      mode.contains = [];
+      if (!mode.begin) mode.begin = /\B|\b/;
+      cmode.beginRe = langRe(mode.begin);
+      if (mode.endSameAsBegin) mode.end = mode.begin;
+      if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/;
+      if (mode.end) cmode.endRe = langRe(mode.end);
+      cmode.terminator_end = regex.source(mode.end) || '';
+      if (mode.endsWithParent && parent.terminator_end) {
+        cmode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end;
+      }
     }
+    if (mode.illegal) cmode.illegalRe = langRe(mode.illegal);
+    // eslint-disable-next-line no-undefined
+    if (mode.relevance === undefined) mode.relevance = 1;
+    if (!mode.contains) mode.contains = [];
+
     mode.contains = [].concat(...mode.contains.map(function(c) {
       return expand_or_clone_mode(c === 'self' ? mode : c);
     }));
-    mode.contains.forEach(function(c) { compileMode(c, mode); });
+    mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); });
 
     if (mode.starts) {
       compileMode(mode.starts, parent);
     }
 
-    mode.matcher = buildModeRegex(mode);
+    cmode.matcher = buildModeRegex(cmode);
+    return cmode;
   }
 
   // self is not valid at the top-level
   if (language.contains && language.contains.includes('self')) {
     throw new Error("ERR: contains `self` is not supported at the top-level of a language.  See documentation.");
   }
-  compileMode(language);
+  return compileMode(/** @type Mode */ (language));
 }
 
+/**
+ * Determines if a mode has a dependency on it's parent or not
+ *
+ * If a mode does have a parent dependency then often we need to clone it if
+ * it's used in multiple places so that each copy points to the correct parent,
+ * where-as modes without a parent can often safely be re-used at the bottom of
+ * a mode chain.
+ *
+ * @param {Mode | null} mode
+ * @returns {boolean} - is there a dependency on the parent?
+ * */
 function dependencyOnParent(mode) {
   if (!mode) return false;
 
   return mode.endsWithParent || dependencyOnParent(mode.starts);
 }
 
+/**
+ * Expands a mode or clones it if necessary
+ *
+ * This is necessary for modes with parental dependenceis (see notes on
+ * `dependencyOnParent`) and for nodes that have `variants` - which must then be
+ * exploded into their own individual modes at compile time.
+ *
+ * @param {Mode} mode
+ * @returns {Mode | Mode[]}
+ * */
 function expand_or_clone_mode(mode) {
   if (mode.variants && !mode.cached_variants) {
     mode.cached_variants = mode.variants.map(function(variant) {
@@ -312,9 +379,18 @@ function expand_or_clone_mode(mode) {
   return mode;
 }
 
-// keywords
+/***********************************************
+  Keywords
+***********************************************/
 
+/**
+ * Given raw keywords from a language definition, compile them.
+ *
+ * @param {string | Record} rawKeywords
+ * @param {boolean} case_insensitive
+ */
 function compileKeywords(rawKeywords, case_insensitive) {
+  /** @type KeywordDict */
   var compiled_keywords = {};
 
   if (typeof rawKeywords === 'string') { // string
@@ -328,17 +404,33 @@ function compileKeywords(rawKeywords, case_insensitive) {
 
   // ---
 
-  function splitAndCompile(className, str) {
+  /**
+   * Compiles an individual list of keywords
+   *
+   * Ex: "for if when while|5"
+   *
+   * @param {string} className
+   * @param {string} keywordList
+   */
+  function splitAndCompile(className, keywordList) {
     if (case_insensitive) {
-      str = str.toLowerCase();
+      keywordList = keywordList.toLowerCase();
     }
-    str.split(' ').forEach(function(keyword) {
+    keywordList.split(' ').forEach(function(keyword) {
       var pair = keyword.split('|');
       compiled_keywords[pair[0]] = [className, scoreForKeyword(pair[0], pair[1])];
     });
   }
 }
 
+/**
+ * Returns the proper score for a given keyword
+ *
+ * Also takes into account comment keywords, which will be scored 0 UNLESS
+ * another score has been manually assigned.
+ * @param {string} keyword
+ * @param {string} [providedScore]
+ */
 function scoreForKeyword(keyword, providedScore) {
   // manual scores always win over common keywords
   // so you can force a score of 1 if you really insist
@@ -349,6 +441,10 @@ function scoreForKeyword(keyword, providedScore) {
   return commonKeyword(keyword) ? 0 : 1;
 }
 
-function commonKeyword(word) {
-  return COMMON_KEYWORDS.includes(word.toLowerCase());
+/**
+ * Determines if a given keyword is common or not
+ *
+ * @param {string} keyword */
+function commonKeyword(keyword) {
+  return COMMON_KEYWORDS.includes(keyword.toLowerCase());
 }
diff --git a/src/lib/modes.js b/src/lib/modes.js
index 669968d58f..33d18a9105 100644
--- a/src/lib/modes.js
+++ b/src/lib/modes.js
@@ -9,6 +9,9 @@ export const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+
 export const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
 export const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
 
+/**
+* @param { Partial & {binary?: string | RegExp} } opts
+*/
 export const SHEBANG = (opts = {}) => {
   const beginShebang = /^#![ ]*\//;
   if (opts.binary) {
@@ -23,6 +26,7 @@ export const SHEBANG = (opts = {}) => {
     begin: beginShebang,
     end: /$/,
     relevance: 0,
+    /** @type {ModeCallback} */
     "on:begin": (m, resp) => {
       if (m.index !== 0) resp.ignoreMatch();
     }
@@ -50,15 +54,23 @@ export const QUOTE_STRING_MODE = {
 export const PHRASAL_WORDS_MODE = {
   begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
 };
-export const COMMENT = function(begin, end, inherits) {
+/**
+ * Creates a comment mode
+ *
+ * @param {string | RegExp} begin
+ * @param {string | RegExp} end
+ * @param {Mode | {}} [modeOptions]
+ * @returns {Partial}
+ */
+export const COMMENT = function(begin, end, modeOptions = {}) {
   var mode = inherit(
     {
       className: 'comment',
-      begin: begin,
-      end: end,
+      begin,
+      end,
       contains: []
     },
-    inherits || {}
+    modeOptions
   );
   mode.contains.push(PHRASAL_WORDS_MODE);
   mode.contains.push({
@@ -139,10 +151,19 @@ export const METHOD_GUARD = {
   relevance: 0
 };
 
+/**
+ * Adds end same as begin mechanics to a mode
+ *
+ * Your mode must include at least a single () match group as that first match
+ * group is what is used for comparison
+ * @param {Partial} mode
+ */
 export const END_SAME_AS_BEGIN = function(mode) {
   return Object.assign(mode,
     {
-    'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; },
-    'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch() }
+      /** @type {ModeCallback} */
+      'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; },
+      /** @type {ModeCallback} */
+      'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); }
     });
 };
diff --git a/src/lib/regex.js b/src/lib/regex.js
index 80a9ce1b70..1727cb57b9 100644
--- a/src/lib/regex.js
+++ b/src/lib/regex.js
@@ -1,26 +1,52 @@
+/**
+ * @param {string} value
+ * @returns {RegExp}
+ * */
 export function escape(value) {
   return new RegExp(value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm');
 }
 
+/**
+ * @param {RegExp | string } re
+ * @returns {string}
+ */
 export function source(re) {
-  // if it's a regex get it's source,
-  // otherwise it's a string already so just return it
-  return (re && re.source) || re;
+  if (!re) return null;
+  if (typeof re === "string") return re;
+
+  return re.source;
 }
 
-export function lookahead(regex) {
-  return concat('(?=', regex, ')');
+/**
+ * @param {RegExp | string } re
+ * @returns {string}
+ */
+export function lookahead(re) {
+  return concat('(?=', re, ')');
 }
 
+/**
+ * @param {(RegExp | string)[] } args
+ * @returns {string}
+ */
 export function concat(...args) {
   const joined = args.map((x) => source(x)).join("");
   return joined;
 }
 
+/**
+ * @param {RegExp} re
+ * @returns {number}
+ */
 export function countMatchGroups(re) {
   return (new RegExp(re.toString() + '|')).exec('').length - 1;
 }
 
+/**
+ * Does lexeme start with a regular expression match at the beginning
+ * @param {RegExp} re
+ * @param {string} lexeme
+ */
 export function startsWith(re, lexeme) {
   var match = re && re.exec(lexeme);
   return match && match.index === 0;
@@ -31,7 +57,12 @@ export function startsWith(re, lexeme) {
 // it also places each individual regular expression into it's own
 // match group, keeping track of the sequencing of those match groups
 // is currently an exercise for the caller. :-)
-export function join(regexps, separator) {
+/**
+ * @param {(string | RegExp)[]} regexps
+ * @param {string} separator
+ * @returns {string}
+ */
+export function join(regexps, separator = "|") {
   // backreferenceRe matches an open parenthesis or backreference. To avoid
   // an incorrect parse, it additionally matches the following:
   // - [...] elements, where the meaning of parentheses and escapes change
diff --git a/src/lib/response.js b/src/lib/response.js
index 9c5bcfa95c..c49c6df59f 100644
--- a/src/lib/response.js
+++ b/src/lib/response.js
@@ -1,7 +1,11 @@
 export default class Response {
+  /**
+   * @param {CompiledMode} mode
+   */
   constructor(mode) {
-    if (mode.data === undefined)
-      mode.data = {};
+    // eslint-disable-next-line no-undefined
+    if (mode.data === undefined) mode.data = {};
+
     this.data = mode.data;
   }
 
diff --git a/src/lib/token_tree.js b/src/lib/token_tree.js
index f3f851f29e..6e2fba63b2 100644
--- a/src/lib/token_tree.js
+++ b/src/lib/token_tree.js
@@ -1,7 +1,12 @@
 import HTMLRenderer from './html_renderer';
 
+/** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} | string} Node */
+/** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} } DataNode */
+/**  */
+
 class TokenTree {
   constructor() {
+    /** @type DataNode */
     this.rootNode = { children: [] };
     this.stack = [this.rootNode];
   }
@@ -12,11 +17,14 @@ class TokenTree {
 
   get root() { return this.rootNode; }
 
+  /** @param {Node} node */
   add(node) {
     this.top.children.push(node);
   }
 
+  /** @param {string} kind */
   openNode(kind) {
+    /** @type Node */
     const node = { kind, children: [] };
     this.add(node);
     this.stack.push(node);
@@ -26,6 +34,8 @@ class TokenTree {
     if (this.stack.length > 1) {
       return this.stack.pop();
     }
+    // eslint-disable-next-line no-undefined
+    return undefined;
   }
 
   closeAllNodes() {
@@ -36,10 +46,21 @@ class TokenTree {
     return JSON.stringify(this.rootNode, null, 4);
   }
 
+  /**
+   * @typedef { import("./html_renderer").Renderer } Renderer
+   * @param {Renderer} builder
+   */
   walk(builder) {
+    // this does not
     return this.constructor._walk(builder, this.rootNode);
+    // this works
+    // return TokenTree._walk(builder, this.rootNode);
   }
 
+  /**
+   * @param {Renderer} builder
+   * @param {Node} node
+   */
   static _walk(builder, node) {
     if (typeof node === "string") {
       builder.addText(node);
@@ -51,16 +72,19 @@ class TokenTree {
     return builder;
   }
 
+  /**
+   * @param {Node} node
+   */
   static _collapse(node) {
-    if (!node.children) {
-      return;
-    }
+    if (typeof node === "string") return;
+    if (!node.children) return;
+
     if (node.children.every(el => typeof el === "string")) {
-      node.text = node.children.join("");
-      delete node.children;
+      // node.text = node.children.join("");
+      // delete node.children;
+      node.children = [node.children.join("")];
     } else {
       node.children.forEach((child) => {
-        if (typeof child === "string") return;
         TokenTree._collapse(child);
       });
     }
@@ -83,12 +107,23 @@ class TokenTree {
   - toHTML()
 
 */
+
+/**
+ * @implements {Emitter}
+ */
 export default class TokenTreeEmitter extends TokenTree {
+  /**
+   * @param {*} options
+   */
   constructor(options) {
     super();
     this.options = options;
   }
 
+  /**
+   * @param {string} text
+   * @param {string} kind
+   */
   addKeyword(text, kind) {
     if (text === "") { return; }
 
@@ -97,13 +132,21 @@ export default class TokenTreeEmitter extends TokenTree {
     this.closeNode();
   }
 
+  /**
+   * @param {string} text
+   */
   addText(text) {
     if (text === "") { return; }
 
     this.add(text);
   }
 
+  /**
+   * @param {Emitter & {root: DataNode}} emitter
+   * @param {string} name
+   */
   addSublanguage(emitter, name) {
+    /** @type DataNode */
     const node = emitter.root;
     node.kind = name;
     node.sublanguage = true;
diff --git a/src/lib/utils.js b/src/lib/utils.js
index d727c46e66..3b9a0f1069 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -1,3 +1,7 @@
+/**
+ * @param {string} value
+ * @returns {string}
+ */
 export function escapeHTML(value) {
   return value
     .replace(/&/g, '&')
@@ -10,31 +14,47 @@ export function escapeHTML(value) {
 /**
  * performs a shallow merge of multiple objects into one
  *
- * @arguments list of objects with properties to merge
- * @returns a single new object
+ * @template T
+ * @param {T} original
+ * @param {Record[]} objects
+ * @returns {T} a single new object
  */
-export function inherit(parent) { // inherit(parent, override_obj, override_obj, ...)
+export function inherit(original, ...objects) {
+  /** @type Record */
   var result = {};
-  var objects = Array.prototype.slice.call(arguments, 1);
 
-  for (const key in parent) {
-    result[key] = parent[key];
+  for (const key in original) {
+    result[key] = original[key];
   }
   objects.forEach(function(obj) {
     for (const key in obj) {
       result[key] = obj[key];
     }
   });
-  return result;
+  return /** @type {T} */ (result);
 }
 
 /* Stream merging */
 
+/**
+ * @typedef Event
+ * @property {'start'|'stop'} event
+ * @property {number} offset
+ * @property {Node} node
+ */
+
+/**
+ * @param {Node} node
+ */
 function tag(node) {
   return node.nodeName.toLowerCase();
 }
 
+/**
+ * @param {Node} node
+ */
 export function nodeStream(node) {
+  /** @type Event[] */
   var result = [];
   (function _nodeStream(node, offset) {
     for (var child = node.firstChild; child; child = child.nextSibling) {
@@ -64,6 +84,11 @@ export function nodeStream(node) {
   return result;
 }
 
+/**
+ * @param {any} original - the original stream
+ * @param {any} highlighted - stream of the highlighted source
+ * @param {string} value - the original source itself
+ */
 export function mergeStreams(original, highlighted, value) {
   var processed = 0;
   var result = '';
@@ -95,17 +120,28 @@ export function mergeStreams(original, highlighted, value) {
     return highlighted[0].event === 'start' ? original : highlighted;
   }
 
+  /**
+   * @param {Node} node
+   */
   function open(node) {
-    function attr_str(a) {
-      return ' ' + a.nodeName + '="' + escapeHTML(a.value) + '"';
+    /** @param {Attr} attr */
+    function attr_str(attr) {
+      return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"';
     }
+    // @ts-ignore
     result += '<' + tag(node) + [].map.call(node.attributes, attr_str).join('') + '>';
   }
 
+  /**
+   * @param {Node} node
+   */
   function close(node) {
     result += '';
   }
 
+  /**
+   * @param {Event} event
+   */
   function render(event) {
     (event.event === 'start' ? open : close)(event.node);
   }
diff --git a/src/vendor/deep_freeze.js b/src/vendor/deep_freeze.js
index 0bc2c2d003..c5ce3eb560 100644
--- a/src/vendor/deep_freeze.js
+++ b/src/vendor/deep_freeze.js
@@ -1,20 +1,21 @@
 // https://github.com/substack/deep-freeze/blob/master/index.js
-export default function deepFreeze (o) {
-  Object.freeze(o);
+/** @param {any} obj */
+export default function deepFreeze(obj) {
+  Object.freeze(obj);
 
-  var objIsFunction = typeof o === 'function';
+  var objIsFunction = typeof obj === 'function';
 
-  Object.getOwnPropertyNames(o).forEach(function (prop) {
-    if (o.hasOwnProperty(prop)
-    && o[prop] !== null
-    && (typeof o[prop] === "object" || typeof o[prop] === "function")
+  Object.getOwnPropertyNames(obj).forEach(function(prop) {
+    if (Object.hasOwnProperty.call(obj, prop)
+    && obj[prop] !== null
+    && (typeof obj[prop] === "object" || typeof obj[prop] === "function")
     // IE11 fix: https://github.com/highlightjs/highlight.js/issues/2318
     // TODO: remove in the future
     && (objIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true)
-    && !Object.isFrozen(o[prop])) {
-      deepFreeze(o[prop]);
+    && !Object.isFrozen(obj[prop])) {
+      deepFreeze(obj[prop]);
     }
   });
 
-  return o;
-};
+  return obj;
+}
diff --git a/test/markup/erlang/numbers.expect.txt b/test/markup/erlang/numbers.expect.txt
new file mode 100644
index 0000000000..9fdb67b1ae
--- /dev/null
+++ b/test/markup/erlang/numbers.expect.txt
@@ -0,0 +1,14 @@
+Integer = 1234
+BigInteger = 1_234_000
+NegInteger = -20_000
+Float = 2.34
+BigFloat = 3_333.14159_26535_89793
+SciFloat = 2.4e23
+PlusSciFloat = 2.4e+23
+SmallSciFloat = 2.4e-23
+Binary = 2#1010
+StrangeBinary = 2#1010_1010_1010
+Octal = 8#777
+StrangeOctal = 8#777_666_555
+Hex = 16#1ABEF
+StrangeHex = 16#1234_FACE_987D
diff --git a/test/markup/erlang/numbers.txt b/test/markup/erlang/numbers.txt
new file mode 100644
index 0000000000..d14aff8819
--- /dev/null
+++ b/test/markup/erlang/numbers.txt
@@ -0,0 +1,14 @@
+Integer = 1234
+BigInteger = 1_234_000
+NegInteger = -20_000
+Float = 2.34
+BigFloat = 3_333.14159_26535_89793
+SciFloat = 2.4e23
+PlusSciFloat = 2.4e+23
+SmallSciFloat = 2.4e-23
+Binary = 2#1010
+StrangeBinary = 2#1010_1010_1010
+Octal = 8#777
+StrangeOctal = 8#777_666_555
+Hex = 16#1ABEF
+StrangeHex = 16#1234_FACE_987D
diff --git a/test/markup/index.js b/test/markup/index.js
index 12686c25c5..e2461a3311 100644
--- a/test/markup/index.js
+++ b/test/markup/index.js
@@ -6,6 +6,8 @@ const hljs = require('../../build');
 const path = require('path');
 const utility = require('../utility');
 
+hljs.debugMode();
+
 const { getThirdPartyPackages } = require("../../tools/lib/external_language")
 
 function testLanguage(language, {testDir}) {
diff --git a/tools/build_node.js b/tools/build_node.js
index 2e258a1c9b..ed30b5d675 100644
--- a/tools/build_node.js
+++ b/tools/build_node.js
@@ -75,9 +75,11 @@ async function buildNode(options) {
   mkdir("lib/languages");
   mkdir("scss");
   mkdir("styles");
+  mkdir("types");
 
   install("./LICENSE", "LICENSE");
   install("./README.md","README.md");
+  install("./types/index.d.ts","types/index.d.ts");
 
   log("Writing styles.");
   const styles = await fs.readdir("./src/styles/");
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..10bdf3ba1e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,64 @@
+{
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "es2016",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
+    "module": "es2015",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    "allowJs": true,                       /* Allow javascript files to be compiled. */
+    "checkJs": true,                       /* Report errors in .js files. */
+    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    // "outFile": "./",                       /* Concatenate and emit output to single file. */
+    "outDir": "./build",                        /* Redirect output structure to the directory. */
+    // "rootDir": "./src",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                     /* Enable project compilation */
+    // "removeComments": true,                /* Do not emit comments to output. */
+    // "noEmit": true,                        /* Do not emit outputs. */
+    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": false,                           /* Enable all strict type-checking options. */
+    "noImplicitAny": true,                   /* Raise error on expressions and declarations with an implied 'any' type. */
+    "strictNullChecks": false,              /* Enable strict null checks. */
+    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    "noUnusedLocals": true,                /* Report errors on unused locals. */
+    "noUnusedParameters": true,            /* Report errors on unused parameters. */
+    "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
+    "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
+
+    /* Module Resolution Options */
+    "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": ["./"],                       /* List of folders to include type definitions from. */
+    "types": [
+      "./types/index"
+      ],                           /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+    "resolveJsonModule": true,
+
+    /* Source Map Options */
+    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */,
+
+  },
+  "include": ["src/**/*", "tests/**/*"]
+}
diff --git a/types/index.d.ts b/types/index.d.ts
new file mode 100644
index 0000000000..b9eb0d317a
--- /dev/null
+++ b/types/index.d.ts
@@ -0,0 +1,212 @@
+/* Public API */
+
+// eslint-disable-next-line
+declare const hljs : HLJSApi;
+
+interface HLJSApi {
+    highlight: (languageName: string, code: string, ignoreIllegals?: boolean, continuation?: Mode) => HighlightResult
+    highlightAuto: (code: string, languageSubset?: string[]) => AutoHighlightResult
+    fixMarkup: (html: string) => string
+    highlightBlock: (element: HTMLElement) => void
+    configure: (options: Partial) => void
+    initHighlighting: () => void
+    initHighlightingOnLoad: () => void
+    registerLanguage: (languageName: string, language: LanguageFn) => void
+    listLanguages: () => string[]
+    registerAliases: (aliasList: string | string[], { languageName } : {languageName: string}) => void
+    getLanguage: (languageName: string) => Language | undefined
+    requireLanguage: (languageName: string) => Language | never
+    autoDetection: (languageName: string) => boolean
+    inherit: (original: T, ...args: Record[]) => T
+    addPlugin: (plugin: HLJSPlugin) => void
+    debugMode: () => void
+    safeMode: () => void
+    versionString: string
+}
+
+interface HLJSApi {
+    SHEBANG: (mode?: Partial & {binary?: string | RegExp}) => Mode
+    BACKSLASH_ESCAPE: Mode
+    QUOTE_STRING_MODE: Mode
+    APOS_STRING_MODE: Mode
+    PHRASAL_WORDS_MODE: Mode
+    COMMENT: (begin: string | RegExp, end: string | RegExp, modeOpts?: Mode | {}) => Mode
+    C_LINE_COMMENT_MODE: Mode
+    C_BLOCK_COMMENT_MODE: Mode
+    HASH_COMMENT_MODE: Mode
+    NUMBER_MODE: Mode
+    C_NUMBER_MODE: Mode
+    BINARY_NUMBER_MODE: Mode
+    CSS_NUMBER_MODE: Mode
+    REGEXP_MODE: Mode
+    TITLE_MODE: Mode
+    UNDERSCORE_TITLE_MODE: Mode
+    METHOD_GUARD: Mode
+    END_SAME_AS_BEGIN: (mode: Mode) => Mode
+    // build in regex
+    IDENT_RE: string
+    UNDERSCORE_IDENT_RE: string
+    NUMBER_RE: string
+    C_NUMBER_RE: string
+    BINARY_NUMBER_RE: string
+    RE_STARTERS_RE: string
+}
+
+type LanguageFn = (hljs: HLJSApi) => Language
+
+// interface RawLanguage {
+//     name?: string
+//     aliases?: string[]
+//     rawDefinition?: () => Language
+// }
+
+interface HighlightResult {
+    relevance : number
+    value : string
+    language? : string
+    emitter : Emitter
+    illegal : boolean
+    top? : Language | CompiledMode
+    illegalBy? : illegalData
+    sofar? : string
+    errorRaised? : Error
+    // * for auto-highlight
+    second_best? : Omit
+}
+
+interface illegalData {
+    msg: string
+    context: string
+    mode: CompiledMode
+}
+
+interface AutoHighlightResult extends HighlightResult {
+}
+
+type PluginEvent =
+    'before:highlight'
+    | 'after:highlight'
+    | 'before:highlightBlock'
+    | 'after:highlightBlock'
+
+type HLJSPlugin = { [K in PluginEvent]? : any }
+
+interface EmitterConstructor {
+    new (opts: any): Emitter
+}
+
+interface HLJSOptions {
+   noHighlightRe: RegExp
+   languageDetectRe: RegExp
+   classPrefix: string
+   tabReplace?: string
+   useBR: boolean
+   languages?: string[]
+   __emitter: EmitterConstructor
+}
+
+interface CallbackResponse {
+    data: Record
+    ignoreMatch: () => void
+}
+
+/************
+ PRIVATE API
+ ************/
+
+/* for jsdoc annotations in the JS source files */
+
+type AnnotatedError = Error & {mode?: Mode | Language, languageName?: string, badRule?: Mode}
+
+type ModeCallback = (match: RegExpMatchArray, response: CallbackResponse) => void
+type HighlightedHTMLElement = HTMLElement & {result?: object, second_best?: object, parentNode: HTMLElement}
+type EnhancedMatch = RegExpMatchArray & {rule: CompiledMode, type: MatchType}
+type MatchType = "begin" | "end" | "illegal"
+
+ interface Emitter {
+    addKeyword(text: string, kind: string): void
+    addText(text: string): void
+    toHTML(): string
+    finalize(): void
+    closeAllNodes(): void
+    openNode(kind: string): void
+    closeNode(): void
+    addSublanguage(emitter: Emitter, subLanguageName: string): void
+ }
+
+/* modes */
+
+ interface ModeCallbacks {
+     "on:end"?: Function,
+     "on:begin"?: Function,
+ }
+
+interface Mode extends ModeCallbacks, ModeDetails {
+
+}
+
+interface LanguageDetail {
+    name?: string
+    rawDefinition?: () => Language
+    aliases?: string[]
+    disableAutodetect?: boolean
+    contains: ("self"|Mode)[]
+    case_insensitive?: boolean
+    keywords?: Record | string
+    compiled?: boolean
+}
+
+type Language = LanguageDetail & Partial
+
+interface CompiledLanguage extends LanguageDetail, CompiledMode {
+    compiled: true
+    contains: CompiledMode[]
+    keywords: Record
+}
+
+type KeywordData = [string, number];
+type KeywordDict = Record
+
+type CompiledMode = Omit &
+    {
+        contains: CompiledMode[]
+        keywords: KeywordDict
+        data: Record
+        terminator_end: string
+        keywordPatternRe: RegExp
+        beginRe: RegExp
+        endRe: RegExp
+        illegalRe: RegExp
+        matcher: any
+        compiled: true
+        starts?: CompiledMode
+        parent?: CompiledMode
+    }
+
+interface ModeDetails {
+    begin?: RegExp | string
+    end?: RegExp | string
+    className?: string
+    contains?: ("self" | Mode)[]
+    endsParent?: boolean
+    endsWithParent?: boolean
+    endSameAsBegin?: boolean
+    skip?: boolean
+    excludeBegin?: boolean
+    excludeEnd?: boolean
+    returnBegin?: boolean
+    returnEnd?: boolean
+    __beforeBegin?: Function
+    parent?: Mode
+    starts?:Mode
+    lexemes?: string | RegExp
+    keywords?: Record | string
+    beginKeywords?: string
+    relevance?: number
+    illegal?: string | RegExp
+    variants?: Mode[]
+    cached_variants?: Mode[]
+    // parsed
+    subLanguage?: string | string[]
+    compiled?: boolean
+}