├── README.md ├── autoload └── webapp.vim └── server ├── server.go ├── server_others.go └── server_windows.go /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 | \ 'jpg': 'image/jpeg', 9 | \ 'gif': 'image/gif', 10 | \ 'png': 'image/png', 11 | \} 12 | 13 | function! webapp#path2slash(path) 14 | return substitute(a:path, '\\', '/', 'g') 15 | endfunction 16 | 17 | function! webapp#fname2mimetype(fname) 18 | let ext = fnamemodify(a:fname, ':e') 19 | if has_key(s:mimetypes, ext) 20 | return s:mimetypes[ext] 21 | else 22 | return 'application/octet-stream' 23 | endif 24 | endfunction 25 | 26 | if !exists('s:handlers') 27 | let s:handlers = {} 28 | endif 29 | 30 | function! webapp#params(req) 31 | let params = {} 32 | for q in split(a:req.query, '&') 33 | let pos = stridx(q, '=') 34 | if pos > 0 35 | let params[q[:pos-1]] = q[pos+1:] 36 | endif 37 | endfor 38 | return params 39 | endfunction 40 | 41 | function! webapp#handle(path, Func) 42 | let s:handlers[a:path] = a:Func 43 | endfunction 44 | 45 | function! webapp#json(req, obj, ...) 46 | let res = webapi#json#encode(a:obj) 47 | let cb = get(a:000, 0, '') 48 | if len(cb) != 0 49 | let res = cb . '(' . res . ')' 50 | endif 51 | return {"header": ["Content-Type: application/json"], "body": res} 52 | endfunction 53 | 54 | function! webapp#redirect(req, to) 55 | return {"header": ["Location: " . a:to], "status": 302} 56 | endfunction 57 | 58 | function! webapp#servefile(req, basedir) 59 | let res = {"header": [], "body": "", "status": 200} 60 | let fname = a:basedir . a:req.path 61 | if isdirectory(fname) 62 | if filereadable(fname . '/index.html') 63 | let fname .= '/index.html' 64 | let mimetype = webapp#fname2mimetype(fname) 65 | call add(res.header, "Content-Type: " . mimetype) 66 | if mimetype =~ '^text/' 67 | let res.body = iconv(join(readfile(fname, 'b'), "\n"), "UTF-8", &encoding) 68 | else 69 | let res.body = map(split(substitute(system("xxd -ps " . fname), "[\r\n]", "", "g"), '..\zs'), '"0x".v:val+0') 70 | endif 71 | else 72 | call add(res.header, "Content-Type: text/plain; charset=UTF-8") 73 | 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") 74 | endif 75 | elseif filereadable(fname) 76 | let mimetype = webapp#fname2mimetype(fname) 77 | call add(res.header, "Content-Type: " . mimetype) 78 | if mimetype =~ '^text/' 79 | let res.body = iconv(join(readfile(fname, 'b'), "\n"), "UTF-8", &encoding) 80 | else 81 | let res.body = map(split(substitute(system("xxd -ps " . fname), "[\r\n]", "", "g"), '..\zs'), '"0x".v:val+0') 82 | endif 83 | else 84 | let res.status = 404 85 | let res.body = "Not Found" 86 | endif 87 | return res 88 | endfunction 89 | 90 | function! webapp#serve(req) 91 | try 92 | for path in reverse(sort(keys(s:handlers))) 93 | if stridx(a:req.path, path) == 0 94 | return s:handlers[path](a:req) 95 | endif 96 | endfor 97 | let res = webapp#servefile(a:req, s:basedir) 98 | catch 99 | let res = {"header": [], "body": "Internal Server Error: " . v:exception, "status": 500} 100 | endtry 101 | return res 102 | endfunction 103 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "github.com/keep94/weblogs" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os/exec" 13 | "regexp" 14 | "strings" 15 | ) 16 | 17 | type response struct { 18 | Header []string `json:"header"` 19 | Body interface{} `json:"body"` 20 | Status int `json:"status"` 21 | } 22 | 23 | type request struct { 24 | Header []string `json:"header"` 25 | Path string `json:"path"` 26 | Query string `json:"query"` 27 | Body string `json:"body"` 28 | Method string `json:"method"` 29 | } 30 | 31 | var re_header = regexp.MustCompile(`^([a-zA-Z][-_a-zA-Z0-9]*):\s*(.*)`) 32 | 33 | var addr = flag.String("addr", ":9001", "server address") 34 | 35 | func main() { 36 | flag.Parse() 37 | 38 | b, err := exec.Command("vim", "--serverlist").Output() 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | vim := "" 43 | for _, line := range strings.Split(string(b), "\n") { 44 | line = strings.TrimSpace(line) 45 | b, err = exec.Command("vim", "--servername", line, "--remote-expr", `string(function('webapp#serve'))`).Output() 46 | if err == nil && strings.TrimSpace(string(b)) == `function('webapp#serve')` { 47 | vim = line 48 | break 49 | } 50 | 51 | } 52 | if vim == "" { 53 | log.Fatal("vim doesn't support remote protocol, if you don't start vim yet, start now") 54 | } 55 | log.Print("Registered vim server: ", vim) 56 | 57 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 58 | body, err := ioutil.ReadAll(r.Body) 59 | if err != nil { 60 | http.Error(w, err.Error(), http.StatusInternalServerError) 61 | return 62 | } 63 | var buf bytes.Buffer 64 | err = r.Header.Write(&buf) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusInternalServerError) 67 | return 68 | } 69 | req := &request{ 70 | Method: r.Method, 71 | Path: r.URL.Path, 72 | Query: r.URL.RawQuery, 73 | Header: strings.Split(strings.TrimSpace(buf.String()), "\r\n"), 74 | Body: string(body), 75 | } 76 | b, err = json.Marshal(req) 77 | if err != nil { 78 | http.Error(w, err.Error(), http.StatusInternalServerError) 79 | return 80 | } 81 | input := string(b) 82 | input = strings.Replace(input, `'`, `\x27`, -1) 83 | payload := fmt.Sprintf(`webapi#json#encode(webapp#serve(webapi#json#decode('%s')))`, input) 84 | b, err = exec.Command("vim", "--servername", vim, "--remote-expr", payload).CombinedOutput() 85 | if err != nil { 86 | http.Error(w, err.Error(), http.StatusInternalServerError) 87 | return 88 | } 89 | if len(b) > 0 && b[0] == 'E' { 90 | http.Error(w, string(b), http.StatusInternalServerError) 91 | return 92 | } 93 | b = convert_input(b) 94 | var res response 95 | err = json.Unmarshal(b, &res) 96 | if err != nil { 97 | http.Error(w, err.Error()+": "+string(b), http.StatusInternalServerError) 98 | return 99 | } 100 | for _, header := range res.Header { 101 | kv := re_header.FindStringSubmatch(header) 102 | if len(kv) == 3 { 103 | w.Header().Set(kv[1], kv[2]) 104 | } 105 | } 106 | if res.Status != 0 { 107 | w.WriteHeader(res.Status) 108 | } 109 | if body, ok := res.Body.(string); ok { 110 | w.Write([]byte(body)) 111 | } else if bf, ok := res.Body.([]interface{}); ok { 112 | b := make([]byte, len(bf)) 113 | for i := range bf { 114 | b[i] = byte(bf[i].(float64)) 115 | } 116 | w.Write(b) 117 | } 118 | }) 119 | 120 | serverAddr := *addr 121 | if len(serverAddr) > 0 && serverAddr[0] == ':' { 122 | serverAddr = "127.0.0.1" + serverAddr 123 | } 124 | log.Println("Starting server:", "http://"+serverAddr) 125 | err = http.ListenAndServe(*addr, weblogs.Handler(http.DefaultServeMux)) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /server/server_others.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | func convert_input(input []byte) []byte { 6 | return input 7 | } 8 | -------------------------------------------------------------------------------- /server/server_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "syscall" 10 | 11 | "github.com/mattn/go-encoding" 12 | enc "golang.org/x/text/encoding" 13 | "golang.org/x/text/transform" 14 | ) 15 | 16 | var ( 17 | kernel32 = syscall.NewLazyDLL("kernel32") 18 | procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP") 19 | inputEncoding enc.Encoding 20 | ) 21 | 22 | func init() { 23 | r1, _, _ := procGetConsoleOutputCP.Call() 24 | if r1 != 0 { 25 | inputEncoding = encoding.GetEncoding(fmt.Sprintf("CP%d", +int(r1))) 26 | } 27 | } 28 | 29 | func convert_input(input []byte) []byte { 30 | if inputEncoding != nil { 31 | in := bytes.NewReader(input) 32 | var out bytes.Buffer 33 | io.Copy(&out, transform.NewReader(in, inputEncoding.NewDecoder())) 34 | return out.Bytes() 35 | } 36 | return input 37 | } 38 | --------------------------------------------------------------------------------