├── screenshots
├── screenshot1.png
└── screenshot2.png
├── activate
├── src
└── gobookmark
│ ├── migrations
│ ├── 0001_append_tags.down.sql
│ └── 0001_append_tags.up.sql
│ ├── public
│ ├── js
│ │ ├── main.js
│ │ └── bootstrap-tagsinput.js
│ └── css
│ │ ├── bootstrap-tagsinput.css
│ │ └── screen.css
│ ├── utils_test.go
│ ├── templates
│ ├── includes
│ │ └── paginate.html
│ ├── login.html
│ ├── edit.html
│ ├── index.html
│ └── layout.html
│ ├── utils.go
│ ├── assetfs.go
│ ├── gobookmark.go
│ ├── views.go
│ ├── main_test.go
│ ├── glide.yaml
│ ├── glide.lock
│ └── models.go
├── Dockerfile
├── .gitignore
├── docker-compose.yml
├── contrib
├── docker-compose.yml
├── README.md
└── bm.example.com.nginx.conf
├── Makefile
└── README.md
/screenshots/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harobed/gobookmark/HEAD/screenshots/screenshot1.png
--------------------------------------------------------------------------------
/screenshots/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/harobed/gobookmark/HEAD/screenshots/screenshot2.png
--------------------------------------------------------------------------------
/activate:
--------------------------------------------------------------------------------
1 | export GOPATH=`pwd`
2 | export GOBIN=`pwd`/bin
3 | export PATH=`pwd`/bin:$PATH
4 | export GO15VENDOREXPERIMENT=1
5 |
--------------------------------------------------------------------------------
/src/gobookmark/migrations/0001_append_tags.down.sql:
--------------------------------------------------------------------------------
1 | DROP INDEX links;
2 | DROP INDEX fk_links_tags;
3 | DROP TABLE rel_links_tags;
4 | DROP TABLE tags;
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:jessie
2 | RUN mkdir /data/
3 | WORKDIR /
4 | ADD ./releases/linux_amd64/gobookmark /gobookmark
5 | ENV GOBOOKMARK_DATABASE=/data/gobookmark
6 | ENV GOBOOKMARK_HOST=0.0.0.0
7 | EXPOSE 8000
8 | CMD /gobookmark web
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /pkg/
3 | vendor/
4 | *.db
5 | bindata.go
6 | src/github.com/
7 | src/gopkg.in/
8 | src/9fans.net/
9 | src/golang.org/
10 | gobookmark.index/
11 | gobookmark-test.index/
12 | src/gobookmark/gobookmark
13 | imports/
14 | .tags
15 | .tags1
16 | releases/
17 |
--------------------------------------------------------------------------------
/src/gobookmark/public/js/main.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | console.log("fooobar");
3 | $('input[name="url"]').bind("propertychange change click keyup input paste", function() {
4 | $.get("/fetch-title/?url=" + $('input[name="url"]').val(), function(data) {
5 | if (data != '') {
6 | $('input[name="title"]').val(data);
7 | }
8 | });
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | gobookmark:
4 | container_name: gobookmark
5 | build: .
6 | restart: always
7 | volumes:
8 | - gobookmark-db-volume:/data/
9 | - ./imports:/imports/
10 | ports:
11 | - "8000:8000"
12 | environment:
13 | - GOBOOKMARK_PASSWORD=password
14 |
15 | volumes:
16 | gobookmark-db-volume:
17 | driver: local
18 |
--------------------------------------------------------------------------------
/contrib/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 | gobookmark:
4 | container_name: gobookmark
5 | restart: always
6 | image: harobed/gobookmark
7 | volumes:
8 | - gobookmark-db-volume:/data/
9 | - ./imports:/imports/
10 | ports:
11 | - "8000:8000"
12 | environment:
13 | - GOBOOKMARK_PASSWORD=password
14 |
15 | volumes:
16 | gobookmark-db-volume:
17 | driver: local
18 |
--------------------------------------------------------------------------------
/src/gobookmark/utils_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "testing"
6 | )
7 |
8 | func TestExtractTags(t *testing.T) {
9 | assert.Equal(t, extractTags("[foo][bar] extra")[0], "foo")
10 | assert.Equal(t, extractTags("[foo][bar ] extra")[1], "bar")
11 | assert.Len(t, extractTags("extra"), 0)
12 | }
13 |
14 | func TestRemoveTags(t *testing.T) {
15 | assert.Equal(t, removeTags("[foo][bar] extra"), "extra")
16 | }
17 |
--------------------------------------------------------------------------------
/contrib/README.md:
--------------------------------------------------------------------------------
1 | # How to use docker-compose on production
2 |
3 | ```
4 | # docker-compose up -d
5 | # docker-compose logs
6 | ```
7 |
8 | How to import your bookmark file to gobookmark :
9 |
10 | * place your bookmark file in imports folder, next :
11 |
12 | ```
13 | # docker-compose stop gobookmark
14 | # docker-compose run --rm gobookmark ./gobookmark import --reset /imports/bookmarks_public_20160318_101550.html
15 | # docker-compose up -d gobookmark
16 | ```
17 |
--------------------------------------------------------------------------------
/src/gobookmark/migrations/0001_append_tags.up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS links (
2 | id INTEGER PRIMARY KEY AUTOINCREMENT,
3 | title TEXT NOT NULL,
4 | url TEXT NOT NULL,
5 | createdate DATE DEFAULT (datetime('now','localtime'))
6 | );
7 |
8 | CREATE TABLE IF NOT EXISTS tags (
9 | id INTEGER PRIMARY KEY AUTOINCREMENT,
10 | title TEXT NOT NULL,
11 | slug TEXT NOT NULL
12 | );
13 | CREATE INDEX fk_tags_slug ON tags (slug);
14 |
15 | CREATE TABLE IF NOT EXISTS rel_links_tags (
16 | link_id INTEGER NOT NULL,
17 | tag_id INTEGER NOT NULL
18 | );
19 |
20 | CREATE INDEX fk_links_tags ON rel_links_tags (link_id, tag_id);
21 |
--------------------------------------------------------------------------------
/contrib/bm.example.com.nginx.conf:
--------------------------------------------------------------------------------
1 | upstream gobookmark {
2 | server localhost:8000;
3 | }
4 |
5 | server {
6 | listen 80;
7 |
8 | server_name bm.example.com;
9 | server_tokens off; # don't show the version number, a security best practice
10 | client_max_body_size 5000M;
11 | access_log /var/log/nginx/bm.example.com_access.log;
12 | error_log /var/log/nginx/bm.example.com_error.log;
13 |
14 | location / {
15 | proxy_pass http://gobookmark;
16 | proxy_set_header Host $http_host; # required for docker client's sake
17 | proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
19 | proxy_set_header X-Forwarded-Proto $scheme;
20 | proxy_read_timeout 900;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/gobookmark/templates/includes/paginate.html:
--------------------------------------------------------------------------------
1 | {{ define "paginate" }}
2 |
28 | {{ end }}
29 |
--------------------------------------------------------------------------------
/src/gobookmark/templates/login.html:
--------------------------------------------------------------------------------
1 | {{ template "layout" . }}
2 | {{ define "content" }}
3 |
4 |
5 | {{ if .Error }}
6 |
7 |
8 | Error: {{ .Error }}
9 |
10 | {{ end }}
11 |
31 |
32 |
33 | {{ end }}
34 |
--------------------------------------------------------------------------------
/src/gobookmark/public/css/bootstrap-tagsinput.css:
--------------------------------------------------------------------------------
1 | .bootstrap-tagsinput {
2 | background-color: #fff;
3 | border: 1px solid #ccc;
4 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
5 | display: inline-block;
6 | padding: 4px 6px;
7 | color: #555;
8 | vertical-align: middle;
9 | border-radius: 4px;
10 | max-width: 100%;
11 | line-height: 22px;
12 | cursor: text;
13 | }
14 | .bootstrap-tagsinput input {
15 | border: none;
16 | box-shadow: none;
17 | outline: none;
18 | background-color: transparent;
19 | padding: 0 6px;
20 | margin: 0;
21 | width: auto;
22 | max-width: inherit;
23 | }
24 | .bootstrap-tagsinput.form-control input::-moz-placeholder {
25 | color: #777;
26 | opacity: 1;
27 | }
28 | .bootstrap-tagsinput.form-control input:-ms-input-placeholder {
29 | color: #777;
30 | }
31 | .bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
32 | color: #777;
33 | }
34 | .bootstrap-tagsinput input:focus {
35 | border: none;
36 | box-shadow: none;
37 | }
38 | .bootstrap-tagsinput .tag {
39 | margin-right: 2px;
40 | color: white;
41 | }
42 | .bootstrap-tagsinput .tag [data-role="remove"] {
43 | margin-left: 8px;
44 | cursor: pointer;
45 | }
46 | .bootstrap-tagsinput .tag [data-role="remove"]:after {
47 | content: "x";
48 | padding: 0px 2px;
49 | }
50 | .bootstrap-tagsinput .tag [data-role="remove"]:hover {
51 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
52 | }
53 | .bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
54 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
55 | }
56 |
--------------------------------------------------------------------------------
/src/gobookmark/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os/user"
9 | "path/filepath"
10 | "regexp"
11 | "strings"
12 | )
13 |
14 | func absPath(path string) (string, error) {
15 | usr, _ := user.Current()
16 | dir := usr.HomeDir
17 |
18 | if path[:2] == "~/" {
19 | path = strings.Replace(path, "~", dir, 1)
20 | }
21 |
22 | return filepath.Abs(path)
23 | }
24 |
25 | func appendHttp(url string) string {
26 | if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
27 | return "http://" + url
28 | }
29 | return url
30 | }
31 |
32 | func assetFS() http.FileSystem {
33 | for k := range _bintree.Children {
34 | return http.Dir(k)
35 | }
36 | panic("unreachable")
37 | }
38 |
39 | func extractPageTitle(url string) (string, error) {
40 | resp, err := http.Get(url)
41 | if err != nil {
42 | return "", err
43 | }
44 | body, err := ioutil.ReadAll(resp.Body)
45 | checkErr(err)
46 |
47 | re := regexp.MustCompile("(.*?)")
48 | result := re.FindStringSubmatch(string(body))
49 | if len(result) == 2 {
50 | return result[1], nil
51 | }
52 | return "", errors.New(
53 | fmt.Sprintf("... not found in %s url", url),
54 | )
55 | }
56 |
57 | func checkErr(err error) {
58 | if err != nil {
59 | panic(err)
60 | }
61 | }
62 |
63 | func extractTags(search string) (result []string) {
64 | re := regexp.MustCompile("\\[(.*?)\\]")
65 |
66 | for _, submatch := range re.FindAllStringSubmatch(search, -1) {
67 | if len(submatch) > 1 {
68 | result = append(result, strings.TrimSpace(submatch[1]))
69 | }
70 | }
71 |
72 | return result
73 | }
74 |
75 | func removeTags(search string) string {
76 | re := regexp.MustCompile("(\\[.*?\\])")
77 | return strings.TrimSpace(re.ReplaceAllString(search, ""))
78 | }
79 |
--------------------------------------------------------------------------------
/src/gobookmark/templates/edit.html:
--------------------------------------------------------------------------------
1 | {{ template "layout" . }}
2 | {{ define "content" }}
3 |
52 | {{ end }}
53 |
--------------------------------------------------------------------------------
/src/gobookmark/templates/index.html:
--------------------------------------------------------------------------------
1 | {{ template "layout" . }}
2 | {{ define "content" }}
3 |
4 |
5 | {{ .TotalLinks }} links
6 |
7 |
8 |
9 | {{ if gt .Page.TotalPages 1 }}
10 |
11 | {{ template "paginate" .Page }}
12 |
13 | {{ end }}
14 |
15 |
16 |
17 | Links per page :
18 |
25 |
19 |
50 |
20 |
100
21 |
22 |
23 |
24 |
25 | {{ range $row := .Bms }}
26 | -
27 | {{ $row.Title }}
28 |
29 |
{{ $row.CreateDate }}
30 | -
31 |
{{ $row.Url }}
32 | {{ if getContextBool "login" }}
33 |
34 |
35 | {{ end }}
36 |
37 |
42 |
43 | {{ end }}
44 |
45 |
46 | {{ if gt .Page.TotalPages 1 }}
47 |
48 | {{ template "paginate" .Page }}
49 |
50 | {{ end }}
51 |
52 |
53 |
54 | {{ end }}
55 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | export GOPATH := $(PWD)
2 | export GOBIN := $(PWD)/bin
3 | export PATH := $(PWD):$(PATH)
4 | export GO15VENDOREXPERIMENT=1
5 |
6 | BINARY=gobookmark
7 |
8 | VERSION=0.1.0
9 | BUILD_TIME=`date +%FT%T%z`
10 |
11 | build_bindata = bin/go-bindata $(1) -o src/gobookmark/bindata.go -prefix src/gobookmark/ src/gobookmark/public/... src/gobookmark/templates/... src/gobookmark/migrations/...
12 | build_gobookmark = go build -v -o bin/${BINARY} gobookmark
13 |
14 | .PHONY: all
15 | all: install serve
16 |
17 | .PHONY: install
18 | install:
19 | go get -u github.com/Masterminds/glide/...
20 | go get -u github.com/jteeuwen/go-bindata/...
21 | cd src/gobookmark/; $(PWD)/bin/glide install; $(PWD)/bin/glide rebuild
22 | $(call build_bindata,-debug)
23 | $(call build_gobookmark)
24 |
25 | .PHONY: build
26 | build:
27 | $(call build_gobookmark)
28 |
29 | .PHONY: serve
30 | serve:
31 | $(call build_gobookmark)
32 | bin/${BINARY}
33 |
34 |
35 | .PHONY: assets
36 | assets:
37 | $(call build_bindata,-debug)
38 |
39 | .PHONY: release
40 | release:
41 | mkdir -p releases/darwin_amd64/
42 | mkdir -p releases/linux_amd64/
43 | $(call build_bindata)
44 | go build -o releases/darwin_amd64/${BINARY} gobookmark
45 | docker run -it --rm -v $(PWD):/usr/src/myapp -w /usr/src/myapp golang:1.6 bash -c "export CGO_ENABLED=1; export GOPATH=/usr/src/myapp/; go build -ldflags '-s' -o releases/linux_amd64/${BINARY} gobookmark"
46 | $(call build_bindata,-debug)
47 | cd releases/darwin_amd64/; tar czf ../gobookmark_darwin_amd64.tar.gz gobookmark
48 | cd releases/linux_amd64/; tar czf ../gobookmark_linux_amd64.tar.gz gobookmark
49 |
50 | .PHONY: test
51 | test:
52 | go test gobookmark -v
53 |
54 | .PHONY: clean
55 | clean:
56 | rm -rf bin/ releases/ pkg/ src/gobookmark/vendor/ src/gopkg.in/ src/github.com/
57 |
58 | .PHONY: build-docker
59 | build-docker: release
60 | docker build -t gobookmark .
61 |
62 | .PHONY: push-docker
63 | push-docker: build-docker
64 | docker tag gobookmark:latest docker.santa-maria.io/stephane/gobookmark
65 | docker push docker.santa-maria.io/stephane/gobookmark
66 |
--------------------------------------------------------------------------------
/src/gobookmark/public/css/screen.css:
--------------------------------------------------------------------------------
1 | .links {
2 | list-style: none;
3 | padding: 0;
4 | margin-top: 20px;
5 | }
6 |
7 | .links .link-title {
8 | color: black;
9 | /*font-weight: bold;*/
10 | font-size: 1.2em;
11 | }
12 |
13 | .links > LI {
14 | line-height: 1.5em;
15 | padding: 15px 15px;
16 | border-bottom: 1px solid #aaa;
17 | background: transparent linear-gradient(#F2F2F2, #FFF) repeat scroll 0% 0%;
18 | }
19 |
20 | .links > LI .line2 {
21 | font-size: 0.8em;
22 | }
23 |
24 | .bootstrap-tagsinput {
25 | width: 100%;
26 | }
27 |
28 | .tags {
29 | list-style: none;
30 | padding: 0;
31 | }
32 |
33 | .tags > LI {
34 | display: inline;
35 | }
36 |
37 | .tags > LI:after {
38 | content: ","
39 | }
40 |
41 | .tags > LI:last-child:after {
42 | content: ""
43 | }
44 |
45 | .navbar-default {
46 | background-color: #345;
47 | background-image: none;
48 | border: 0;
49 | border-bottom: 1px solid #234;
50 | border-radius: 0;
51 | }
52 |
53 | .navbar-default .navbar-brand {
54 | color: #ddd;
55 | text-shadow: 0 1px 1px #444;
56 | border-right: 1px solid #234;
57 | outline: 1px solid #456;
58 | }
59 |
60 | .navbar-login {
61 | color: #ddd;
62 | text-shadow: 0 1px 1px #444;
63 | border-left: 1px solid #234;
64 | outline: 1px solid #456;
65 | }
66 |
67 | .navbar-default .navbar-brand:hover {
68 | color: #fff;
69 | text-shadow: 0 1px 1px #444;
70 | }
71 |
72 | .navbar-default .navbar-nav > li > a {
73 | color: #ddd;
74 | text-shadow: 0 1px 1px #444;
75 | }
76 |
77 | .navbar-default .navbar-nav > li > a:hover {
78 | color: #fff;
79 | text-shadow: 0 1px 1px #444;
80 | }
81 |
82 | .items_per_page A.active {
83 | color: black;
84 | font-weight: bold;
85 | }
86 |
87 | .navbar-form {
88 | padding: 0;
89 | margin-left: 10px;
90 | margin-right: 10px;
91 | }
92 |
93 | FOOTER {
94 | border-top: 1px solid #d6d6d6;
95 | background-color: #eee;
96 | text-align: center;
97 | padding: 10px;
98 | font-size: small;
99 | }
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gobookmark
2 |
3 | Inspired by [Shaarli](https://github.com/sebsauvage/Shaarli) (Delicious clone), easier to install (only one standalone executable).
4 |
5 | Built with Go - GoBookmark runs without installation on multiple platforms.
6 |
7 | [](https://hub.docker.com/r/harobed/gobookmark/)
8 | [](https://gitter.im/harobed/gobookmark)
9 |
10 |
11 | ## Overview
12 |
13 | GoBookmark is a personnal web bookmark web service with :
14 |
15 | * tags support
16 | * plain text search engine
17 | * no dependencies, only based on filesystem (SQLite + [Bleve](http://www.blevesearch.com/))
18 |
19 | ## Getting started
20 |
21 | 1. [Download the latest release](https://github.com/harobed/gobookmark/releases/) of GoBookmark for your platform
22 | 2. Configure GoBookmark, or use the default settings
23 |
24 | ```
25 | $ ./gobookmark web
26 | [negroni] listening on localhost:8000
27 | ```
28 |
29 | Default password is ```password```.
30 |
31 | More info :
32 |
33 | ```
34 | $ bin/gobookmark --help
35 | NAME:
36 | gobookmark - A personnal bookmark service
37 |
38 | USAGE:
39 | gobookmark [global options] command [command options] [arguments...]
40 |
41 | VERSION:
42 | 0.1.0
43 |
44 | COMMANDS:
45 | web Start Gobookmark web server
46 | import Import bookmark HTML file
47 | reindex Execute plain text search indexation
48 | help, h Shows a list of commands or help for specific command
49 |
50 | GLOBAL OPTIONS:
51 | --data, -d "gobookmark" Database filename [$GOBOOKMARK_DATABASE]
52 | --help, -h show help
53 | --version, -v print the version
54 | ```
55 |
56 |
57 | ## Screenshots
58 |
59 | ***
60 |
61 | 
62 |
63 | ***
64 |
65 | 
66 |
67 | ***
68 |
69 | ## Contributions
70 |
71 | ### Build
72 |
73 | On OSX :
74 |
75 | $ brew install go
76 | $ go --version
77 | go version go1.6 darwin/amd64
78 |
79 | Download source :
80 |
81 | $ git clone git@gitlab.stephane-klein.info:stephane-klein/gobookmark.git
82 |
83 | Build and execute :
84 |
85 | $ cd gobookmark
86 | $ make install
87 | $ make serve
88 | [negroni] listening on :8080
89 |
90 |
91 | ### Test
92 |
93 | $ make test
94 |
95 |
96 | ### Build Docker image
97 |
98 | $ docker build -t gobookmark .
99 |
100 | Next push image :
101 |
102 | $ docker tag gobookmark:latest harobed/gobookmark
103 | $ docker push harobed/gobookmark
104 |
105 |
106 | ## Why I did this project ?
107 |
108 | It's my second Golang project, my first Golang web application, it's also a training project.
109 |
110 | What I've learned in this project :
111 |
112 | * how to use [go-bindata](https://github.com/jteeuwen/go-bindata) to include all assets in a standalone binary
113 | * how to use [migrate](https://github.com/mattes/migrate/), a database migration tool, it's very simple to apply, very KISS
114 | * how to use [bleve](github.com/blevesearch/bleve), a modern text indexing library, it's less powerful than [ElasticSearch](https://github.com/elastic/elasticsearch) but
115 | Bleve is embeded in *gobookmark* standalone executable
116 | * how to use [glide](https://github.com/Masterminds/glide) to handle dependencies (after played with [govendor](https://github.com/kardianos/govendor), [godeps](https://github.com/tools/godep)…)
117 | * how to use the [httptreemux](https://github.com/dimfeld/httptreemux) High-speed, flexible tree-based HTTP router
118 | * how to use [gorilla/context](https://github.com/gorilla/context) a golang registry for global request variables
119 | * how to use default golang template engine and database api
120 |
--------------------------------------------------------------------------------
/src/gobookmark/templates/layout.html:
--------------------------------------------------------------------------------
1 | {{ define "layout" }}
2 |
3 |
4 |
5 |
6 |
7 | GoBookmark
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
67 |
68 |
69 |
70 | {{ template "content" . }}
71 |
72 |
73 |
78 |
79 |
80 | {{ end }}
81 |
--------------------------------------------------------------------------------
/src/gobookmark/assetfs.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "io"
7 | "io/ioutil"
8 | "net/http"
9 | "os"
10 | "path"
11 | "path/filepath"
12 | "time"
13 | )
14 |
15 | var (
16 | defaultFileTimestamp = time.Now()
17 | )
18 |
19 | // FakeFile implements os.FileInfo interface for a given path and size
20 | type FakeFile struct {
21 | // Path is the path of this file
22 | Path string
23 | // Dir marks of the path is a directory
24 | Dir bool
25 | // Len is the length of the fake file, zero if it is a directory
26 | Len int64
27 | // Timestamp is the ModTime of this file
28 | Timestamp time.Time
29 | }
30 |
31 | func (f *FakeFile) Name() string {
32 | _, name := filepath.Split(f.Path)
33 | return name
34 | }
35 |
36 | func (f *FakeFile) Mode() os.FileMode {
37 | mode := os.FileMode(0644)
38 | if f.Dir {
39 | return mode | os.ModeDir
40 | }
41 | return mode
42 | }
43 |
44 | func (f *FakeFile) ModTime() time.Time {
45 | return f.Timestamp
46 | }
47 |
48 | func (f *FakeFile) Size() int64 {
49 | return f.Len
50 | }
51 |
52 | func (f *FakeFile) IsDir() bool {
53 | return f.Mode().IsDir()
54 | }
55 |
56 | func (f *FakeFile) Sys() interface{} {
57 | return nil
58 | }
59 |
60 | // AssetFile implements http.File interface for a no-directory file with content
61 | type AssetFile struct {
62 | *bytes.Reader
63 | io.Closer
64 | FakeFile
65 | }
66 |
67 | func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
68 | if timestamp.IsZero() {
69 | timestamp = defaultFileTimestamp
70 | }
71 | return &AssetFile{
72 | bytes.NewReader(content),
73 | ioutil.NopCloser(nil),
74 | FakeFile{name, false, int64(len(content)), timestamp}}
75 | }
76 |
77 | func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) {
78 | return nil, errors.New("not a directory")
79 | }
80 |
81 | func (f *AssetFile) Size() int64 {
82 | return f.FakeFile.Size()
83 | }
84 |
85 | func (f *AssetFile) Stat() (os.FileInfo, error) {
86 | return f, nil
87 | }
88 |
89 | // AssetDirectory implements http.File interface for a directory
90 | type AssetDirectory struct {
91 | AssetFile
92 | ChildrenRead int
93 | Children []os.FileInfo
94 | }
95 |
96 | func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
97 | fileinfos := make([]os.FileInfo, 0, len(children))
98 | for _, child := range children {
99 | _, err := fs.AssetDir(filepath.Join(name, child))
100 | fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}})
101 | }
102 | return &AssetDirectory{
103 | AssetFile{
104 | bytes.NewReader(nil),
105 | ioutil.NopCloser(nil),
106 | FakeFile{name, true, 0, time.Time{}},
107 | },
108 | 0,
109 | fileinfos}
110 | }
111 |
112 | func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
113 | if count <= 0 {
114 | return f.Children, nil
115 | }
116 | if f.ChildrenRead+count > len(f.Children) {
117 | count = len(f.Children) - f.ChildrenRead
118 | }
119 | rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
120 | f.ChildrenRead += count
121 | return rv, nil
122 | }
123 |
124 | func (f *AssetDirectory) Stat() (os.FileInfo, error) {
125 | return f, nil
126 | }
127 |
128 | // AssetFS implements http.FileSystem, allowing
129 | // embedded files to be served from net/http package.
130 | type AssetFS struct {
131 | // Asset should return content of file in path if exists
132 | Asset func(path string) ([]byte, error)
133 | // AssetDir should return list of files in the path
134 | AssetDir func(path string) ([]string, error)
135 | // AssetInfo should return the info of file in path if exists
136 | AssetInfo func(path string) (os.FileInfo, error)
137 | // Prefix would be prepended to http requests
138 | Prefix string
139 | }
140 |
141 | func (fs *AssetFS) Open(name string) (http.File, error) {
142 | name = path.Join(fs.Prefix, name)
143 | if len(name) > 0 && name[0] == '/' {
144 | name = name[1:]
145 | }
146 | if b, err := fs.Asset(name); err == nil {
147 | timestamp := defaultFileTimestamp
148 | if info, err := fs.AssetInfo(name); err == nil {
149 | timestamp = info.ModTime()
150 | }
151 | return NewAssetFile(name, b, timestamp), nil
152 | }
153 | if children, err := fs.AssetDir(name); err == nil {
154 | return NewAssetDirectory(name, children, fs), nil
155 | } else {
156 | return nil, err
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/gobookmark/gobookmark.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/PuerkitoBio/goquery"
6 | "github.com/cheggaaa/pb"
7 | "github.com/codegangsta/cli"
8 | "github.com/codegangsta/negroni"
9 | "github.com/dimfeld/httptreemux"
10 | "github.com/goincremental/negroni-sessions"
11 | "github.com/goincremental/negroni-sessions/cookiestore"
12 | "github.com/gorilla/context"
13 | "log"
14 | "net/http"
15 | "os"
16 | "path"
17 | "strconv"
18 | "strings"
19 | "time"
20 | )
21 |
22 | var (
23 | Password string
24 | )
25 |
26 | func stringFlag(name, value, usage string, envvar string) cli.StringFlag {
27 | return cli.StringFlag{
28 | Name: name,
29 | Value: value,
30 | Usage: usage,
31 | EnvVar: envvar,
32 | }
33 | }
34 |
35 | func openDatabases(filename string) {
36 | cwd, _ := os.Getwd()
37 | filename = path.Join(cwd, filename)
38 |
39 | db_filename := fmt.Sprintf("%s.db", filename)
40 | log.Printf("Use %s SqlLite database", db_filename)
41 | DB = openDatabase(db_filename)
42 |
43 | index_filename := fmt.Sprintf("%s.index", filename)
44 | log.Printf("Use %s Bleve database", index_filename)
45 | INDEX = openBleve(index_filename)
46 | }
47 |
48 | func resetDatabases(filename string) {
49 | cwd, _ := os.Getwd()
50 | filename = path.Join(cwd, filename)
51 |
52 | db_filename := fmt.Sprintf("%s.db", filename)
53 | log.Printf("Reset %s SqlLite database", db_filename)
54 | os.Remove(db_filename)
55 |
56 | index_filename := fmt.Sprintf("%s.index", filename)
57 | log.Printf("Reset %s Bleve database", index_filename)
58 | os.RemoveAll(index_filename)
59 | }
60 |
61 | const default_items_by_page = 25
62 |
63 | func init() {
64 | Password = "password"
65 | }
66 |
67 | func GlobalVariableMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
68 | context.Set(r, "login", false)
69 | context.Set(r, "index_page", false)
70 | next(rw, r)
71 | }
72 |
73 | func initApp() *negroni.Negroni {
74 | router := httptreemux.New()
75 | router.GET("/", Index)
76 | router.GET("/add/", Edit)
77 | router.GET("/fetch-title/", FetchTitle)
78 | router.POST("/add/", Save)
79 | router.GET("/:id/delete/", Delete)
80 | router.GET("/:id/edit/", Edit)
81 | router.POST("/:id/edit/", Save)
82 | router.GET("/login/", LoginForm)
83 | router.POST("/login/", Login)
84 | router.GET("/logout/", Logout)
85 |
86 | n := negroni.Classic()
87 |
88 | store := cookiestore.New([]byte("secret123"))
89 | n.Use(sessions.Sessions("my_session", store))
90 | n.Use(negroni.HandlerFunc(GlobalVariableMiddleware))
91 | n.Use(negroni.NewStatic(
92 | &AssetFS{
93 | Asset: Asset,
94 | AssetDir: AssetDir,
95 | AssetInfo: AssetInfo,
96 | Prefix: "public",
97 | },
98 | ))
99 |
100 | n.UseHandler(router)
101 | return n
102 | }
103 |
104 | func importFile(filename string) {
105 | filename, err := absPath(filename)
106 | checkErr(err)
107 | f, err := os.Open(filename)
108 | checkErr(err)
109 |
110 | doc, err := goquery.NewDocumentFromReader(f)
111 | checkErr(err)
112 | items := doc.Find("DT > A")
113 | bar := pb.StartNew(len(items.Nodes))
114 | items.Each(func(i int, s *goquery.Selection) {
115 | bar.Increment()
116 | add_date_str, _ := s.Attr("add_date")
117 | add_date_int, err := strconv.ParseInt(add_date_str, 10, 64)
118 | checkErr(err)
119 |
120 | stmt, err := DB.Prepare("INSERT INTO links (title, url, createdate) VALUES(?, ?, ?)")
121 | checkErr(err)
122 |
123 | href, _ := s.Attr("href")
124 |
125 | bm := new(BookmarkItem)
126 | bm.Title = s.Text()
127 | bm.Url = href
128 | bm.CreateDate = time.Unix(add_date_int, 0)
129 |
130 | res, err := stmt.Exec(
131 | bm.Title,
132 | bm.Url,
133 | bm.CreateDate,
134 | )
135 | checkErr(err)
136 |
137 | bm.Id, err = res.LastInsertId()
138 | tags, _ := s.Attr("tags")
139 | updateLinksTags(bm.Id, strings.Split(tags, ","))
140 |
141 | // Index
142 |
143 | bm.Tags = getLinksTags(bm.Id)
144 | indexBookmarkItem(bm)
145 | })
146 | bar.FinishPrint("The End!")
147 | }
148 |
149 | func main() {
150 | app := cli.NewApp()
151 | app.Name = "gobookmark"
152 | app.Version = "0.1.0"
153 | app.Usage = "A personnal bookmark service"
154 | app.Flags = []cli.Flag{
155 | stringFlag("data, d", "gobookmark", "Database filename", "GOBOOKMARK_DATABASE"),
156 | }
157 | app.Commands = []cli.Command{
158 | {
159 | Name: "web",
160 | Usage: "Start Gobookmark web server",
161 | Description: `Gobookmark web server is the only thing you need to run,
162 | and it takes care of all the other things for you`,
163 | Flags: []cli.Flag{
164 | stringFlag("port, p", "8000", "Web server port", "GOBOOKMARK_PORT"),
165 | stringFlag("host", "localhost", "Web server host", "GOBOOKMARK_HOST"),
166 | stringFlag("password", "password", "Set login password", "GOBOOKMARK_PASSWORD"),
167 | },
168 | Action: func(c *cli.Context) {
169 | Password = c.String("password")
170 | openDatabases(c.Parent().String("data"))
171 | n := initApp()
172 | n.Run(fmt.Sprintf("%s:%s", c.String("host"), c.String("port")))
173 | },
174 | },
175 | {
176 | Name: "import",
177 | Usage: "Import bookmark HTML file",
178 | Flags: []cli.Flag{
179 | cli.BoolFlag{
180 | Name: "reset, r",
181 | Usage: "Reset database before importation",
182 | },
183 | },
184 | ArgsUsage: "",
185 | Action: func(c *cli.Context) {
186 | if len(c.Args()) == 0 {
187 | log.Print("Error : missing")
188 | } else {
189 | if c.Bool("reset") {
190 | resetDatabases(c.Parent().String("data"))
191 | }
192 | openDatabases(c.Parent().String("data"))
193 | importFile(c.Args()[0])
194 | }
195 | },
196 | },
197 | {
198 | Name: "reindex",
199 | Usage: "Execute plain text search indexation",
200 | Action: func(c *cli.Context) {
201 | openDatabases(c.Parent().String("data"))
202 | log.Print("Reindex database with Bleve")
203 | indexAllBookmark()
204 | },
205 | },
206 | }
207 | app.Run(os.Args)
208 | }
209 |
--------------------------------------------------------------------------------
/src/gobookmark/views.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/Unknwon/paginater"
5 | "github.com/arschles/go-bindata-html-template"
6 | "github.com/goincremental/negroni-sessions"
7 | "github.com/gorilla/context"
8 | "net/http"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | func getTemplate(r *http.Request, template_name string) *template.Template {
14 | funcMap := template.FuncMap{
15 | "paginate_url": func(page int) string {
16 | values := r.URL.Query()
17 | values.Del("page")
18 | values.Add("page", strconv.Itoa(page))
19 | r.URL.RawQuery = values.Encode()
20 | return r.URL.String()
21 | },
22 | "per_page_url": func(per_page int) string {
23 | values := r.URL.Query()
24 | values.Del("items_by_page")
25 | values.Add("items_by_page", strconv.Itoa(per_page))
26 | r.URL.RawQuery = values.Encode()
27 | return r.URL.String()
28 | },
29 | "getContextBool": func(key string) bool {
30 | return context.Get(r, key).(bool)
31 | },
32 | }
33 | t, err := template.New("mytmpl", Asset).Funcs(funcMap).ParseFiles(
34 | template_name,
35 | "templates/layout.html",
36 | "templates/includes/paginate.html",
37 | )
38 | checkErr(err)
39 | return t
40 | }
41 |
42 | func Index(w http.ResponseWriter, r *http.Request, _ map[string]string) {
43 | session := sessions.GetSession(r)
44 | t := getTemplate(r, "templates/index.html")
45 |
46 | page, err := strconv.Atoi(r.URL.Query().Get("page"))
47 | if err != nil {
48 | page = 1
49 | }
50 |
51 | items_by_page, err := strconv.Atoi(r.URL.Query().Get("items_by_page"))
52 | if err != nil {
53 | items_by_page = default_items_by_page
54 | }
55 |
56 | total_links := countLinks("")
57 | var result_total int
58 |
59 | var bms []*BookmarkItem
60 | search := r.URL.Query().Get("search")
61 | if search != "" {
62 | result_total, bms = searchBookmark(search, page, items_by_page)
63 | } else {
64 | bms = queryBookmark(page, items_by_page, r.URL.Query().Get("tags"))
65 | result_total = countLinks(r.URL.Query().Get("tags"))
66 | }
67 |
68 | data := struct {
69 | Bms []*BookmarkItem
70 | Page *paginater.Paginater
71 | TotalLinks int
72 | ItemsByPage int
73 | Search string
74 | }{
75 | Bms: bms,
76 | Page: paginater.New(result_total, items_by_page, page, 9),
77 | TotalLinks: total_links,
78 | ItemsByPage: items_by_page,
79 | Search: search,
80 | }
81 |
82 | context.Set(r, "index_page", true)
83 | if session.Get("login") != nil {
84 | context.Set(r, "login", true)
85 | }
86 |
87 | err = t.Execute(w, data)
88 | }
89 |
90 | func Edit(w http.ResponseWriter, r *http.Request, params map[string]string) {
91 | session := sessions.GetSession(r)
92 | t := getTemplate(r, "templates/edit.html")
93 |
94 | var bookmark_item BookmarkItem
95 |
96 | if _, ok := params["id"]; ok {
97 | id, err := strconv.ParseInt(params["id"], 10, 64)
98 | checkErr(err)
99 |
100 | bookmark_item = *getBookmark(id)
101 | } else {
102 | url := appendHttp(r.URL.Query().Get("url"))
103 | title, err := extractPageTitle(url)
104 | if err != nil {
105 | title = ""
106 | }
107 | bookmark_item = BookmarkItem{
108 | 0,
109 | url,
110 | title,
111 | time.Time{},
112 | nil,
113 | }
114 | }
115 |
116 | data := struct {
117 | Item BookmarkItem
118 | }{
119 | Item: bookmark_item,
120 | }
121 | if session.Get("login") != nil {
122 | context.Set(r, "login", true)
123 | }
124 |
125 | err := t.Execute(w, data)
126 | checkErr(err)
127 | }
128 |
129 | func Save(w http.ResponseWriter, r *http.Request, params map[string]string) {
130 | session := sessions.GetSession(r)
131 | if session.Get("login") == nil {
132 | http.Redirect(w, r, "../../", 303)
133 | return
134 | }
135 |
136 | var link_id int64
137 | var err error
138 | if _, ok := params["id"]; ok {
139 | link_id, err = strconv.ParseInt(params["id"], 10, 64)
140 | checkErr(err)
141 |
142 | updateLink(
143 | link_id,
144 | r.FormValue("title"),
145 | appendHttp(r.FormValue("url")),
146 | r.FormValue("tags"),
147 | )
148 | } else {
149 | link_id = insertLink(
150 | r.FormValue("title"),
151 | appendHttp(r.FormValue("url")),
152 | r.FormValue("tags"),
153 | )
154 | }
155 | bookmark_item := getBookmark(link_id)
156 | indexBookmarkItem(bookmark_item)
157 |
158 | http.Redirect(w, r, "../../", 303)
159 | }
160 |
161 | func Delete(w http.ResponseWriter, r *http.Request, params map[string]string) {
162 | session := sessions.GetSession(r)
163 | if session.Get("login") == nil {
164 | http.Redirect(w, r, "../../", 303)
165 | return
166 | }
167 |
168 | stmt, err := DB.Prepare("DELETE FROM links WHERE id=?")
169 | checkErr(err)
170 |
171 | _, err = stmt.Exec(params["id"])
172 | checkErr(err)
173 |
174 | http.Redirect(w, r, "../../", 303)
175 | }
176 |
177 | func LoginForm(w http.ResponseWriter, r *http.Request, _ map[string]string) {
178 | session := sessions.GetSession(r)
179 | t := getTemplate(r, "templates/login.html")
180 |
181 | data := struct {
182 | Error string
183 | }{
184 | Error: "",
185 | }
186 |
187 | errors := session.Flashes("errors")
188 | if len(errors) > 0 {
189 | data.Error = errors[0].(string)
190 | }
191 |
192 | t.Execute(w, data)
193 | }
194 |
195 | func Login(w http.ResponseWriter, r *http.Request, _ map[string]string) {
196 | session := sessions.GetSession(r)
197 | if r.FormValue("password") == Password {
198 | session.Set("login", true)
199 | http.Redirect(w, r, "../", 303)
200 | } else {
201 | session.AddFlash("Password invalid", "errors")
202 | http.Redirect(w, r, ".", 303)
203 | }
204 | }
205 |
206 | func Logout(w http.ResponseWriter, r *http.Request, _ map[string]string) {
207 | session := sessions.GetSession(r)
208 | session.Delete("login")
209 | http.Redirect(w, r, "../", 303)
210 | }
211 |
212 | func FetchTitle(w http.ResponseWriter, r *http.Request, _ map[string]string) {
213 | url := appendHttp(r.URL.Query().Get("url"))
214 | title, err := extractPageTitle(url)
215 | if err != nil {
216 | title = ""
217 | }
218 | w.Write([]byte(title))
219 | }
220 |
--------------------------------------------------------------------------------
/src/gobookmark/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "database/sql"
6 | "github.com/stretchr/testify/assert"
7 | "io/ioutil"
8 | "net/http"
9 | "net/http/cookiejar"
10 | "net/http/httptest"
11 | "net/url"
12 | "os"
13 | "testing"
14 | )
15 |
16 | func openTestDatabase() *sql.DB {
17 | const test_database = "gobookmark-test.db"
18 | const test_bleve = "gobookmark-test.index"
19 |
20 | if _, err := os.Stat(test_database); err == nil {
21 | os.Remove(test_database)
22 | }
23 |
24 | if _, err := os.Stat(test_bleve); err == nil {
25 | os.RemoveAll(test_bleve)
26 | }
27 |
28 | INDEX = openBleve(test_bleve)
29 |
30 | return openDatabase(test_database)
31 | }
32 |
33 | func TestIndex(t *testing.T) {
34 | DB = openTestDatabase()
35 | defer DB.Close()
36 | app := initApp()
37 | server := httptest.NewServer(app)
38 | defer server.Close()
39 |
40 | cookieJar, _ := cookiejar.New(nil)
41 | client := &http.Client{
42 | Jar: cookieJar,
43 | }
44 |
45 | resp, _ := client.Get(server.URL + "/")
46 | assert.Equal(t, resp.StatusCode, http.StatusOK)
47 | }
48 |
49 | func TestLogin(t *testing.T) {
50 | DB = openTestDatabase()
51 | defer DB.Close()
52 | app := initApp()
53 | server := httptest.NewServer(app)
54 | defer server.Close()
55 |
56 | cookieJar, _ := cookiejar.New(nil)
57 | client := &http.Client{
58 | Jar: cookieJar,
59 | }
60 |
61 | resp, _ := client.Get(server.URL + "/login/")
62 | assert.Equal(t, resp.StatusCode, http.StatusOK)
63 | assertResponseBodyContains(t, resp, "Password")
64 | }
65 |
66 | func assertResponseBodyContains(t *testing.T, resp *http.Response, contains string) {
67 | bodyBytes, _ := ioutil.ReadAll(resp.Body)
68 | resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
69 | assert.Contains(t, string(bodyBytes), contains)
70 | }
71 |
72 | func assertResponseBodyNotContains(t *testing.T, resp *http.Response, contains string) {
73 | body, _ := ioutil.ReadAll(resp.Body)
74 |
75 | assert.NotContains(t, string(body), contains)
76 | }
77 |
78 | func TestAddBookmark(t *testing.T) {
79 | DB = openTestDatabase()
80 | defer DB.Close()
81 | app := initApp()
82 | server := httptest.NewServer(app)
83 | defer server.Close()
84 |
85 | cookieJar, _ := cookiejar.New(nil)
86 | client := &http.Client{
87 | Jar: cookieJar,
88 | }
89 |
90 | resp, _ := client.Get(server.URL + "/add/?url=http://cv.stephane-klein.info")
91 | assert.Equal(t, resp.StatusCode, http.StatusOK)
92 | assertResponseBodyContains(t, resp, "Développeur")
93 | }
94 |
95 | func TestSaveNewBookmark(t *testing.T) {
96 | DB = openTestDatabase()
97 | defer DB.Close()
98 | app := initApp()
99 | server := httptest.NewServer(app)
100 | defer server.Close()
101 |
102 | cookieJar, _ := cookiejar.New(nil)
103 | client := &http.Client{
104 | Jar: cookieJar,
105 | }
106 | client.PostForm(
107 | server.URL+"/login/",
108 | url.Values{
109 | "password": {"password"},
110 | },
111 | )
112 |
113 | resp, _ := client.PostForm(
114 | server.URL+"/add/",
115 | url.Values{
116 | "url": {"http://cv.stephane-klein.info"},
117 | "title": {"Le CV de Stéphane Klein"},
118 | "tags": {"tag1,tag2,tag3"},
119 | },
120 | )
121 |
122 | assert.Equal(t, resp.StatusCode, http.StatusOK)
123 | assert.Equal(t, resp.Request.URL.Path, "/")
124 |
125 | assertResponseBodyContains(t, resp, "stephane")
126 | assertResponseBodyContains(t, resp, "tag2")
127 | }
128 |
129 | func TestDeleteBookmark(t *testing.T) {
130 | DB = openTestDatabase()
131 | defer DB.Close()
132 | app := initApp()
133 | server := httptest.NewServer(app)
134 | defer server.Close()
135 |
136 | cookieJar, _ := cookiejar.New(nil)
137 | client := &http.Client{
138 | Jar: cookieJar,
139 | }
140 | client.PostForm(
141 | server.URL+"/login/",
142 | url.Values{
143 | "password": {"password"},
144 | },
145 | )
146 |
147 | insertLink("Le Curriculum vitae de Stéphane Klein", "http://cv.stephane-klein.info", "")
148 |
149 | resp, _ := client.Get(server.URL + "/1/delete/")
150 |
151 | assert.Equal(t, resp.StatusCode, http.StatusOK)
152 | assert.Equal(t, resp.Request.URL.Path, "/")
153 |
154 | assertResponseBodyNotContains(t, resp, "Curriculum")
155 | }
156 |
157 | func TestEditBookmark(t *testing.T) {
158 | DB = openTestDatabase()
159 | defer DB.Close()
160 | app := initApp()
161 | server := httptest.NewServer(app)
162 | defer server.Close()
163 |
164 | cookieJar, _ := cookiejar.New(nil)
165 | client := &http.Client{
166 | Jar: cookieJar,
167 | }
168 | client.PostForm(
169 | server.URL+"/login/",
170 | url.Values{
171 | "password": {"password"},
172 | },
173 | )
174 |
175 | insertLink("Le CV de Stéphane Klein", "http://cv.stephane-klein.info", "")
176 |
177 | resp, _ := client.Get(server.URL + "/1/edit/")
178 |
179 | assert.Equal(t, resp.StatusCode, http.StatusOK)
180 | assertResponseBodyContains(t, resp, "stephane")
181 |
182 | resp, _ = client.PostForm(
183 | server.URL+"/1/edit/",
184 | url.Values{
185 | "url": {"http://cv.noelie-deschamps.info"},
186 | "title": {"Le CV de Noëlie Deschamps"},
187 | },
188 | )
189 |
190 | assert.Equal(t, resp.StatusCode, http.StatusOK)
191 | assertResponseBodyContains(t, resp, "noelie")
192 | }
193 |
194 | func TestExtractPageTitle(t *testing.T) {
195 | title, _ := extractPageTitle("http://cv.stephane-klein.info")
196 | assert.Equal(t, title, "Curriculum vitæ de Stéphane Klein | CV | Développeur, Administrateur Système | 15 ans d'expérience")
197 |
198 | title, _ = extractPageTitle("https://golang.org/")
199 | assert.Equal(t, title, "The Go Programming Language")
200 | }
201 |
202 | func TestSearchByTags(t *testing.T) {
203 | DB = openTestDatabase()
204 | defer DB.Close()
205 | app := initApp()
206 | server := httptest.NewServer(app)
207 | defer server.Close()
208 |
209 | insertLink("AAAAAAAA", "http://example1.com", "python")
210 | insertLink("BBBBBBBB", "http://example2.com", "python")
211 | insertLink("CCCCCCCC", "http://example3.com", "python,golang")
212 | insertLink("DDDDDDDD", "http://example4.com", "python,golang")
213 | insertLink("EEEEEEEE", "http://example5.com", "golang")
214 | insertLink("FFFFFFFF", "http://example6.com", "golang")
215 | indexAllBookmark()
216 |
217 | total, _ := searchBookmark("[python]", 1, 10)
218 | assert.Equal(t, total, 4)
219 |
220 | total, _ = searchBookmark("[golang]", 1, 10)
221 | assert.Equal(t, total, 4)
222 |
223 | total, _ = searchBookmark("[golang][python]", 1, 10)
224 | assert.Equal(t, total, 2)
225 |
226 | total, bms := searchBookmark("[python] BBBBBBBB", 1, 10)
227 | assert.Equal(t, total, 4)
228 | assert.Equal(t, bms[0].Title, "BBBBBBBB")
229 | }
230 |
--------------------------------------------------------------------------------
/src/gobookmark/glide.yaml:
--------------------------------------------------------------------------------
1 | package: gobookmark
2 | import:
3 | - package: github.com/andybalholm/cascadia
4 | version: 3ad29d1ad1c4f2023e355603324348cf1f4b2d48
5 | - package: github.com/arschles/go-bindata-html-template
6 | version: 4ae2add2b2f4e490b61ce4e7fdfa622824318375
7 | - package: github.com/bitly/go-simplejson
8 | version: aabad6e819789e569bd6aabf444c935aa9ba1e44
9 | - package: github.com/blevesearch/bleve
10 | version: 96577606c33b4be0a9993a7daf581028f84849ee
11 | subpackages:
12 | - analysis
13 | - analysis/analyzers/standard_analyzer
14 | - analysis/byte_array_converters/json
15 | - analysis/datetime_parsers/datetime_optional
16 | - document
17 | - index
18 | - index/firestorm
19 | - index/store
20 | - index/store/boltdb
21 | - index/store/gtreap
22 | - index/upside_down
23 | - numeric_util
24 | - registry
25 | - search
26 | - search/collectors
27 | - search/facets
28 | - search/highlight/highlighters/html
29 | - search/searchers
30 | - analysis/language/en
31 | - analysis/token_filters/lower_case_filter
32 | - analysis/tokenizers/unicode
33 | - analysis/datetime_parsers/flexible_go
34 | - search/highlight
35 | - search/highlight/fragment_formatters/html
36 | - search/highlight/fragmenters/simple
37 | - search/highlight/highlighters/simple
38 | - search/scorers
39 | - analysis/token_filters/porter
40 | - analysis/token_filters/stop_tokens_filter
41 | - package: github.com/blevesearch/go-porterstemmer
42 | version: 23a2c8e5cf1f380f27722c6d2ae8896431dc7d0e
43 | - package: github.com/blevesearch/segment
44 | version: db70c57796cc8c310613541dfade3dce627d09c7
45 | - package: github.com/boj/redistore
46 | version: 9c0e6bab4dd444285424f189b8fb0cb03f653242
47 | - package: github.com/boltdb/bolt
48 | version: c2745b3c62985affcf08d0522135f4747e9b81f3
49 | - package: github.com/cheggaaa/pb
50 | version: c089c0e183064d83038db7c2ae1b711fb2e747a4
51 | - package: github.com/codegangsta/cli
52 | version: f9cc3001e04f9783cb4ad08ca6791aa07134787c
53 | - package: github.com/codegangsta/negroni
54 | version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
55 | - package: github.com/crowdmob/goamz
56 | version: 3a06871fe9fc0281ca90f3a7d97258d042ed64c0
57 | subpackages:
58 | - aws
59 | - dynamodb
60 | - dynamodb/dynamizer
61 | - ec2
62 | - elb
63 | - iam
64 | - s3
65 | - package: github.com/davecgh/go-spew
66 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
67 | subpackages:
68 | - spew
69 | - package: github.com/dchest/safefile
70 | version: 855e8d98f1852d48dde521e0522408d1fe7e836a
71 | - package: github.com/denizeren/dynamostore
72 | version: 69258d14eb58e5a5b894138d7f2f2609a5effc1f
73 | - package: github.com/dimfeld/httppath
74 | version: c8e499c3ef3c3e272ed8bdcc1ccf39f73c88debc
75 | - package: github.com/dimfeld/httptreemux
76 | version: f74b8868fbe8534a40358ce27b77818eeea2d6e0
77 | - package: github.com/extemporalgenome/slug
78 | version: 0320c85e32e0015b090f08a5d1324d5ab6094f07
79 | - package: github.com/feyeleanor/raw
80 | version: 724aedf6e1a5d8971aafec384b6bde3d5608fba4
81 | - package: github.com/feyeleanor/sets
82 | version: 6c54cb57ea406ff6354256a4847e37298194478f
83 | - package: github.com/feyeleanor/slices
84 | version: bb44bb2e4817fe71ba7082d351fd582e7d40e3ea
85 | - package: github.com/garyburd/redigo
86 | version: 836b6e58b3358112c8291565d01c35b8764070d7
87 | subpackages:
88 | - internal
89 | - redis
90 | - package: github.com/goincremental/dal
91 | version: 14fa60d0e842a8b7e8589b3fdb66c1d51762b11b
92 | - package: github.com/goincremental/negroni-sessions
93 | version: b07e2d18b97e3f627edaa67876c21fa23eba6a08
94 | subpackages:
95 | - cookiestore
96 | - package: github.com/golang/protobuf
97 | version: 5fc2294e655b78ed8a02082d37808d46c17d7e64
98 | subpackages:
99 | - proto
100 | - package: github.com/gorilla/context
101 | version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
102 | - package: github.com/gorilla/securecookie
103 | version: e95799a481bbcc3d01c2ad5178524cb8bec9f370
104 | - package: github.com/gorilla/sessions
105 | version: 99b16d2edd22458c541b3640ce83689b08a05c20
106 | - package: github.com/jteeuwen/go-bindata
107 | version: a0ff2567cfb70903282db057e799fd826784d41d
108 | - package: github.com/mattn/go-sqlite3
109 | version: 5651a9d9d49ec25811d08220ee972080378d52ea
110 | - package: github.com/pmezard/go-difflib
111 | version: e8554b8641db39598be7f6342874b958f12ae1d4
112 | subpackages:
113 | - difflib
114 | - package: github.com/PuerkitoBio/goquery
115 | version: 417cce822c7b9a379df5824be95228d177c5698b
116 | - package: github.com/steveyen/gtreap
117 | version: 0abe01ef9be25c4aedc174758ec2d917314d6d70
118 | - package: github.com/stretchr/objx
119 | version: 1a9d0bb9f541897e62256577b352fdbc1fb4fd94
120 | - package: github.com/stretchr/testify
121 | version: e3a8ff8ce36581f87a15341206f205b1da467059
122 | subpackages:
123 | - assert
124 | - http
125 | - mock
126 | - require
127 | - package: github.com/Unknwon/paginater
128 | version: 7748a72e01415173a27d79866b984328e7b0c12b
129 | - package: github.com/willf/bitset
130 | version: bb0da3785c4fe9d26f6029c77c8fce2aa4d0b291
131 | - package: golang.org/x/crypto
132 | version: f18420efc3b4f8e9f3d51f6bd2476e92c46260e9
133 | subpackages:
134 | - blowfish
135 | - cast5
136 | - curve25519
137 | - nacl/secretbox
138 | - openpgp/armor
139 | - openpgp/elgamal
140 | - openpgp/errors
141 | - openpgp/packet
142 | - openpgp/s2k
143 | - pbkdf2
144 | - pkcs12/internal/rc2
145 | - poly1305
146 | - salsa20/salsa
147 | - ssh
148 | - ssh/terminal
149 | - package: golang.org/x/net
150 | version: ea6dba8c93880aa07d6ebed83c3c680cd9faa63a
151 | subpackages:
152 | - context
153 | - html
154 | - html/atom
155 | - http2
156 | - http2/hpack
157 | - internal/iana
158 | - internal/timeseries
159 | - ipv4
160 | - ipv6
161 | - webdav/internal/xml
162 | - dict
163 | - package: golang.org/x/text
164 | version: cf4986612c83df6c55578ba198316d1684a9a287
165 | subpackages:
166 | - collate
167 | - collate/colltab
168 | - encoding
169 | - encoding/charmap
170 | - encoding/htmlindex
171 | - encoding/internal
172 | - encoding/internal/identifier
173 | - encoding/japanese
174 | - encoding/korean
175 | - encoding/simplifiedchinese
176 | - encoding/traditionalchinese
177 | - encoding/unicode
178 | - internal
179 | - internal/colltab
180 | - internal/format
181 | - internal/gen
182 | - internal/tag
183 | - internal/utf8internal
184 | - language
185 | - runes
186 | - transform
187 | - unicode/cldr
188 | - unicode/norm
189 | - package: gopkg.in/check.v1
190 | version: 11d3bc7aa68e238947792f30573146a3231fc0f1
191 | - package: gopkg.in/mgo.v2
192 | version: e30de8ac9ae3b30df7065f766c71f88bba7d4e49
193 | subpackages:
194 | - bson
195 | - internal/scram
196 | - package: gopkg.in/tomb.v2
197 | version: 14b3d72120e8d10ea6e6b7f87f7175734b1faab8
198 | - package: github.com/mattes/migrate
199 | version: 33fa6fda9d5bd8c47c69ac4a69a771eb24ce9599
200 | repo: https://github.com/harobed/migrate.git
201 | vcs: git
202 | - package: github.com/ikawaha/kagome
203 | - package: github.com/syndtr/goleveldb
204 | - package: github.com/rcrowley/go-metrics
205 | - package: github.com/go-sql-driver/mysql
206 | - package: github.com/gocql/gocql
207 | - package: github.com/lib/pq
208 |
--------------------------------------------------------------------------------
/src/gobookmark/glide.lock:
--------------------------------------------------------------------------------
1 | hash: c49a240434b485d1f176a990052c66e8f8922b55ed1bf234be8db2a242e9fdc7
2 | updated: 2016-03-29T22:59:59.869822375Z
3 | imports:
4 | - name: github.com/andybalholm/cascadia
5 | version: 3ad29d1ad1c4f2023e355603324348cf1f4b2d48
6 | - name: github.com/arschles/go-bindata-html-template
7 | version: 4ae2add2b2f4e490b61ce4e7fdfa622824318375
8 | - name: github.com/bitly/go-simplejson
9 | version: aabad6e819789e569bd6aabf444c935aa9ba1e44
10 | - name: github.com/blevesearch/bleve
11 | version: 96577606c33b4be0a9993a7daf581028f84849ee
12 | subpackages:
13 | - analysis
14 | - analysis/analyzers/standard_analyzer
15 | - analysis/byte_array_converters/json
16 | - analysis/datetime_parsers/datetime_optional
17 | - document
18 | - index
19 | - index/firestorm
20 | - index/store
21 | - index/store/boltdb
22 | - index/store/gtreap
23 | - index/upside_down
24 | - numeric_util
25 | - registry
26 | - search
27 | - search/collectors
28 | - search/facets
29 | - search/highlight/highlighters/html
30 | - search/searchers
31 | - analysis/language/en
32 | - analysis/token_filters/lower_case_filter
33 | - analysis/tokenizers/unicode
34 | - analysis/datetime_parsers/flexible_go
35 | - search/highlight
36 | - search/highlight/fragment_formatters/html
37 | - search/highlight/fragmenters/simple
38 | - search/highlight/highlighters/simple
39 | - search/scorers
40 | - analysis/token_filters/porter
41 | - analysis/token_filters/stop_tokens_filter
42 | - name: github.com/blevesearch/go-porterstemmer
43 | version: 23a2c8e5cf1f380f27722c6d2ae8896431dc7d0e
44 | - name: github.com/blevesearch/segment
45 | version: db70c57796cc8c310613541dfade3dce627d09c7
46 | - name: github.com/boj/redistore
47 | version: 9c0e6bab4dd444285424f189b8fb0cb03f653242
48 | - name: github.com/boltdb/bolt
49 | version: c2745b3c62985affcf08d0522135f4747e9b81f3
50 | - name: github.com/cheggaaa/pb
51 | version: c089c0e183064d83038db7c2ae1b711fb2e747a4
52 | - name: github.com/codegangsta/cli
53 | version: f9cc3001e04f9783cb4ad08ca6791aa07134787c
54 | - name: github.com/codegangsta/negroni
55 | version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
56 | - name: github.com/crowdmob/goamz
57 | version: 3a06871fe9fc0281ca90f3a7d97258d042ed64c0
58 | subpackages:
59 | - aws
60 | - dynamodb
61 | - dynamodb/dynamizer
62 | - ec2
63 | - elb
64 | - iam
65 | - s3
66 | - name: github.com/davecgh/go-spew
67 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
68 | subpackages:
69 | - spew
70 | - name: github.com/dchest/safefile
71 | version: 855e8d98f1852d48dde521e0522408d1fe7e836a
72 | - name: github.com/denizeren/dynamostore
73 | version: 69258d14eb58e5a5b894138d7f2f2609a5effc1f
74 | - name: github.com/dimfeld/httppath
75 | version: c8e499c3ef3c3e272ed8bdcc1ccf39f73c88debc
76 | - name: github.com/dimfeld/httptreemux
77 | version: f74b8868fbe8534a40358ce27b77818eeea2d6e0
78 | - name: github.com/extemporalgenome/slug
79 | version: 0320c85e32e0015b090f08a5d1324d5ab6094f07
80 | - name: github.com/feyeleanor/raw
81 | version: 724aedf6e1a5d8971aafec384b6bde3d5608fba4
82 | - name: github.com/feyeleanor/sets
83 | version: 6c54cb57ea406ff6354256a4847e37298194478f
84 | - name: github.com/feyeleanor/slices
85 | version: bb44bb2e4817fe71ba7082d351fd582e7d40e3ea
86 | - name: github.com/garyburd/redigo
87 | version: 836b6e58b3358112c8291565d01c35b8764070d7
88 | subpackages:
89 | - internal
90 | - redis
91 | - name: github.com/go-sql-driver/mysql
92 | version: 1421caf44f6464fd2ee8de694c7508ee13f92964
93 | - name: github.com/gocql/gocql
94 | version: 277df5b5d96f3f58606a6c52418463370c132e2a
95 | - name: github.com/goincremental/dal
96 | version: 14fa60d0e842a8b7e8589b3fdb66c1d51762b11b
97 | - name: github.com/goincremental/negroni-sessions
98 | version: b07e2d18b97e3f627edaa67876c21fa23eba6a08
99 | subpackages:
100 | - cookiestore
101 | - name: github.com/golang/protobuf
102 | version: 5fc2294e655b78ed8a02082d37808d46c17d7e64
103 | subpackages:
104 | - proto
105 | - name: github.com/gorilla/context
106 | version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
107 | - name: github.com/gorilla/securecookie
108 | version: e95799a481bbcc3d01c2ad5178524cb8bec9f370
109 | - name: github.com/gorilla/sessions
110 | version: 99b16d2edd22458c541b3640ce83689b08a05c20
111 | - name: github.com/ikawaha/kagome
112 | version: 053bff1aa00bb6d786a8bb9c49dc88ee930aa764
113 | - name: github.com/jteeuwen/go-bindata
114 | version: a0ff2567cfb70903282db057e799fd826784d41d
115 | - name: github.com/lib/pq
116 | version: 3cd0097429be7d611bb644ef85b42bfb102ceea4
117 | - name: github.com/mattes/migrate
118 | version: 33fa6fda9d5bd8c47c69ac4a69a771eb24ce9599
119 | repo: https://github.com/harobed/migrate.git
120 | vcs: git
121 | subpackages:
122 | - driver/sqlite3
123 | - file
124 | - migrate
125 | - driver
126 | - migrate/direction
127 | - pipe
128 | - name: github.com/mattn/go-sqlite3
129 | version: 5651a9d9d49ec25811d08220ee972080378d52ea
130 | - name: github.com/pmezard/go-difflib
131 | version: e8554b8641db39598be7f6342874b958f12ae1d4
132 | subpackages:
133 | - difflib
134 | - name: github.com/PuerkitoBio/goquery
135 | version: 417cce822c7b9a379df5824be95228d177c5698b
136 | - name: github.com/rcrowley/go-metrics
137 | version: eeba7bd0dd01ace6e690fa833b3f22aaec29af43
138 | - name: github.com/steveyen/gtreap
139 | version: 0abe01ef9be25c4aedc174758ec2d917314d6d70
140 | - name: github.com/stretchr/objx
141 | version: 1a9d0bb9f541897e62256577b352fdbc1fb4fd94
142 | - name: github.com/stretchr/testify
143 | version: e3a8ff8ce36581f87a15341206f205b1da467059
144 | subpackages:
145 | - assert
146 | - http
147 | - mock
148 | - require
149 | - name: github.com/syndtr/goleveldb
150 | version: 93fc893f2dadb96ffde441c7546cc67ea290a3a8
151 | - name: github.com/Unknwon/paginater
152 | version: 7748a72e01415173a27d79866b984328e7b0c12b
153 | - name: github.com/willf/bitset
154 | version: bb0da3785c4fe9d26f6029c77c8fce2aa4d0b291
155 | - name: golang.org/x/crypto
156 | version: f18420efc3b4f8e9f3d51f6bd2476e92c46260e9
157 | subpackages:
158 | - blowfish
159 | - cast5
160 | - curve25519
161 | - nacl/secretbox
162 | - openpgp/armor
163 | - openpgp/elgamal
164 | - openpgp/errors
165 | - openpgp/packet
166 | - openpgp/s2k
167 | - pbkdf2
168 | - pkcs12/internal/rc2
169 | - poly1305
170 | - salsa20/salsa
171 | - ssh
172 | - ssh/terminal
173 | - name: golang.org/x/net
174 | version: ea6dba8c93880aa07d6ebed83c3c680cd9faa63a
175 | subpackages:
176 | - context
177 | - html
178 | - html/atom
179 | - http2
180 | - http2/hpack
181 | - internal/iana
182 | - internal/timeseries
183 | - ipv4
184 | - ipv6
185 | - webdav/internal/xml
186 | - dict
187 | - name: golang.org/x/text
188 | version: cf4986612c83df6c55578ba198316d1684a9a287
189 | subpackages:
190 | - collate
191 | - collate/colltab
192 | - encoding
193 | - encoding/charmap
194 | - encoding/htmlindex
195 | - encoding/internal
196 | - encoding/internal/identifier
197 | - encoding/japanese
198 | - encoding/korean
199 | - encoding/simplifiedchinese
200 | - encoding/traditionalchinese
201 | - encoding/unicode
202 | - internal
203 | - internal/colltab
204 | - internal/format
205 | - internal/gen
206 | - internal/tag
207 | - internal/utf8internal
208 | - language
209 | - runes
210 | - transform
211 | - unicode/cldr
212 | - unicode/norm
213 | - name: gopkg.in/check.v1
214 | version: 11d3bc7aa68e238947792f30573146a3231fc0f1
215 | - name: gopkg.in/mgo.v2
216 | version: e30de8ac9ae3b30df7065f766c71f88bba7d4e49
217 | subpackages:
218 | - bson
219 | - internal/scram
220 | - name: gopkg.in/tomb.v2
221 | version: 14b3d72120e8d10ea6e6b7f87f7175734b1faab8
222 | devImports: []
223 |
--------------------------------------------------------------------------------
/src/gobookmark/models.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "github.com/blevesearch/bleve"
6 | "github.com/extemporalgenome/slug"
7 | _ "github.com/mattes/migrate/driver/sqlite3"
8 | "github.com/mattes/migrate/file"
9 | "github.com/mattes/migrate/migrate"
10 | _ "github.com/mattn/go-sqlite3"
11 | "log"
12 | "os"
13 | "strconv"
14 | "strings"
15 | "time"
16 | )
17 |
18 | var (
19 | DB *sql.DB
20 | INDEX bleve.Index
21 | )
22 |
23 | type Tag struct {
24 | Id int64
25 | Title string
26 | Slug string
27 | }
28 |
29 | type BookmarkItem struct {
30 | Id int64
31 | Url string
32 | Title string
33 | CreateDate time.Time
34 | Tags []*Tag
35 | }
36 |
37 | func countLinks(tags string) int {
38 | var count int
39 | if tags != "" {
40 | err := DB.QueryRow(
41 | `SELECT
42 | COUNT(links.id)
43 | FROM
44 | links
45 | LEFT JOIN
46 | rel_links_tags
47 | ON
48 | rel_links_tags.link_id = links.id
49 | LEFT JOIN
50 | tags
51 | ON
52 | rel_links_tags.tag_id = tags.id
53 | WHERE
54 | tags.slug IN (?)`, tags).Scan(&count)
55 | checkErr(err)
56 | } else {
57 | err := DB.QueryRow("SELECT COUNT(id) FROM links").Scan(&count)
58 | checkErr(err)
59 | }
60 | return count
61 | }
62 |
63 | func openDatabase(filename string) *sql.DB {
64 | migrate.NonGraceful()
65 | migrate.UseStore(file.AssetStore{
66 | Asset: Asset,
67 | AssetDir: AssetDir,
68 | })
69 | errors, ok := migrate.UpSync("sqlite3://"+filename, "migrations")
70 | if !ok {
71 | log.Fatalf("%v", errors)
72 | }
73 |
74 | db, err := sql.Open("sqlite3", filename)
75 | checkErr(err)
76 |
77 | return db
78 | }
79 |
80 | func indexBookmarkItem(item *BookmarkItem) error {
81 | x := struct {
82 | Id int64 `json:"id"`
83 | Url string `json:"url"`
84 | Title string `json:"title"`
85 | Tags string `json:"tags"`
86 | }{
87 | Id: item.Id,
88 | Url: item.Url,
89 | Title: item.Title,
90 | Tags: "",
91 | }
92 | for _, t := range item.Tags {
93 | x.Tags = x.Tags + " " + t.Slug
94 | }
95 | return INDEX.Index(strconv.FormatInt(item.Id, 10), x)
96 | }
97 |
98 | func indexAllBookmark() {
99 | rows, err := DB.Query("SELECT id, title, url, createdate FROM links")
100 | checkErr(err)
101 | defer rows.Close()
102 |
103 | for rows.Next() {
104 | bm := new(BookmarkItem)
105 |
106 | err := rows.Scan(&bm.Id, &bm.Title, &bm.Url, &bm.CreateDate)
107 | checkErr(err)
108 |
109 | bm.Tags = getLinksTags(bm.Id)
110 |
111 | indexBookmarkItem(bm)
112 | }
113 | }
114 |
115 | func openBleve(filename string) (index bleve.Index) {
116 | if _, err := os.Stat(filename); os.IsNotExist(err) {
117 | indexMapping := bleve.NewIndexMapping()
118 | linkMapping := bleve.NewDocumentMapping()
119 |
120 | linkTitleFieldMapping := bleve.NewTextFieldMapping()
121 | linkTitleFieldMapping.Analyzer = "en"
122 | linkMapping.AddFieldMappingsAt("title", linkTitleFieldMapping)
123 |
124 | linkUrlFieldMapping := bleve.NewTextFieldMapping()
125 | linkMapping.AddFieldMappingsAt("url", linkUrlFieldMapping)
126 |
127 | linkTagsFieldMapping := bleve.NewTextFieldMapping()
128 | linkMapping.AddFieldMappingsAt("tags", linkTagsFieldMapping)
129 |
130 | indexMapping.AddDocumentMapping("link", linkMapping)
131 |
132 | index, err = bleve.New(filename, indexMapping)
133 | checkErr(err)
134 | } else {
135 | index, err = bleve.Open(filename)
136 | checkErr(err)
137 | }
138 |
139 | return index
140 | }
141 |
142 | func getOrCreateTag(tag_name string) int64 {
143 | var id int64
144 | stmt, err := DB.Prepare("SELECT id FROM tags WHERE title=?")
145 | checkErr(err)
146 |
147 | rows, err := stmt.Query(tag_name)
148 | defer rows.Close()
149 | if rows.Next() {
150 | rows.Scan(&id)
151 | } else {
152 | stmt, err = DB.Prepare("INSERT INTO tags (title, slug) VALUES(?, ?)")
153 | checkErr(err)
154 | res, err := stmt.Exec(tag_name, slug.Slug(tag_name))
155 | id, err = res.LastInsertId()
156 | checkErr(err)
157 | }
158 | return id
159 | }
160 |
161 | func insertLink(title string, url string, tags string) (id int64) {
162 | stmt, err := DB.Prepare("INSERT INTO links (title, url) VALUES(?, ?)")
163 | checkErr(err)
164 |
165 | res, err := stmt.Exec(title, url)
166 | checkErr(err)
167 | link_id, err := res.LastInsertId()
168 | checkErr(err)
169 | updateLinksTags(link_id, strings.Split(tags, ","))
170 | return link_id
171 | }
172 |
173 | func updateLink(id int64, title string, url string, tags string) {
174 | stmt, err := DB.Prepare("UPDATE links SET title=?, url=? WHERE id=?")
175 | checkErr(err)
176 |
177 | _, err = stmt.Exec(title, url, id)
178 |
179 | updateLinksTags(id, strings.Split(tags, ","))
180 | }
181 |
182 | func updateLinksTags(link_id int64, tag_name_list []string) {
183 | stmt, err := DB.Prepare("DELETE FROM rel_links_tags WHERE link_id=?")
184 | checkErr(err)
185 | _, err = stmt.Exec(link_id)
186 | checkErr(err)
187 |
188 | for _, tag_name := range tag_name_list {
189 | tag_id := getOrCreateTag(tag_name)
190 | stmt, err = DB.Prepare("INSERT INTO rel_links_tags (link_id, tag_id) VALUES(?, ?)")
191 | _, err = stmt.Exec(link_id, tag_id)
192 | checkErr(err)
193 | }
194 | }
195 |
196 | func getLinksTags(link_id int64) (result []*Tag) {
197 | stmt, err := DB.Prepare(
198 | `SELECT
199 | tags.id,
200 | tags.title,
201 | tags.slug
202 | FROM
203 | rel_links_tags
204 | LEFT JOIN
205 | tags
206 | ON
207 | rel_links_tags.tag_id = tags.id
208 | WHERE
209 | rel_links_tags.link_id=?`)
210 | checkErr(err)
211 | rows, err := stmt.Query(link_id)
212 | defer rows.Close()
213 | checkErr(err)
214 | for rows.Next() {
215 | tag := new(Tag)
216 | err := rows.Scan(&tag.Id, &tag.Title, &tag.Slug)
217 | checkErr(err)
218 | result = append(result, tag)
219 | }
220 | return result
221 | }
222 |
223 | func getBookmark(id int64) *BookmarkItem {
224 | bookmark_item := new(BookmarkItem)
225 | err := DB.QueryRow("SELECT id, title, url, createdate FROM links WHERE id=?", id).Scan(
226 | &bookmark_item.Id,
227 | &bookmark_item.Title,
228 | &bookmark_item.Url,
229 | &bookmark_item.CreateDate,
230 | )
231 | checkErr(err)
232 | bookmark_item.Tags = getLinksTags(id)
233 | return bookmark_item
234 | }
235 |
236 | func queryBookmark(page int, items_by_page int, tags string) []*BookmarkItem {
237 | var rows *sql.Rows
238 |
239 | if tags != "" {
240 | stmt, err := DB.Prepare(
241 | `SELECT
242 | links.id,
243 | links.title,
244 | links.url,
245 | links.createdate
246 | FROM
247 | links
248 | LEFT JOIN
249 | rel_links_tags
250 | ON
251 | rel_links_tags.link_id = links.id
252 | LEFT JOIN
253 | tags
254 | ON
255 | rel_links_tags.tag_id = tags.id
256 | WHERE
257 | tags.slug IN (?)
258 | ORDER BY
259 | links.createdate DESC
260 | LIMIT ? OFFSET ?`)
261 | checkErr(err)
262 | rows, err = stmt.Query(tags, items_by_page, (page-1)*items_by_page)
263 | checkErr(err)
264 | } else {
265 | stmt, err := DB.Prepare(
266 | `SELECT
267 | id,
268 | title,
269 | url,
270 | createdate
271 | FROM
272 | links
273 | ORDER BY
274 | createdate DESC
275 | LIMIT ? OFFSET ?`)
276 | checkErr(err)
277 | rows, err = stmt.Query(items_by_page, (page-1)*items_by_page)
278 | checkErr(err)
279 | }
280 | defer rows.Close()
281 |
282 | bms := make([]*BookmarkItem, 0)
283 | for rows.Next() {
284 | bm := new(BookmarkItem)
285 | err := rows.Scan(&bm.Id, &bm.Title, &bm.Url, &bm.CreateDate)
286 | checkErr(err)
287 |
288 | bm.Tags = getLinksTags(bm.Id)
289 |
290 | bms = append(bms, bm)
291 | }
292 | return bms
293 | }
294 |
295 | func searchBookmark(search string, page int, items_by_page int) (total int, bms []*BookmarkItem) {
296 | tags := extractTags(search)
297 | search = removeTags(search)
298 |
299 | var tags_query_list []bleve.Query
300 | tags_query_list = nil
301 |
302 | if len(tags) > 0 {
303 | tags_query_list = make([]bleve.Query, 0)
304 | for _, tag := range tags {
305 | q := bleve.NewQueryStringQuery("tags:" + tag)
306 | tags_query_list = append(tags_query_list, q)
307 | }
308 | }
309 |
310 | var query bleve.Query
311 |
312 | if search != "" {
313 | query_list := make([]bleve.Query, 2)
314 | fuzzy_query := bleve.NewFuzzyQuery(search)
315 | fuzzy_query.FuzzinessVal = 1
316 | query_list[0] = fuzzy_query
317 | query_list[1] = bleve.NewRegexpQuery("[a-zA-Z0-9_]*" + search + "[a-zA-Z0-9_]*")
318 | query = bleve.NewBooleanQuery(tags_query_list, query_list, nil)
319 | } else {
320 | query = bleve.NewBooleanQuery(tags_query_list, nil, nil)
321 | }
322 | searchRequest := bleve.NewSearchRequestOptions(query, items_by_page, (page-1)*items_by_page, false)
323 | sr, err := INDEX.Search(searchRequest)
324 | checkErr(err)
325 |
326 | bms = make([]*BookmarkItem, 0)
327 |
328 | if sr.Total > 0 {
329 | if sr.Request.Size > 0 {
330 | for _, hit := range sr.Hits {
331 | id, err := strconv.ParseInt(hit.ID, 10, 64)
332 | checkErr(err)
333 | bms = append(bms, getBookmark(id))
334 | }
335 | }
336 | }
337 |
338 | return int(sr.Total), bms
339 | }
340 |
--------------------------------------------------------------------------------
/src/gobookmark/public/js/bootstrap-tagsinput.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | "use strict";
3 |
4 | var defaultOptions = {
5 | tagClass: function(item) {
6 | return 'label label-info';
7 | },
8 | itemValue: function(item) {
9 | return item ? item.toString() : item;
10 | },
11 | itemText: function(item) {
12 | return this.itemValue(item);
13 | },
14 | itemTitle: function(item) {
15 | return null;
16 | },
17 | freeInput: true,
18 | addOnBlur: true,
19 | maxTags: undefined,
20 | maxChars: undefined,
21 | confirmKeys: [13, 44],
22 | delimiter: ',',
23 | delimiterRegex: null,
24 | cancelConfirmKeysOnEmpty: false,
25 | onTagExists: function(item, $tag) {
26 | $tag.hide().fadeIn();
27 | },
28 | trimValue: false,
29 | allowDuplicates: false
30 | };
31 |
32 | /**
33 | * Constructor function
34 | */
35 | function TagsInput(element, options) {
36 | this.isInit = true;
37 | this.itemsArray = [];
38 |
39 | this.$element = $(element);
40 | this.$element.hide();
41 |
42 | this.isSelect = (element.tagName === 'SELECT');
43 | this.multiple = (this.isSelect && element.hasAttribute('multiple'));
44 | this.objectItems = options && options.itemValue;
45 | this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
46 | this.inputSize = Math.max(1, this.placeholderText.length);
47 |
48 | this.$container = $('');
49 | this.$input = $('').appendTo(this.$container);
50 |
51 | this.$element.before(this.$container);
52 |
53 | this.build(options);
54 | this.isInit = false;
55 | }
56 |
57 | TagsInput.prototype = {
58 | constructor: TagsInput,
59 |
60 | /**
61 | * Adds the given item as a new tag. Pass true to dontPushVal to prevent
62 | * updating the elements val()
63 | */
64 | add: function(item, dontPushVal, options) {
65 | var self = this;
66 |
67 | if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
68 | return;
69 |
70 | // Ignore falsey values, except false
71 | if (item !== false && !item)
72 | return;
73 |
74 | // Trim value
75 | if (typeof item === "string" && self.options.trimValue) {
76 | item = $.trim(item);
77 | }
78 |
79 | // Throw an error when trying to add an object while the itemValue option was not set
80 | if (typeof item === "object" && !self.objectItems)
81 | throw("Can't add objects when itemValue option is not set");
82 |
83 | // Ignore strings only containg whitespace
84 | if (item.toString().match(/^\s*$/))
85 | return;
86 |
87 | // If SELECT but not multiple, remove current tag
88 | if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
89 | self.remove(self.itemsArray[0]);
90 |
91 | if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
92 | var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
93 | var items = item.split(delimiter);
94 | if (items.length > 1) {
95 | for (var i = 0; i < items.length; i++) {
96 | this.add(items[i], true);
97 | }
98 |
99 | if (!dontPushVal)
100 | self.pushVal();
101 | return;
102 | }
103 | }
104 |
105 | var itemValue = self.options.itemValue(item),
106 | itemText = self.options.itemText(item),
107 | tagClass = self.options.tagClass(item),
108 | itemTitle = self.options.itemTitle(item);
109 |
110 | // Ignore items allready added
111 | var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
112 | if (existing && !self.options.allowDuplicates) {
113 | // Invoke onTagExists
114 | if (self.options.onTagExists) {
115 | var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
116 | self.options.onTagExists(item, $existingTag);
117 | }
118 | return;
119 | }
120 |
121 | // if length greater than limit
122 | if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
123 | return;
124 |
125 | // raise beforeItemAdd arg
126 | var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
127 | self.$element.trigger(beforeItemAddEvent);
128 | if (beforeItemAddEvent.cancel)
129 | return;
130 |
131 | // register item in internal array and map
132 | self.itemsArray.push(item);
133 |
134 | // add a tag element
135 |
136 | var $tag = $('' + htmlEncode(itemText) + '');
137 | $tag.data('item', item);
138 | self.findInputWrapper().before($tag);
139 | $tag.after(' ');
140 |
141 | // Check to see if the tag exists in its raw or uri-encoded form
142 | var optionExists = (
143 | $('option[value="' + encodeURIComponent(itemValue) + '"]', self.$element).length ||
144 | $('option[value="' + htmlEncode(itemValue) + '"]', self.$element).length
145 | );
146 |
147 | // add if item represents a value not present in one of the 's options
148 | if (self.isSelect && !optionExists) {
149 | var $option = $('');
150 | $option.data('item', item);
151 | $option.attr('value', itemValue);
152 | self.$element.append($option);
153 | }
154 |
155 | if (!dontPushVal)
156 | self.pushVal();
157 |
158 | // Add class when reached maxTags
159 | if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
160 | self.$container.addClass('bootstrap-tagsinput-max');
161 |
162 | // If using typeahead, once the tag has been added, clear the typeahead value so it does not stick around in the input.
163 | if ($('.typeahead, .twitter-typeahead', self.$container).length) {
164 | self.$input.typeahead('val', '');
165 | }
166 |
167 | if (this.isInit) {
168 | self.$element.trigger($.Event('itemAddedOnInit', { item: item, options: options }));
169 | } else {
170 | self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
171 | }
172 | },
173 |
174 | /**
175 | * Removes the given item. Pass true to dontPushVal to prevent updating the
176 | * elements val()
177 | */
178 | remove: function(item, dontPushVal, options) {
179 | var self = this;
180 |
181 | if (self.objectItems) {
182 | if (typeof item === "object")
183 | item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
184 | else
185 | item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
186 |
187 | item = item[item.length-1];
188 | }
189 |
190 | if (item) {
191 | var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
192 | self.$element.trigger(beforeItemRemoveEvent);
193 | if (beforeItemRemoveEvent.cancel)
194 | return;
195 |
196 | $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
197 | $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
198 | if($.inArray(item, self.itemsArray) !== -1)
199 | self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
200 | }
201 |
202 | if (!dontPushVal)
203 | self.pushVal();
204 |
205 | // Remove class when reached maxTags
206 | if (self.options.maxTags > self.itemsArray.length)
207 | self.$container.removeClass('bootstrap-tagsinput-max');
208 |
209 | self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
210 | },
211 |
212 | /**
213 | * Removes all items
214 | */
215 | removeAll: function() {
216 | var self = this;
217 |
218 | $('.tag', self.$container).remove();
219 | $('option', self.$element).remove();
220 |
221 | while(self.itemsArray.length > 0)
222 | self.itemsArray.pop();
223 |
224 | self.pushVal();
225 | },
226 |
227 | /**
228 | * Refreshes the tags so they match the text/value of their corresponding
229 | * item.
230 | */
231 | refresh: function() {
232 | var self = this;
233 | $('.tag', self.$container).each(function() {
234 | var $tag = $(this),
235 | item = $tag.data('item'),
236 | itemValue = self.options.itemValue(item),
237 | itemText = self.options.itemText(item),
238 | tagClass = self.options.tagClass(item);
239 |
240 | // Update tag's class and inner text
241 | $tag.attr('class', null);
242 | $tag.addClass('tag ' + htmlEncode(tagClass));
243 | $tag.contents().filter(function() {
244 | return this.nodeType == 3;
245 | })[0].nodeValue = htmlEncode(itemText);
246 |
247 | if (self.isSelect) {
248 | var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
249 | option.attr('value', itemValue);
250 | }
251 | });
252 | },
253 |
254 | /**
255 | * Returns the items added as tags
256 | */
257 | items: function() {
258 | return this.itemsArray;
259 | },
260 |
261 | /**
262 | * Assembly value by retrieving the value of each item, and set it on the
263 | * element.
264 | */
265 | pushVal: function() {
266 | var self = this,
267 | val = $.map(self.items(), function(item) {
268 | return self.options.itemValue(item).toString();
269 | });
270 |
271 | self.$element.val(val, true).trigger('change');
272 | },
273 |
274 | /**
275 | * Initializes the tags input behaviour on the element
276 | */
277 | build: function(options) {
278 | var self = this;
279 |
280 | self.options = $.extend({}, defaultOptions, options);
281 | // When itemValue is set, freeInput should always be false
282 | if (self.objectItems)
283 | self.options.freeInput = false;
284 |
285 | makeOptionItemFunction(self.options, 'itemValue');
286 | makeOptionItemFunction(self.options, 'itemText');
287 | makeOptionFunction(self.options, 'tagClass');
288 |
289 | // Typeahead Bootstrap version 2.3.2
290 | if (self.options.typeahead) {
291 | var typeahead = self.options.typeahead || {};
292 |
293 | makeOptionFunction(typeahead, 'source');
294 |
295 | self.$input.typeahead($.extend({}, typeahead, {
296 | source: function (query, process) {
297 | function processItems(items) {
298 | var texts = [];
299 |
300 | for (var i = 0; i < items.length; i++) {
301 | var text = self.options.itemText(items[i]);
302 | map[text] = items[i];
303 | texts.push(text);
304 | }
305 | process(texts);
306 | }
307 |
308 | this.map = {};
309 | var map = this.map,
310 | data = typeahead.source(query);
311 |
312 | if ($.isFunction(data.success)) {
313 | // support for Angular callbacks
314 | data.success(processItems);
315 | } else if ($.isFunction(data.then)) {
316 | // support for Angular promises
317 | data.then(processItems);
318 | } else {
319 | // support for functions and jquery promises
320 | $.when(data)
321 | .then(processItems);
322 | }
323 | },
324 | updater: function (text) {
325 | self.add(this.map[text]);
326 | return this.map[text];
327 | },
328 | matcher: function (text) {
329 | return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
330 | },
331 | sorter: function (texts) {
332 | return texts.sort();
333 | },
334 | highlighter: function (text) {
335 | var regex = new RegExp( '(' + this.query + ')', 'gi' );
336 | return text.replace( regex, "$1" );
337 | }
338 | }));
339 | }
340 |
341 | // typeahead.js
342 | if (self.options.typeaheadjs) {
343 | var typeaheadConfig = null;
344 | var typeaheadDatasets = {};
345 |
346 | // Determine if main configurations were passed or simply a dataset
347 | var typeaheadjs = self.options.typeaheadjs;
348 | if ($.isArray(typeaheadjs)) {
349 | typeaheadConfig = typeaheadjs[0];
350 | typeaheadDatasets = typeaheadjs[1];
351 | } else {
352 | typeaheadDatasets = typeaheadjs;
353 | }
354 |
355 | self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) {
356 | if (typeaheadDatasets.valueKey)
357 | self.add(datum[typeaheadDatasets.valueKey]);
358 | else
359 | self.add(datum);
360 | self.$input.typeahead('val', '');
361 | }, self));
362 | }
363 |
364 | self.$container.on('click', $.proxy(function(event) {
365 | if (! self.$element.attr('disabled')) {
366 | self.$input.removeAttr('disabled');
367 | }
368 | self.$input.focus();
369 | }, self));
370 |
371 | if (self.options.addOnBlur && self.options.freeInput) {
372 | self.$input.on('focusout', $.proxy(function(event) {
373 | // HACK: only process on focusout when no typeahead opened, to
374 | // avoid adding the typeahead text as tag
375 | if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
376 | self.add(self.$input.val());
377 | self.$input.val('');
378 | }
379 | }, self));
380 | }
381 |
382 |
383 | self.$container.on('keydown', 'input', $.proxy(function(event) {
384 | var $input = $(event.target),
385 | $inputWrapper = self.findInputWrapper();
386 |
387 | if (self.$element.attr('disabled')) {
388 | self.$input.attr('disabled', 'disabled');
389 | return;
390 | }
391 |
392 | switch (event.which) {
393 | // BACKSPACE
394 | case 8:
395 | if (doGetCaretPosition($input[0]) === 0) {
396 | var prev = $inputWrapper.prev();
397 | if (prev.length) {
398 | self.remove(prev.data('item'));
399 | }
400 | }
401 | break;
402 |
403 | // DELETE
404 | case 46:
405 | if (doGetCaretPosition($input[0]) === 0) {
406 | var next = $inputWrapper.next();
407 | if (next.length) {
408 | self.remove(next.data('item'));
409 | }
410 | }
411 | break;
412 |
413 | // LEFT ARROW
414 | case 37:
415 | // Try to move the input before the previous tag
416 | var $prevTag = $inputWrapper.prev();
417 | if ($input.val().length === 0 && $prevTag[0]) {
418 | $prevTag.before($inputWrapper);
419 | $input.focus();
420 | }
421 | break;
422 | // RIGHT ARROW
423 | case 39:
424 | // Try to move the input after the next tag
425 | var $nextTag = $inputWrapper.next();
426 | if ($input.val().length === 0 && $nextTag[0]) {
427 | $nextTag.after($inputWrapper);
428 | $input.focus();
429 | }
430 | break;
431 | default:
432 | // ignore
433 | }
434 |
435 | // Reset internal input's size
436 | var textLength = $input.val().length,
437 | wordSpace = Math.ceil(textLength / 5),
438 | size = textLength + wordSpace + 1;
439 | $input.attr('size', Math.max(this.inputSize, $input.val().length));
440 | }, self));
441 |
442 | self.$container.on('keypress', 'input', $.proxy(function(event) {
443 | var $input = $(event.target);
444 |
445 | if (self.$element.attr('disabled')) {
446 | self.$input.attr('disabled', 'disabled');
447 | return;
448 | }
449 |
450 | var text = $input.val(),
451 | maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
452 | if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
453 | // Only attempt to add a tag if there is data in the field
454 | if (text.length !== 0) {
455 | self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
456 | $input.val('');
457 | }
458 |
459 | // If the field is empty, let the event triggered fire as usual
460 | if (self.options.cancelConfirmKeysOnEmpty === false) {
461 | event.preventDefault();
462 | }
463 | }
464 |
465 | // Reset internal input's size
466 | var textLength = $input.val().length,
467 | wordSpace = Math.ceil(textLength / 5),
468 | size = textLength + wordSpace + 1;
469 | $input.attr('size', Math.max(this.inputSize, $input.val().length));
470 | }, self));
471 |
472 | // Remove icon clicked
473 | self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
474 | if (self.$element.attr('disabled')) {
475 | return;
476 | }
477 | self.remove($(event.target).closest('.tag').data('item'));
478 | }, self));
479 |
480 | // Only add existing value as tags when using strings as tags
481 | if (self.options.itemValue === defaultOptions.itemValue) {
482 | if (self.$element[0].tagName === 'INPUT') {
483 | self.add(self.$element.val());
484 | } else {
485 | $('option', self.$element).each(function() {
486 | self.add($(this).attr('value'), true);
487 | });
488 | }
489 | }
490 | },
491 |
492 | /**
493 | * Removes all tagsinput behaviour and unregsiter all event handlers
494 | */
495 | destroy: function() {
496 | var self = this;
497 |
498 | // Unbind events
499 | self.$container.off('keypress', 'input');
500 | self.$container.off('click', '[role=remove]');
501 |
502 | self.$container.remove();
503 | self.$element.removeData('tagsinput');
504 | self.$element.show();
505 | },
506 |
507 | /**
508 | * Sets focus on the tagsinput
509 | */
510 | focus: function() {
511 | this.$input.focus();
512 | },
513 |
514 | /**
515 | * Returns the internal input element
516 | */
517 | input: function() {
518 | return this.$input;
519 | },
520 |
521 | /**
522 | * Returns the element which is wrapped around the internal input. This
523 | * is normally the $container, but typeahead.js moves the $input element.
524 | */
525 | findInputWrapper: function() {
526 | var elt = this.$input[0],
527 | container = this.$container[0];
528 | while(elt && elt.parentNode !== container)
529 | elt = elt.parentNode;
530 |
531 | return $(elt);
532 | }
533 | };
534 |
535 | /**
536 | * Register JQuery plugin
537 | */
538 | $.fn.tagsinput = function(arg1, arg2, arg3) {
539 | var results = [];
540 |
541 | this.each(function() {
542 | var tagsinput = $(this).data('tagsinput');
543 | // Initialize a new tags input
544 | if (!tagsinput) {
545 | tagsinput = new TagsInput(this, arg1);
546 | $(this).data('tagsinput', tagsinput);
547 | results.push(tagsinput);
548 |
549 | if (this.tagName === 'SELECT') {
550 | $('option', $(this)).attr('selected', 'selected');
551 | }
552 |
553 | // Init tags from $(this).val()
554 | $(this).val($(this).val());
555 | } else if (!arg1 && !arg2) {
556 | // tagsinput already exists
557 | // no function, trying to init
558 | results.push(tagsinput);
559 | } else if(tagsinput[arg1] !== undefined) {
560 | // Invoke function on existing tags input
561 | if(tagsinput[arg1].length === 3 && arg3 !== undefined){
562 | var retVal = tagsinput[arg1](arg2, null, arg3);
563 | }else{
564 | var retVal = tagsinput[arg1](arg2);
565 | }
566 | if (retVal !== undefined)
567 | results.push(retVal);
568 | }
569 | });
570 |
571 | if ( typeof arg1 == 'string') {
572 | // Return the results from the invoked function calls
573 | return results.length > 1 ? results : results[0];
574 | } else {
575 | return results;
576 | }
577 | };
578 |
579 | $.fn.tagsinput.Constructor = TagsInput;
580 |
581 | /**
582 | * Most options support both a string or number as well as a function as
583 | * option value. This function makes sure that the option with the given
584 | * key in the given options is wrapped in a function
585 | */
586 | function makeOptionItemFunction(options, key) {
587 | if (typeof options[key] !== 'function') {
588 | var propertyName = options[key];
589 | options[key] = function(item) { return item[propertyName]; };
590 | }
591 | }
592 | function makeOptionFunction(options, key) {
593 | if (typeof options[key] !== 'function') {
594 | var value = options[key];
595 | options[key] = function() { return value; };
596 | }
597 | }
598 | /**
599 | * HtmlEncodes the given value
600 | */
601 | var htmlEncodeContainer = $('');
602 | function htmlEncode(value) {
603 | if (value) {
604 | return htmlEncodeContainer.text(value).html();
605 | } else {
606 | return '';
607 | }
608 | }
609 |
610 | /**
611 | * Returns the position of the caret in the given input field
612 | * http://flightschool.acylt.com/devnotes/caret-position-woes/
613 | */
614 | function doGetCaretPosition(oField) {
615 | var iCaretPos = 0;
616 | if (document.selection) {
617 | oField.focus ();
618 | var oSel = document.selection.createRange();
619 | oSel.moveStart ('character', -oField.value.length);
620 | iCaretPos = oSel.text.length;
621 | } else if (oField.selectionStart || oField.selectionStart == '0') {
622 | iCaretPos = oField.selectionStart;
623 | }
624 | return (iCaretPos);
625 | }
626 |
627 | /**
628 | * Returns boolean indicates whether user has pressed an expected key combination.
629 | * @param object keyPressEvent: JavaScript event object, refer
630 | * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
631 | * @param object lookupList: expected key combinations, as in:
632 | * [13, {which: 188, shiftKey: true}]
633 | */
634 | function keyCombinationInList(keyPressEvent, lookupList) {
635 | var found = false;
636 | $.each(lookupList, function (index, keyCombination) {
637 | if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
638 | found = true;
639 | return false;
640 | }
641 |
642 | if (keyPressEvent.which === keyCombination.which) {
643 | var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
644 | shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
645 | ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
646 | if (alt && shift && ctrl) {
647 | found = true;
648 | return false;
649 | }
650 | }
651 | });
652 |
653 | return found;
654 | }
655 |
656 | /**
657 | * Initialize tagsinput behaviour on inputs and selects which have
658 | * data-role=tagsinput
659 | */
660 | $(function() {
661 | $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
662 | });
663 | })(window.jQuery);
664 |
--------------------------------------------------------------------------------