UNPKG

text-aid-too

Version:

Edit web inputs (including on GMail) with your favourite native text editor; and (experimentally) use markdown.

197 lines (162 loc) 5.54 kB
#!/usr/bin/env coffee # Required modules: # npm install watchr # npm install optimist # npm install ws # npm install markdown # npm install html # npm install coffee-script # Set the environment variable below, and the server will refuse to serve clients who don't know the secret. secret = process.env.TEXT_AID_TOO_SECRET for module in [ # The first of these must be installed via "npm". "watchr" "optimist" "ws" "markdown" "html" # These are standard. "os" "fs" "path" "child_process" ] try global[module] = require module catch console.log "ERROR\n#{module} is not available: sudo npm install -g #{module}" process.exit 1 config = port: "9293" host: "localhost" editor: "gvim -f" defaultEditor = if process.env.TEXT_AID_TOO_EDITOR process.env.TEXT_AID_TOO_EDITOR else config.editor pjson = require path.join "..", "package.json" version = pjson.version helpText = """ Usage: text-aid-too [--port PORT] [--editor EDITOR-COMMAND] [--markdown] Example: export TEXT_AID_TOO_EDITOR="gvim -f" TEXT_AID_TOO_SECRET=hul8quahJ4eeL1Ib text-aid-too --port 9293 Markdown (experimental): With the "--markdown" flag, text-aid-too tries to find non-HTML paragraphs in HTML texts and parses them as markdown. This only applies to texts from contentEditable elements (e.g. the GMail compose window). Environment variables: TEXT_AID_TOO_EDITOR: the editor command to use. TEXT_AID_TOO_SECRET: the shared secret; set this in the extension too. Version: #{version} """ args = optimist.usage(helpText) .alias("h", "help") .default("port", config.port) .default("editor", defaultEditor) .default("markdown", false) .argv if args.help optimist.showHelp() process.exit(0) console.log """ server ws://#{config.host}:#{args.port} secret #{if secret? then secret else '<NONE>'} editor #{args.editor} version #{version} """ WSS = ws.Server wss = new WSS port: args.port, host: config.host wss.on "connection", (ws) -> ws.on "message", handler ws getEditCommand = (filename) -> command = if 0 <= args.editor.indexOf "%s" then args.editor.replace "%s", filename else "#{args.editor} #{filename}" console.log "exec:", command command handler = (ws) -> (message) -> request = JSON.parse message onExit = [] onExit.push -> ws.close() exit = (continuation = null) -> callback() for callback in onExit.reverse() onExit = [] continuation?() if secret? and 0 < secret.length unless request.secret? and request.secret == secret console.log """ mismatched or invalid secret; aborting request: required secret: #{secret} received secret: #{request.secret} """ return exit() sendResponse = (response, continuation = null) -> response.serverVersion = version ws.send JSON.stringify response continuation?() handlers = ping: -> console.log "ping: ok" request.isOk = true sendResponse request, exit edit: -> username = process.env.USER ? "unknown" directory = process.env.TMPDIR ? os.tmpdir() timestamp = process.hrtime().join "-" suffix = if request.isContentEditable then "html" else "txt" filename = path.join directory, "#{username}-text-aid-too-#{timestamp}.#{suffix}" console.log "edit:", filename onExit.push -> console.log " done:", filename fs.writeFile filename, (request.originalText ? request.text), (error) -> return exit() if error onExit.push -> fs.unlink filename, -> sendText = (continuation = null) -> fs.readFile filename, "utf8", (error, data) -> return exit() if error console.log " send: #{filename} [#{data.length}]" data = data.replace /\n$/, "" request.text = request.originalText = data request.text = formatMarkdown data if request.isContentEditable and args.markdown sendResponse request, continuation monitor = watchr.watch path: filename listener: sendText # This is only used for the "watch" method. catchupDelay: 400 # Unfortunately, the "watch" method isn't reliable. So we're actually using the "watchFile" method # instead. See https://github.com/bevry/watchr/issues/33. preferredMethods: [ 'watchFile', 'watch' ] interval: 500 onExit.push -> monitor.close() child = child_process.exec getEditCommand filename child.on "exit", (error) -> if error then exit() else sendText exit if handlers[request.name]? handlers[request.name]() else console.log "error; unknown request:", request markdownToHtml = (text) -> try html.prettyPrint markdown.markdown.toHTML text catch text # This is best-effort markdown handling. Paragraphs are separated by "\n\n". We collect together as many # paragraphs which don't seem to contain HTML as we can, and process them as markdown. Everything else just gets # passed through. formatMarkdown = (text) -> [ output, texts, input ] = [ [], [], text.split("\n\n").reverse() ] flushMarkdown = -> if 0 < texts.length output.push markdownToHtml texts.join "\n\n" texts = [] while 0 < input.length paragraph = input.pop() if /<\/?[a-zA-Z]+/.test paragraph flushMarkdown() output.push paragraph else texts.push paragraph flushMarkdown() output.join "\n\n"