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
text/coffeescript
#!/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"