├── plugin └── webapp.vim ├── README.md └── autoload └── webapp.vim /plugin/webapp.vim: -------------------------------------------------------------------------------- 1 | function! s:start() abort 2 | let s:ch = ch_listen("127.0.0.1:8888", {"callback": function("webapp#accept")}) 3 | endfunction 4 | 5 | command WebServer call s:start() 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webapp-vim 2 | 3 | ## What is this? 4 | 5 | Web server that can write web application in Vim script. 6 | 7 | ![](http://go-gyazo.appspot.com/9f3e1755f0ee695b.png) 8 | 9 | 10 | 11 | # Requirements 12 | 13 | [webapi-vim](https://github.com/mattn/webapi-vim) 14 | 15 | # Install 16 | 17 | You need to compile server. To comiple server, you need to install [golang](http://golang.org). 18 | After installing golang, type following. 19 | 20 | $ cd ~/.vim/bundle 21 | $ git clone https://github.com/mattn/webapp-vim 22 | $ cd webapp-vim/server 23 | $ go build 24 | $ ./server 25 | 26 | # Example application 27 | 28 | This is application server. So this don't contains example to run webapp. 29 | Check [webapp-foo-vim](https://github.com/mattn/webapp-foo-vim) 30 | 31 | # How to register your webapp 32 | 33 | You need to make following directory structure. 34 | 35 | +---autoload 36 | | | 37 | | +--- myapp.vim ... add code for your application 38 | | 39 | +--- plugin ... script to register your application 40 | | 41 | +--- static ... static files 42 | 43 | 1. Add script to register your application in `plugin/myapp.vim`. 44 | 45 | call webapp#handle("/myapp", function('myapp#handle')) 46 | 47 | 2. Put html/js/css into `static` directory. 48 | 49 | 3. Write application 50 | 51 | function! myapp#handle(req) 52 | if a:req.path == '/foo' 53 | return {"body": "hello world"} 54 | else 55 | let a:req.path = a:req.path[4:] 56 | return webapp#servefile(a:req, s:basedir) 57 | endif 58 | endfunction 59 | 60 | # License 61 | 62 | MIT 63 | 64 | # Author 65 | 66 | Yasuhiro Matsumoto 67 | -------------------------------------------------------------------------------- /autoload/webapp.vim: -------------------------------------------------------------------------------- 1 | let s:basedir = get(g:, 'webapp_static_dir', expand(':h:h') . '/static') 2 | 3 | let s:mimetypes = { 4 | \ 'ico': 'image/x-icon', 5 | \ 'html': 'text/html; charset=UTF-8', 6 | \ 'js': 'application/javascript; charset=UTF-8', 7 | \ 'txt': 'text/plain; charset=UTF-8', 8 | \ 'css': 'text/css; charset=UTF-8', 9 | \ 'jpg': 'image/jpeg', 10 | \ 'gif': 'image/gif', 11 | \ 'png': 'image/png', 12 | \} 13 | 14 | let s:statustexts = { 15 | \ "100": "Continue", 16 | \ "101": "Switching Protocols", 17 | \ "102": "Processing", 18 | \ "200": "OK", 19 | \ "201": "Created", 20 | \ "202": "Accepted", 21 | \ "203": "Non-Authoritative Information", 22 | \ "204": "No Content", 23 | \ "205": "Reset Content", 24 | \ "206": "Partial Content", 25 | \ "207": "Multi-Status", 26 | \ "208": "Already Reported", 27 | \ "226": "IM Used", 28 | \ "300": "Multiple Choices", 29 | \ "301": "Moved Permanently", 30 | \ "302": "Found", 31 | \ "303": "See Other", 32 | \ "304": "Not Modified", 33 | \ "305": "Use Proxy", 34 | \ "307": "Temporary Redirect", 35 | \ "308": "Permanent Redirect", 36 | \ "400": "Bad Request", 37 | \ "401": "Unauthorized", 38 | \ "402": "Payment Required", 39 | \ "403": "Forbidden", 40 | \ "404": "Not Found", 41 | \ "405": "Method Not Allowed", 42 | \ "406": "Not Acceptable", 43 | \ "407": "Proxy Authentication Required", 44 | \ "408": "Request Timeout", 45 | \ "409": "Conflict", 46 | \ "410": "Gone", 47 | \ "411": "Length Required", 48 | \ "412": "Precondition Failed", 49 | \ "413": "Request Entity Too Large", 50 | \ "414": "Request URI Too Long", 51 | \ "415": "Unsupported Media Type", 52 | \ "416": "Requested Range Not Satisfiable", 53 | \ "417": "Expectation Failed", 54 | \ "418": "I'm a teapot", 55 | \ "421": "Misdirected Request", 56 | \ "422": "Unprocessable Entity", 57 | \ "423": "Locked", 58 | \ "424": "Failed Dependency", 59 | \ "426": "Upgrade Required", 60 | \ "428": "Precondition Required", 61 | \ "429": "Too Many Requests", 62 | \ "431": "Request Header Fields Too Large", 63 | \ "451": "Unavailable For Legal Reasons", 64 | \ "500": "Internal Server Error", 65 | \ "501": "Not Implemented", 66 | \ "502": "Bad Gateway", 67 | \ "503": "Service Unavailable", 68 | \ "504": "Gateway Timeout", 69 | \ "505": "HTTP Version Not Supported", 70 | \ "506": "Variant Also Negotiates", 71 | \ "507": "Insufficient Storage", 72 | \ "508": "Loop Detected", 73 | \ "510": "Not Extended", 74 | \ "511": "Network Authentication Required", 75 | \} 76 | 77 | function! webapp#path2slash(path) abort 78 | return substitute(a:path, '\\', '/', 'g') 79 | endfunction 80 | 81 | function! webapp#fname2mimetype(fname) abort 82 | let ext = fnamemodify(a:fname, ':e') 83 | if has_key(s:mimetypes, ext) 84 | return s:mimetypes[ext] 85 | else 86 | return 'application/octet-stream' 87 | endif 88 | endfunction 89 | 90 | if !exists('s:handlers') 91 | let s:handlers = {} 92 | endif 93 | 94 | function! webapp#form_params(req) abort 95 | let params = {} 96 | for q in split(a:req.body, '&') 97 | let pos = stridx(q, '=') 98 | if pos > 0 99 | let params[q[:pos-1]] = q[pos+1:] 100 | endif 101 | endfor 102 | return params 103 | endfunction 104 | 105 | function! webapp#params(req) abort 106 | let params = {} 107 | for q in split(a:req.query, '&') 108 | let pos = stridx(q, '=') 109 | if pos > 0 110 | let params[q[:pos-1]] = q[pos+1:] 111 | endif 112 | endfor 113 | return params 114 | endfunction 115 | 116 | function! webapp#handle(path, Func) abort 117 | let s:handlers[a:path] = a:Func 118 | endfunction 119 | 120 | function! webapp#json(req, obj, ...) abort 121 | let res = json_encode(a:obj) 122 | let cb = get(a:000, 0, '') 123 | if len(cb) != 0 124 | let res = cb . '(' . res . ')' 125 | endif 126 | return {"header": ["Content-Type: application/json; charset=UTF-8"], "body": res} 127 | endfunction 128 | 129 | function! webapp#redirect(req, to) abort 130 | return {"header": ["Location: " . a:to], "status": 302} 131 | endfunction 132 | 133 | function! webapp#servefile(req, basedir) abort 134 | let res = {"header": [], "body": "", "status": 200} 135 | let fname = a:basedir . a:req.path 136 | if isdirectory(fname) 137 | let fname .= '/index.html' 138 | endif 139 | if filereadable(fname) 140 | let mimetype = webapp#fname2mimetype(fname) 141 | call add(res.header, "Content-Type: " . mimetype) 142 | if exists('v:t_blob') 143 | let res.body = readfile(fname, 'B') 144 | else 145 | if mimetype =~ '^text/' 146 | let res.body = iconv(join(readfile(fname, 'b'), "\n"), "UTF-8", &encoding) 147 | else 148 | let res.body = map(split(substitute(system("xxd -ps " . fname), "[\r\n]", "", "g"), '..\zs'), '"0x".v:val+0') 149 | endif 150 | endif 151 | else 152 | let res.status = 404 153 | let res.body = "Not Found" 154 | "call add(res.header, "Content-Type: text/plain; charset=UTF-8") 155 | "let res.body = join(map(map(split(glob(fname . '/*'), "\n"), 'a:req.path . webapp#path2slash(v:val[len(fname):])'), '"".webapi#html#encodeEntityReference(v:val)."
"'), "\n") 156 | endif 157 | return res 158 | endfunction 159 | 160 | function! webapp#serve(req, ...) abort 161 | try 162 | let basedir = get(empty(a:000) ? {'basedir': s:basedir} : a:000[0], 'basedir', s:basedir) 163 | for path in reverse(sort(keys(s:handlers))) 164 | if stridx(a:req.path, path) == 0 165 | return s:handlers[path](a:req) 166 | endif 167 | endfor 168 | let res = webapp#servefile(a:req, basedir) 169 | catch 170 | let res = {"header": ['Content-Type: text/plain; charset=UTF-8'], "body": "Internal Server Error: " . v:exception, "status": 500} 171 | endtry 172 | return res 173 | endfunction 174 | 175 | function! webapp#accept(ch, b) abort 176 | call ch_setoptions(a:ch, {'mode': 'raw'}) 177 | let req = { 'header': [] } 178 | let content = ch_readraw(a:ch) 179 | let pos = stridx(content, "\n") 180 | if pos == -1 181 | call ch_close(a:ch) 182 | return 183 | endif 184 | let tok = split(content[:pos], '\s\+') 185 | if len(tok) < 2 186 | call ch_close(a:ch) 187 | return 188 | endif 189 | let content = content[pos+1:] 190 | let req['method'] = tok[0] 191 | let pos = stridx(tok[1], "?") 192 | let req['path'] = pos != -1 ? tok[1][:pos] : tok[1] 193 | let req['query'] = pos != -1 ? tok[1][pos+1:] : tok[1] 194 | 195 | let pos = stridx(content, "\r\n\r\n") 196 | if pos != -1 197 | let header = content[:pos] 198 | let body = content[pos+4:] 199 | else 200 | let pos = stridx(content, "\n\n") 201 | if pos != -1 202 | let header = content[:pos] 203 | let body = content[pos+2:] 204 | else 205 | let header = content 206 | let body = '' 207 | endif 208 | endif 209 | let req.header = split(header, '\r\?\n') 210 | let req.body = body 211 | let status = 0 212 | try 213 | let res = webapp#serve(req) 214 | catch 215 | let res = {'status': 500} 216 | endtry 217 | let status = get(res, 'status', 200) 218 | let header = get(res, 'header', []) 219 | call insert(header, printf('HTTP/1.0 %d %s', status, s:statustexts[status])) 220 | call ch_sendraw(a:ch, join(header, "\n") . "\n\n") 221 | call ch_sendraw(a:ch, get(res, 'body', '')) 222 | call ch_close(a:ch) 223 | endfunction 224 | --------------------------------------------------------------------------------