Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
1'use strict'; 2 3var javascript$1 = require('@lezer/javascript'); 4var language = require('@codemirror/language'); 5var state = require('@codemirror/state'); 6var view = require('@codemirror/view'); 7var autocomplete = require('@codemirror/autocomplete'); 8var common = require('@lezer/common'); 9 10/** 11A collection of JavaScript-related 12[snippets](https://codemirror.net/6/docs/ref/#autocomplete.snippet). 13*/ 14const snippets = [ 15 autocomplete.snippetCompletion("function ${name}(${params}) {\n\t${}\n}", { 16 label: "function", 17 detail: "definition", 18 type: "keyword" 19 }), 20 autocomplete.snippetCompletion("for (let ${index} = 0; ${index} < ${bound}; ${index}++) {\n\t${}\n}", { 21 label: "for", 22 detail: "loop", 23 type: "keyword" 24 }), 25 autocomplete.snippetCompletion("for (let ${name} of ${collection}) {\n\t${}\n}", { 26 label: "for", 27 detail: "of loop", 28 type: "keyword" 29 }), 30 autocomplete.snippetCompletion("do {\n\t${}\n} while (${})", { 31 label: "do", 32 detail: "loop", 33 type: "keyword" 34 }), 35 autocomplete.snippetCompletion("while (${}) {\n\t${}\n}", { 36 label: "while", 37 detail: "loop", 38 type: "keyword" 39 }), 40 autocomplete.snippetCompletion("try {\n\t${}\n} catch (${error}) {\n\t${}\n}", { 41 label: "try", 42 detail: "/ catch block", 43 type: "keyword" 44 }), 45 autocomplete.snippetCompletion("if (${}) {\n\t${}\n}", { 46 label: "if", 47 detail: "block", 48 type: "keyword" 49 }), 50 autocomplete.snippetCompletion("if (${}) {\n\t${}\n} else {\n\t${}\n}", { 51 label: "if", 52 detail: "/ else block", 53 type: "keyword" 54 }), 55 autocomplete.snippetCompletion("class ${name} {\n\tconstructor(${params}) {\n\t\t${}\n\t}\n}", { 56 label: "class", 57 detail: "definition", 58 type: "keyword" 59 }), 60 autocomplete.snippetCompletion("import {${names}} from \"${module}\"\n${}", { 61 label: "import", 62 detail: "named", 63 type: "keyword" 64 }), 65 autocomplete.snippetCompletion("import ${name} from \"${module}\"\n${}", { 66 label: "import", 67 detail: "default", 68 type: "keyword" 69 }) 70]; 71/** 72A collection of snippet completions for TypeScript. Includes the 73JavaScript [snippets](https://codemirror.net/6/docs/ref/#lang-javascript.snippets). 74*/ 75const typescriptSnippets = snippets.concat([ 76 autocomplete.snippetCompletion("interface ${name} {\n\t${}\n}", { 77 label: "interface", 78 detail: "definition", 79 type: "keyword" 80 }), 81 autocomplete.snippetCompletion("type ${name} = ${type}", { 82 label: "type", 83 detail: "definition", 84 type: "keyword" 85 }), 86 autocomplete.snippetCompletion("enum ${name} {\n\t${}\n}", { 87 label: "enum", 88 detail: "definition", 89 type: "keyword" 90 }) 91]); 92 93const cache = new common.NodeWeakMap(); 94const ScopeNodes = new Set([ 95 "Script", "Block", 96 "FunctionExpression", "FunctionDeclaration", "ArrowFunction", "MethodDeclaration", 97 "ForStatement" 98]); 99function defID(type) { 100 return (node, def) => { 101 let id = node.node.getChild("VariableDefinition"); 102 if (id) 103 def(id, type); 104 return true; 105 }; 106} 107const functionContext = ["FunctionDeclaration"]; 108const gatherCompletions = { 109 FunctionDeclaration: defID("function"), 110 ClassDeclaration: defID("class"), 111 ClassExpression: () => true, 112 EnumDeclaration: defID("constant"), 113 TypeAliasDeclaration: defID("type"), 114 NamespaceDeclaration: defID("namespace"), 115 VariableDefinition(node, def) { if (!node.matchContext(functionContext)) 116 def(node, "variable"); }, 117 TypeDefinition(node, def) { def(node, "type"); }, 118 __proto__: null 119}; 120function getScope(doc, node) { 121 let cached = cache.get(node); 122 if (cached) 123 return cached; 124 let completions = [], top = true; 125 function def(node, type) { 126 let name = doc.sliceString(node.from, node.to); 127 completions.push({ label: name, type }); 128 } 129 node.cursor(common.IterMode.IncludeAnonymous).iterate(node => { 130 if (top) { 131 top = false; 132 } 133 else if (node.name) { 134 let gather = gatherCompletions[node.name]; 135 if (gather && gather(node, def) || ScopeNodes.has(node.name)) 136 return false; 137 } 138 else if (node.to - node.from > 8192) { 139 // Allow caching for bigger internal nodes 140 for (let c of getScope(doc, node.node)) 141 completions.push(c); 142 return false; 143 } 144 }); 145 cache.set(node, completions); 146 return completions; 147} 148const Identifier = /^[\w$\xa1-\uffff][\w$\d\xa1-\uffff]*$/; 149const dontComplete = [ 150 "TemplateString", "String", "RegExp", 151 "LineComment", "BlockComment", 152 "VariableDefinition", "TypeDefinition", "Label", 153 "PropertyDefinition", "PropertyName", 154 "PrivatePropertyDefinition", "PrivatePropertyName", 155 "JSXText", "JSXAttributeValue", "JSXOpenTag", "JSXCloseTag", "JSXSelfClosingTag", 156 ".", "?." 157]; 158/** 159Completion source that looks up locally defined names in 160JavaScript code. 161*/ 162function localCompletionSource(context) { 163 let inner = language.syntaxTree(context.state).resolveInner(context.pos, -1); 164 if (dontComplete.indexOf(inner.name) > -1) 165 return null; 166 let isWord = inner.name == "VariableName" || 167 inner.to - inner.from < 20 && Identifier.test(context.state.sliceDoc(inner.from, inner.to)); 168 if (!isWord && !context.explicit) 169 return null; 170 let options = []; 171 for (let pos = inner; pos; pos = pos.parent) { 172 if (ScopeNodes.has(pos.name)) 173 options = options.concat(getScope(context.state.doc, pos)); 174 } 175 return { 176 options, 177 from: isWord ? inner.from : context.pos, 178 validFor: Identifier 179 }; 180} 181function pathFor(read, member, name) { 182 var _a; 183 let path = []; 184 for (;;) { 185 let obj = member.firstChild, prop; 186 if ((obj === null || obj === void 0 ? void 0 : obj.name) == "VariableName") { 187 path.push(read(obj)); 188 return { path: path.reverse(), name }; 189 } 190 else if ((obj === null || obj === void 0 ? void 0 : obj.name) == "MemberExpression" && ((_a = (prop = obj.lastChild)) === null || _a === void 0 ? void 0 : _a.name) == "PropertyName") { 191 path.push(read(prop)); 192 member = obj; 193 } 194 else { 195 return null; 196 } 197 } 198} 199/** 200Helper function for defining JavaScript completion sources. It 201returns the completable name and object path for a completion 202context, or null if no name/property completion should happen at 203that position. For example, when completing after `a.b.c` it will 204return `{path: ["a", "b"], name: "c"}`. When completing after `x` 205it will return `{path: [], name: "x"}`. When not in a property or 206name, it will return null if `context.explicit` is false, and 207`{path: [], name: ""}` otherwise. 208*/ 209function completionPath(context) { 210 let read = (node) => context.state.doc.sliceString(node.from, node.to); 211 let inner = language.syntaxTree(context.state).resolveInner(context.pos, -1); 212 if (inner.name == "PropertyName") { 213 return pathFor(read, inner.parent, read(inner)); 214 } 215 else if ((inner.name == "." || inner.name == "?.") && inner.parent.name == "MemberExpression") { 216 return pathFor(read, inner.parent, ""); 217 } 218 else if (dontComplete.indexOf(inner.name) > -1) { 219 return null; 220 } 221 else if (inner.name == "VariableName" || inner.to - inner.from < 20 && Identifier.test(read(inner))) { 222 return { path: [], name: read(inner) }; 223 } 224 else if (inner.name == "MemberExpression") { 225 return pathFor(read, inner, ""); 226 } 227 else { 228 return context.explicit ? { path: [], name: "" } : null; 229 } 230} 231function enumeratePropertyCompletions(obj, top) { 232 let originalObj = obj; 233 let options = [], seen = new Set; 234 for (let depth = 0;; depth++) { 235 for (let name of (Object.getOwnPropertyNames || Object.keys)(obj)) { 236 if (!/^[a-zA-Z_$\xaa-\uffdc][\w$\xaa-\uffdc]*$/.test(name) || seen.has(name)) 237 continue; 238 seen.add(name); 239 let value; 240 try { 241 value = originalObj[name]; 242 } 243 catch (_) { 244 continue; 245 } 246 options.push({ 247 label: name, 248 type: typeof value == "function" ? (/^[A-Z]/.test(name) ? "class" : top ? "function" : "method") 249 : top ? "variable" : "property", 250 boost: -depth 251 }); 252 } 253 let next = Object.getPrototypeOf(obj); 254 if (!next) 255 return options; 256 obj = next; 257 } 258} 259/** 260Defines a [completion source](https://codemirror.net/6/docs/ref/#autocomplete.CompletionSource) that 261completes from the given scope object (for example `globalThis`). 262Will enter properties of the object when completing properties on 263a directly-named path. 264*/ 265function scopeCompletionSource(scope) { 266 let cache = new Map; 267 return (context) => { 268 let path = completionPath(context); 269 if (!path) 270 return null; 271 let target = scope; 272 for (let step of path.path) { 273 target = target[step]; 274 if (!target) 275 return null; 276 } 277 let options = cache.get(target); 278 if (!options) 279 cache.set(target, options = enumeratePropertyCompletions(target, !path.path.length)); 280 return { 281 from: context.pos - path.name.length, 282 options, 283 validFor: Identifier 284 }; 285 }; 286} 287 288/** 289A language provider based on the [Lezer JavaScript 290parser](https://github.com/lezer-parser/javascript), extended with 291highlighting and indentation information. 292*/ 293const javascriptLanguage = language.LRLanguage.define({ 294 name: "javascript", 295 parser: javascript$1.parser.configure({ 296 props: [ 297 language.indentNodeProp.add({ 298 IfStatement: language.continuedIndent({ except: /^\s*({|else\b)/ }), 299 TryStatement: language.continuedIndent({ except: /^\s*({|catch\b|finally\b)/ }), 300 LabeledStatement: language.flatIndent, 301 SwitchBody: context => { 302 let after = context.textAfter, closed = /^\s*\}/.test(after), isCase = /^\s*(case|default)\b/.test(after); 303 return context.baseIndent + (closed ? 0 : isCase ? 1 : 2) * context.unit; 304 }, 305 Block: language.delimitedIndent({ closing: "}" }), 306 ArrowFunction: cx => cx.baseIndent + cx.unit, 307 "TemplateString BlockComment": () => null, 308 "Statement Property": language.continuedIndent({ except: /^\s*{/ }), 309 JSXElement(context) { 310 let closed = /^\s*<\//.test(context.textAfter); 311 return context.lineIndent(context.node.from) + (closed ? 0 : context.unit); 312 }, 313 JSXEscape(context) { 314 let closed = /\s*\}/.test(context.textAfter); 315 return context.lineIndent(context.node.from) + (closed ? 0 : context.unit); 316 }, 317 "JSXOpenTag JSXSelfClosingTag"(context) { 318 return context.column(context.node.from) + context.unit; 319 } 320 }), 321 language.foldNodeProp.add({ 322 "Block ClassBody SwitchBody EnumBody ObjectExpression ArrayExpression ObjectType": language.foldInside, 323 BlockComment(tree) { return { from: tree.from + 2, to: tree.to - 2 }; }, 324 JSXElement(tree) { 325 let open = tree.firstChild; 326 if (!open || open.name == "JSXSelfClosingTag") 327 return null; 328 let close = tree.lastChild; 329 return { from: open.to, to: close.type.isError ? tree.to : close.from }; 330 }, 331 "JSXSelfClosingTag JSXOpenTag"(tree) { 332 var _a; 333 let name = (_a = tree.firstChild) === null || _a === void 0 ? void 0 : _a.nextSibling, close = tree.lastChild; 334 if (!name || name.type.isError) 335 return null; 336 return { from: name.to, to: close.type.isError ? tree.to : close.from }; 337 } 338 }) 339 ] 340 }), 341 languageData: { 342 closeBrackets: { brackets: ["(", "[", "{", "'", '"', "`"] }, 343 commentTokens: { line: "//", block: { open: "/*", close: "*/" } }, 344 indentOnInput: /^\s*(?:case |default:|\{|\}|<\/)$/, 345 wordChars: "$" 346 } 347}); 348const jsxSublanguage = { 349 test: node => /^JSX/.test(node.name), 350 facet: language.defineLanguageFacet({ commentTokens: { block: { open: "{/*", close: "*/}" } } }) 351}; 352/** 353A language provider for TypeScript. 354*/ 355const typescriptLanguage = javascriptLanguage.configure({ dialect: "ts" }, "typescript"); 356/** 357Language provider for JSX. 358*/ 359const jsxLanguage = javascriptLanguage.configure({ 360 dialect: "jsx", 361 props: [language.sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)] 362}); 363/** 364Language provider for JSX + TypeScript. 365*/ 366const tsxLanguage = javascriptLanguage.configure({ 367 dialect: "jsx ts", 368 props: [language.sublanguageProp.add(n => n.isTop ? [jsxSublanguage] : undefined)] 369}, "typescript"); 370let kwCompletion = (name) => ({ label: name, type: "keyword" }); 371const keywords = "break case const continue default delete export extends false finally in instanceof let new return static super switch this throw true typeof var yield".split(" ").map(kwCompletion); 372const typescriptKeywords = keywords.concat(["declare", "implements", "private", "protected", "public"].map(kwCompletion)); 373/** 374JavaScript support. Includes [snippet](https://codemirror.net/6/docs/ref/#lang-javascript.snippets) 375and local variable completion. 376*/ 377function javascript(config = {}) { 378 let lang = config.jsx ? (config.typescript ? tsxLanguage : jsxLanguage) 379 : config.typescript ? typescriptLanguage : javascriptLanguage; 380 let completions = config.typescript ? typescriptSnippets.concat(typescriptKeywords) : snippets.concat(keywords); 381 return new language.LanguageSupport(lang, [ 382 javascriptLanguage.data.of({ 383 autocomplete: autocomplete.ifNotIn(dontComplete, autocomplete.completeFromList(completions)) 384 }), 385 javascriptLanguage.data.of({ 386 autocomplete: localCompletionSource 387 }), 388 config.jsx ? autoCloseTags : [], 389 ]); 390} 391function findOpenTag(node) { 392 for (;;) { 393 if (node.name == "JSXOpenTag" || node.name == "JSXSelfClosingTag" || node.name == "JSXFragmentTag") 394 return node; 395 if (node.name == "JSXEscape" || !node.parent) 396 return null; 397 node = node.parent; 398 } 399} 400function elementName(doc, tree, max = doc.length) { 401 for (let ch = tree === null || tree === void 0 ? void 0 : tree.firstChild; ch; ch = ch.nextSibling) { 402 if (ch.name == "JSXIdentifier" || ch.name == "JSXBuiltin" || ch.name == "JSXNamespacedName" || 403 ch.name == "JSXMemberExpression") 404 return doc.sliceString(ch.from, Math.min(ch.to, max)); 405 } 406 return ""; 407} 408const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent); 409/** 410Extension that will automatically insert JSX close tags when a `>` or 411`/` is typed. 412*/ 413const autoCloseTags = view.EditorView.inputHandler.of((view, from, to, text, defaultInsert) => { 414 if ((android ? view.composing : view.compositionStarted) || view.state.readOnly || 415 from != to || (text != ">" && text != "/") || 416 !javascriptLanguage.isActiveAt(view.state, from, -1)) 417 return false; 418 let base = defaultInsert(), { state: state$1 } = base; 419 let closeTags = state$1.changeByRange(range => { 420 var _a; 421 let { head } = range, around = language.syntaxTree(state$1).resolveInner(head - 1, -1), name; 422 if (around.name == "JSXStartTag") 423 around = around.parent; 424 if (state$1.doc.sliceString(head - 1, head) != text || around.name == "JSXAttributeValue" && around.to > head) ; 425 else if (text == ">" && around.name == "JSXFragmentTag") { 426 return { range, changes: { from: head, insert: `</>` } }; 427 } 428 else if (text == "/" && around.name == "JSXStartCloseTag") { 429 let empty = around.parent, base = empty.parent; 430 if (base && empty.from == head - 2 && 431 ((name = elementName(state$1.doc, base.firstChild, head)) || ((_a = base.firstChild) === null || _a === void 0 ? void 0 : _a.name) == "JSXFragmentTag")) { 432 let insert = `${name}>`; 433 return { range: state.EditorSelection.cursor(head + insert.length, -1), changes: { from: head, insert } }; 434 } 435 } 436 else if (text == ">") { 437 let openTag = findOpenTag(around); 438 if (openTag && openTag.name == "JSXOpenTag" && 439 !/^\/?>|^<\//.test(state$1.doc.sliceString(head, head + 2)) && 440 (name = elementName(state$1.doc, openTag, head))) 441 return { range, changes: { from: head, insert: `</${name}>` } }; 442 } 443 return { range }; 444 }); 445 if (closeTags.changes.empty) 446 return false; 447 view.dispatch([ 448 base, 449 state$1.update(closeTags, { userEvent: "input.complete", scrollIntoView: true }) 450 ]); 451 return true; 452}); 453 454/** 455Connects an [ESLint](https://eslint.org/) linter to CodeMirror's 456[lint](https://codemirror.net/6/docs/ref/#lint) integration. `eslint` should be an instance of the 457[`Linter`](https://eslint.org/docs/developer-guide/nodejs-api#linter) 458class, and `config` an optional ESLint configuration. The return 459value of this function can be passed to [`linter`](https://codemirror.net/6/docs/ref/#lint.linter) 460to create a JavaScript linting extension. 461 462Note that ESLint targets node, and is tricky to run in the 463browser. The 464[eslint-linter-browserify](https://github.com/UziTech/eslint-linter-browserify) 465package may help with that (see 466[example](https://github.com/UziTech/eslint-linter-browserify/blob/master/example/script.js)). 467*/ 468function esLint(eslint, config) { 469 if (!config) { 470 config = { 471 parserOptions: { ecmaVersion: 2019, sourceType: "module" }, 472 env: { browser: true, node: true, es6: true, es2015: true, es2017: true, es2020: true }, 473 rules: {} 474 }; 475 eslint.getRules().forEach((desc, name) => { 476 var _a; 477 if ((_a = desc.meta.docs) === null || _a === void 0 ? void 0 : _a.recommended) 478 config.rules[name] = 2; 479 }); 480 } 481 return (view) => { 482 let { state } = view, found = []; 483 for (let { from, to } of javascriptLanguage.findRegions(state)) { 484 let fromLine = state.doc.lineAt(from), offset = { line: fromLine.number - 1, col: from - fromLine.from, pos: from }; 485 for (let d of eslint.verify(state.sliceDoc(from, to), config)) 486 found.push(translateDiagnostic(d, state.doc, offset)); 487 } 488 return found; 489 }; 490} 491function mapPos(line, col, doc, offset) { 492 return doc.line(line + offset.line).from + col + (line == 1 ? offset.col - 1 : -1); 493} 494function translateDiagnostic(input, doc, offset) { 495 let start = mapPos(input.line, input.column, doc, offset); 496 let result = { 497 from: start, 498 to: input.endLine != null && input.endColumn != 1 ? mapPos(input.endLine, input.endColumn, doc, offset) : start, 499 message: input.message, 500 source: input.ruleId ? "eslint:" + input.ruleId : "eslint", 501 severity: input.severity == 1 ? "warning" : "error", 502 }; 503 if (input.fix) { 504 let { range, text } = input.fix, from = range[0] + offset.pos - start, to = range[1] + offset.pos - start; 505 result.actions = [{ 506 name: "fix", 507 apply(view, start) { 508 view.dispatch({ changes: { from: start + from, to: start + to, insert: text }, scrollIntoView: true }); 509 } 510 }]; 511 } 512 return result; 513} 514 515exports.autoCloseTags = autoCloseTags; 516exports.completionPath = completionPath; 517exports.esLint = esLint; 518exports.javascript = javascript; 519exports.javascriptLanguage = javascriptLanguage; 520exports.jsxLanguage = jsxLanguage; 521exports.localCompletionSource = localCompletionSource; 522exports.scopeCompletionSource = scopeCompletionSource; 523exports.snippets = snippets; 524exports.tsxLanguage = tsxLanguage; 525exports.typescriptLanguage = typescriptLanguage; 526exports.typescriptSnippets = typescriptSnippets;