UNPKG

@xterm/addon-web-links

Version:

An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables web links. This addon requires xterm.js v4+.

8 lines (7 loc) 13 kB
{ "version": 3, "sources": ["../src/WebLinkProvider.ts", "../src/WebLinksAddon.ts"], "sourcesContent": ["/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ILinkProvider, ILink, Terminal, IViewportRange, IBufferLine } from '@xterm/xterm';\n\nexport interface ILinkProviderOptions {\n hover?(event: MouseEvent, text: string, location: IViewportRange): void;\n leave?(event: MouseEvent, text: string): void;\n urlRegex?: RegExp;\n}\n\nexport class WebLinkProvider implements ILinkProvider {\n\n constructor(\n private readonly _terminal: Terminal,\n private readonly _regex: RegExp,\n private readonly _handler: (event: MouseEvent, uri: string) => void,\n private readonly _options: ILinkProviderOptions = {}\n ) {\n\n }\n\n public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {\n const links = LinkComputer.computeLink(y, this._regex, this._terminal, this._handler);\n callback(this._addCallbacks(links));\n }\n\n private _addCallbacks(links: ILink[]): ILink[] {\n return links.map(link => {\n link.leave = this._options.leave;\n link.hover = (event: MouseEvent, uri: string): void => {\n if (this._options.hover) {\n const { range } = link;\n this._options.hover(event, uri, range);\n }\n };\n return link;\n });\n }\n}\n\nfunction isUrl(urlString: string): boolean {\n try {\n const url = new URL(urlString);\n const parsedBase = url.password && url.username\n ? `${url.protocol}//${url.username}:${url.password}@${url.host}`\n : url.username\n ? `${url.protocol}//${url.username}@${url.host}`\n : `${url.protocol}//${url.host}`;\n return urlString.toLocaleLowerCase().startsWith(parsedBase.toLocaleLowerCase());\n } catch (e) {\n return false;\n }\n}\n\nexport class LinkComputer {\n public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {\n const rex = new RegExp(regex.source, (regex.flags || '') + 'g');\n\n const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);\n const line = lines.join('');\n\n let match;\n const result: ILink[] = [];\n\n while (match = rex.exec(line)) {\n const text = match[0];\n\n // check via URL if the matched text would form a proper url\n if (!isUrl(text)) {\n continue;\n }\n\n // map string positions back to buffer positions\n // values are 0-based right side excluding\n const [startY, startX] = LinkComputer._mapStrIdx(terminal, startLineIndex, 0, match.index);\n const [endY, endX] = LinkComputer._mapStrIdx(terminal, startY, startX, text.length);\n\n if (startY === -1 || startX === -1 || endY === -1 || endX === -1) {\n continue;\n }\n\n // range expects values 1-based right side including, thus +1 except for endX\n const range = {\n start: {\n x: startX + 1,\n y: startY + 1\n },\n end: {\n x: endX,\n y: endY + 1\n }\n };\n\n result.push({ range, text, activate });\n }\n\n return result;\n }\n\n /**\n * Get wrapped content lines for the current line index.\n * The top/bottom line expansion stops at whitespaces or length > 2048.\n * Returns an array with line strings and the top line index.\n *\n * NOTE: We pull line strings with trimRight=true on purpose to make sure\n * to correctly match urls with early wrapped wide chars. This corrupts the string index\n * for 1:1 backmapping to buffer positions, thus needs an additional correction in _mapStrIdx.\n */\n private static _getWindowedLineStrings(lineIndex: number, terminal: Terminal): [string[], number] {\n let line: IBufferLine | undefined;\n let topIdx = lineIndex;\n let bottomIdx = lineIndex;\n let length = 0;\n let content = '';\n const lines: string[] = [];\n\n if ((line = terminal.buffer.active.getLine(lineIndex))) {\n const currentContent = line.translateToString(true);\n\n // expand top, stop on whitespaces or length > 2048\n if (line.isWrapped && currentContent[0] !== ' ') {\n length = 0;\n while ((line = terminal.buffer.active.getLine(--topIdx)) && length < 2048) {\n content = line.translateToString(true);\n length += content.length;\n lines.push(content);\n if (!line.isWrapped || content.indexOf(' ') !== -1) {\n break;\n }\n }\n lines.reverse();\n }\n\n // append current line\n lines.push(currentContent);\n\n // expand bottom, stop on whitespaces or length > 2048\n length = 0;\n while ((line = terminal.buffer.active.getLine(++bottomIdx)) && line.isWrapped && length < 2048) {\n content = line.translateToString(true);\n length += content.length;\n lines.push(content);\n if (content.indexOf(' ') !== -1) {\n break;\n }\n }\n }\n return [lines, topIdx];\n }\n\n /**\n * Map a string index back to buffer positions.\n * Returns buffer position as [lineIndex, columnIndex] 0-based,\n * or [-1, -1] in case the lookup ran into a non-existing line.\n */\n private static _mapStrIdx(terminal: Terminal, lineIndex: number, rowIndex: number, stringIndex: number): [number, number] {\n const buf = terminal.buffer.active;\n const cell = buf.getNullCell();\n let start = rowIndex;\n while (stringIndex) {\n const line = buf.getLine(lineIndex);\n if (!line) {\n return [-1, -1];\n }\n for (let i = start; i < line.length; ++i) {\n line.getCell(i, cell);\n const chars = cell.getChars();\n const width = cell.getWidth();\n if (width) {\n stringIndex -= chars.length || 1;\n\n // correct stringIndex for early wrapped wide chars:\n // - currently only happens at last cell\n // - cells to the right are reset with chars='' and width=1 in InputHandler.print\n // - follow-up line must be wrapped and contain wide char at first cell\n // --> if all these conditions are met, correct stringIndex by +1\n if (i === line.length - 1 && chars === '') {\n const line = buf.getLine(lineIndex + 1);\n if (line && line.isWrapped) {\n line.getCell(0, cell);\n if (cell.getWidth() === 2) {\n stringIndex += 1;\n }\n }\n }\n }\n if (stringIndex < 0) {\n return [lineIndex, i];\n }\n }\n lineIndex++;\n start = 0;\n }\n return [lineIndex, start];\n }\n}\n", "/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';\nimport type { WebLinksAddon as IWebLinksApi } from '@xterm/addon-web-links';\nimport { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';\n\n// consider everthing starting with http:// or https://\n// up to first whitespace, `\"` or `'` as url\n// NOTE: The repeated end clause is needed to not match a dangling `:`\n// resembling the old (...)*([^:\"\\'\\\\s]) final path clause\n// additionally exclude early + final:\n// - unsafe from rfc3986: !*'()\n// - unsafe chars from rfc1738: {}|\\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)\n// also exclude as finals:\n// - final interpunction like ,.!?\n// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)\n// - unsafe chars from rfc1738: {}|\\^~[]`\nconst strictUrlRegex = /(https?|HTTPS?):[/]{2}[^\\s\"'!*(){}|\\\\\\^<>`]*[^\\s\"':,.!?{}|\\\\\\^~\\[\\]`()<>]/;\n\n\nfunction handleLink(event: MouseEvent, uri: string): void {\n const newWindow = window.open();\n if (newWindow) {\n try {\n newWindow.opener = null;\n } catch {\n // no-op, Electron can throw\n }\n newWindow.location.href = uri;\n } else {\n console.warn('Opening link blocked as opener could not be cleared');\n }\n}\n\nexport class WebLinksAddon implements ITerminalAddon , IWebLinksApi {\n private _terminal: Terminal | undefined;\n private _linkProvider: IDisposable | undefined;\n\n constructor(\n private _handler: (event: MouseEvent, uri: string) => void = handleLink,\n private _options: ILinkProviderOptions = {}\n ) {\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n const options = this._options as ILinkProviderOptions;\n const regex = options.urlRegex || strictUrlRegex;\n this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));\n }\n\n public dispose(): void {\n this._linkProvider?.dispose();\n }\n}\n"], "mappings": "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", "names": ["WebLinkProvider", "_terminal", "_regex", "_handler", "_options", "y", "callback", "links", "LinkComputer", "link", "event", "uri", "range", "isUrl", "urlString", "url", "parsedBase", "_LinkComputer", "regex", "terminal", "activate", "rex", "lines", "startLineIndex", "line", "match", "result", "text", "startY", "startX", "endY", "endX", "lineIndex", "topIdx", "bottomIdx", "length", "content", "currentContent", "rowIndex", "stringIndex", "buf", "cell", "start", "i", "chars", "strictUrlRegex", "handleLink", "event", "uri", "newWindow", "WebLinksAddon", "_handler", "_options", "terminal", "options", "regex", "WebLinkProvider"] }