mirror of
https://github.com/eugeny/tabby
synced 2025-12-12 02:35:41 +00:00
140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
import * as fs from 'fs'
|
|
import { ElectronService } from 'services/electron'
|
|
|
|
const debounceDelay = 500
|
|
|
|
abstract class Handler {
|
|
constructor (protected plugin) { }
|
|
regex: string
|
|
convert (uri: string): string { return uri }
|
|
verify (_uri: string): boolean { return true }
|
|
abstract handle (uri: string): void
|
|
}
|
|
|
|
class URLHandler extends Handler {
|
|
regex = 'http(s)?://[^\\s;\'"]+[^.,;\\s]'
|
|
|
|
handle (uri: string) {
|
|
this.plugin.electron.shell.openExternal(uri)
|
|
}
|
|
}
|
|
|
|
class FileHandler extends Handler {
|
|
regex = '/[^\\s.,;\'"]+'
|
|
|
|
verify (uri: string) {
|
|
return fs.existsSync(uri)
|
|
}
|
|
|
|
handle (uri: string) {
|
|
this.plugin.electron.shell.openExternal('file://' + uri)
|
|
}
|
|
}
|
|
|
|
export default class HyperlinksPlugin {
|
|
handlers = []
|
|
handlerClasses = [
|
|
URLHandler,
|
|
FileHandler,
|
|
]
|
|
electron: ElectronService
|
|
|
|
constructor ({ electron }) {
|
|
this.electron = electron
|
|
this.handlers = this.handlerClasses.map((x) => new x(this))
|
|
}
|
|
|
|
preTerminalInit ({ terminal }) {
|
|
const oldInsertString = terminal.screen_.constructor.prototype.insertString
|
|
const oldDeleteChars = terminal.screen_.constructor.prototype.deleteChars
|
|
terminal.screen_.insertString = (...args) => {
|
|
let ret = oldInsertString.bind(terminal.screen_)(...args)
|
|
this.debounceInsertLinks(terminal.screen_)
|
|
return ret
|
|
}
|
|
terminal.screen_.deleteChars = (...args) => {
|
|
let ret = oldDeleteChars.bind(terminal.screen_)(...args)
|
|
this.debounceInsertLinks(terminal.screen_)
|
|
return ret
|
|
}
|
|
}
|
|
|
|
debounceInsertLinks (screen) {
|
|
if (screen.__insertLinksTimeout) {
|
|
screen.__insertLinksRebounce = true
|
|
} else {
|
|
screen.__insertLinksTimeout = window.setTimeout(() => {
|
|
this.insertLinks(screen)
|
|
screen.__insertLinksTimeout = null
|
|
if (screen.__insertLinksRebounce) {
|
|
screen.__insertLinksRebounce = false
|
|
this.debounceInsertLinks(screen)
|
|
}
|
|
}, debounceDelay)
|
|
}
|
|
}
|
|
|
|
insertLinks (screen) {
|
|
const traverse = (parentNode: Node) => {
|
|
Array.from(parentNode.childNodes).forEach((node) => {
|
|
if (node.nodeName == '#text') {
|
|
parentNode.replaceChild(this.urlizeNode(node), node)
|
|
} else if (node.nodeName != 'A') {
|
|
traverse(node)
|
|
}
|
|
})
|
|
}
|
|
|
|
screen.rowsArray.forEach((x) => traverse(x))
|
|
}
|
|
|
|
urlizeNode (node) {
|
|
let matches = []
|
|
this.handlers.forEach((handler) => {
|
|
let regex = new RegExp(handler.regex, 'gi')
|
|
let match
|
|
while (match = regex.exec(node.textContent)) {
|
|
let uri = handler.convert(match[0])
|
|
if (!handler.verify(uri)) {
|
|
continue;
|
|
}
|
|
matches.push({
|
|
start: regex.lastIndex - match[0].length,
|
|
end: regex.lastIndex,
|
|
text: match[0],
|
|
uri,
|
|
handler
|
|
})
|
|
}
|
|
})
|
|
|
|
if (matches.length == 0) {
|
|
return node
|
|
}
|
|
|
|
matches.sort((a, b) => a.start < b.start ? -1 : 1)
|
|
|
|
let span = document.createElement('span')
|
|
let position = 0
|
|
matches.forEach((match) => {
|
|
if (match.start < position) {
|
|
return
|
|
}
|
|
if (match.start > position) {
|
|
span.appendChild(document.createTextNode(node.textContent.slice(position, match.start)))
|
|
}
|
|
|
|
let a = document.createElement('a')
|
|
a.textContent = match.text
|
|
a.addEventListener('click', () => {
|
|
match.handler.handle(match.uri)
|
|
})
|
|
span.appendChild(a)
|
|
|
|
position = match.end
|
|
})
|
|
span.appendChild(document.createTextNode(node.textContent.slice(position)))
|
|
return span
|
|
}
|
|
}
|