├── .gitignore ├── COPYING ├── README ├── bench ├── README └── bench.go ├── demo.sh ├── key.go ├── main.go ├── store.go └── talk ├── balancer.png ├── bumper640x360.png ├── code ├── 0 │ ├── key.go │ ├── main.go │ └── store.go ├── 1 │ ├── key.go │ ├── main.go │ └── store.go ├── 2 │ ├── key.go │ ├── main.go │ └── store.go └── 3 │ ├── demo.sh │ ├── key.go │ ├── main.go │ └── store.go ├── gopher.png ├── index.html ├── notes.txt ├── slidy.css ├── slidy.js ├── structure.png └── urlstore.png /.gitignore: -------------------------------------------------------------------------------- 1 | _go_.6 2 | store.json 3 | /talk/code/?/goto 4 | /bench/bench 5 | /goto 6 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Goto - A URL shortening service 2 | 3 | This code is the basis of the presentation 'Practical Go Programming', 4 | available in the talk/ directory. 5 | 6 | The accompanying code samples are in talk/code. 7 | 8 | The code in the main directory is the full-featured URL shortener, complete 9 | with bindings to github.com/nf/stat, a statistics-collection library. 10 | 11 | There is a stress tester in bench/, which also depends on stat. 12 | 13 | The demo.sh script launches the stats server (stat must be checked out and 14 | built in ../stat), 4 goto servers (3 slaves and 1 master), and several 15 | iterations of the stress-tester. 16 | 17 | Run it and visit http://localhost:8090/ for a pretty graph. 18 | -------------------------------------------------------------------------------- /bench/README: -------------------------------------------------------------------------------- 1 | A stress-tester for goto 2 | 3 | To avoid exhausting source TCP ports (they tend to get stuck in TIME_WAIT), 4 | you'll need to set these sysctl values: 5 | 6 | Linux: 7 | sudo sysctl net.ipv4.tcp_tw_recycle=1 8 | sudo sysctl net.ipv4.tcp_tw_reuse=1 (may not be necessary) 9 | OS X: 10 | sudo sysctl -w net.inet.tcp.msl=1000 11 | 12 | -------------------------------------------------------------------------------- /bench/bench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "github.com/nf/stat" 21 | "io/ioutil" 22 | "log" 23 | "math/rand" 24 | "net/http" 25 | "net/url" 26 | "regexp" 27 | "strings" 28 | "time" 29 | ) 30 | 31 | var ( 32 | n = flag.Int("n", 10, "magnitude of assault") 33 | host = flag.String("host", "localhost:8080", "target host:port") 34 | statServer = flag.String("stats", "localhost:8090", "stat server host") 35 | hosts []string 36 | hostRe = regexp.MustCompile("http://[a-zA-Z0-9:.]+") 37 | ) 38 | 39 | const ( 40 | fooUrl = "http://example.net/foobar" 41 | monDelay = 1e9 42 | getDelay = 100e6 43 | getters = 10 44 | postDelay = 100e6 45 | posters = 1 46 | ) 47 | 48 | var ( 49 | newURL = make(chan string) 50 | randURL = make(chan string) 51 | ) 52 | 53 | func keeper() { 54 | var urls []string 55 | urls = append(urls, <-newURL) 56 | for { 57 | r := urls[rand.Intn(len(urls))] 58 | select { 59 | case u := <-newURL: 60 | for _, h := range hosts { 61 | u = hostRe.ReplaceAllString(u, "http://"+h) 62 | urls = append(urls, u) 63 | } 64 | case randURL <- r: 65 | } 66 | } 67 | } 68 | 69 | func post() { 70 | u := fmt.Sprintf("http://%s/add", hosts[rand.Intn(len(hosts))]) 71 | r, err := http.PostForm(u, url.Values{"url": {fooUrl}}) 72 | if err != nil { 73 | log.Println("post:", err) 74 | return 75 | } 76 | defer r.Body.Close() 77 | b, err := ioutil.ReadAll(r.Body) 78 | if err != nil { 79 | log.Println("post:", err) 80 | return 81 | } 82 | newURL <- string(b) 83 | stat.In <- "put" 84 | } 85 | 86 | func get() { 87 | u := <-randURL 88 | req, err := http.NewRequest("HEAD",u,nil) 89 | r, err := http.DefaultTransport.RoundTrip(req) 90 | if err != nil { 91 | log.Println("get:", err) 92 | return 93 | } 94 | defer r.Body.Close() 95 | b, err := ioutil.ReadAll(r.Body) 96 | if err != nil { 97 | log.Println("get:", err) 98 | return 99 | } 100 | if r.StatusCode != 302 { 101 | log.Println("get: wrong StatusCode:", r.StatusCode) 102 | if r.StatusCode == 500 { 103 | log.Printf("Error: %s\n", b) 104 | } 105 | } 106 | if l := r.Header.Get("Location"); l != fooUrl { 107 | log.Println("get: wrong Location:", l) 108 | } 109 | stat.In <- "get" 110 | } 111 | 112 | func loop(fn func(), delay time.Duration) { 113 | for { 114 | fn() 115 | time.Sleep(delay) 116 | } 117 | } 118 | 119 | func main() { 120 | flag.Parse() 121 | hosts = strings.Split(*host, ",") 122 | rand.Seed(time.Now().UnixNano()) 123 | go keeper() 124 | for i := 0; i < getters*(*n); i++ { 125 | go loop(get, getDelay) 126 | } 127 | for i := 0; i < posters*(*n); i++ { 128 | go loop(post, postDelay) 129 | } 130 | stat.Process = "!bench" 131 | stat.Monitor(*statServer) 132 | } 133 | -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | STATS=127.0.0.1:8090 4 | MASTER=127.0.0.1:8080 5 | N1=1 6 | N2=8 7 | N3=16 8 | 9 | echo "Starting up" 10 | cd ../stat/server 11 | go build -o stats 12 | ./stats & 13 | stats_pid=$! 14 | cd ../../goto 15 | sleep 1 16 | go build -o goto 17 | ./goto -stats=$STATS -host=$MASTER -rpc=true & 18 | master_pid=$! 19 | sleep 1 20 | ./goto -stats=$STATS -host=$MASTER -master=$MASTER -http=:8081 & 21 | slave1_pid=$! 22 | ./goto -stats=$STATS -host=$MASTER -master=$MASTER -http=:8082 & 23 | slave2_pid=$! 24 | ./goto -stats=$STATS -host=$MASTER -master=$MASTER -http=:8083 & 25 | slave3_pid=$! 26 | sleep 1 27 | 28 | echo "Testing the master (n=$N1)" 29 | go build -o bench/bench ./bench 30 | bench/bench -stats=$STATS -host=$MASTER -n=$N1 & 31 | pid=$! 32 | read 33 | kill $pid 34 | 35 | echo "Testing the master (n=$N2)" 36 | bench/bench -stats=$STATS -host=$MASTER -n=$N2 & 37 | pid=$! 38 | read 39 | kill $pid 40 | 41 | echo "Testing 1 slave (n=$N2)" 42 | bench/bench -stats=$STATS -host=127.0.0.1:8081 -n=$N2 & 43 | pid=$! 44 | read 45 | kill $pid 46 | 47 | echo "Testing 2 slaves (n=$N2)" 48 | bench/bench -stats=$STATS -host=127.0.0.1:8081,127.0.0.1:8082 -n=$N2 & 49 | pid=$! 50 | read 51 | kill $pid 52 | 53 | echo "Testing 3 slaves (n=$N2)" 54 | bench/bench -stats=$STATS -host=127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083 -n=$N2 & 55 | pid=$! 56 | read 57 | kill $pid 58 | 59 | echo "Testing 3 slaves (n=$N3)" 60 | bench/bench -stats=$STATS -host=127.0.0.1:8081,127.0.0.1:8082,127.0.0.1:8083 -n=$N3 & 61 | pid=$! 62 | read 63 | kill $pid 64 | 65 | echo "Shutting down" 66 | kill $stats_pid 67 | kill $master_pid 68 | kill $slave1_pid 69 | kill $slave2_pid 70 | kill $slave3_pid 71 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 18 | 19 | func genKey(n int) string { 20 | if n == 0 { 21 | return string(keyChar[0]) 22 | } 23 | l := len(keyChar) 24 | s := make([]byte, 20) // FIXME: will overflow. eventually. 25 | i := len(s) 26 | for n > 0 && i >= 0 { 27 | i-- 28 | j := n % l 29 | n = (n - j) / l 30 | s[i] = keyChar[j] 31 | } 32 | return string(s[i:]) 33 | } 34 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "github.com/nf/stat" 21 | "net/http" 22 | "net/rpc" 23 | ) 24 | 25 | var ( 26 | listenAddr = flag.String("http", ":8080", "http listen address") 27 | dataFile = flag.String("file", "store.json", "data store file name") 28 | hostname = flag.String("host", "localhost:8080", "http host name") 29 | masterAddr = flag.String("master", "", "RPC master address") 30 | rpcEnabled = flag.Bool("rpc", false, "enable RPC server") 31 | statServer = flag.String("stats", "", "stat server address") 32 | ) 33 | 34 | var store Store 35 | 36 | func main() { 37 | flag.Parse() 38 | if *masterAddr != "" { 39 | store = NewProxyStore(*masterAddr) 40 | } else { 41 | store = NewURLStore(*dataFile) 42 | } 43 | if *rpcEnabled { 44 | rpc.RegisterName("Store", store) 45 | rpc.HandleHTTP() 46 | } 47 | if *statServer != "" { 48 | stat.Process = *listenAddr 49 | go stat.Monitor(*statServer) 50 | } 51 | http.HandleFunc("/", Redirect) 52 | http.HandleFunc("/add", Add) 53 | http.ListenAndServe(*listenAddr, nil) 54 | 55 | } 56 | 57 | func Redirect(w http.ResponseWriter, r *http.Request) { 58 | key := r.URL.Path[1:] 59 | if key == "favicon.ico" || key == "" { 60 | http.NotFound(w, r) 61 | return 62 | } 63 | var url string 64 | if err := store.Get(&key, &url); err != nil { 65 | http.Error(w, err.Error(), http.StatusInternalServerError) 66 | return 67 | } 68 | http.Redirect(w, r, url, http.StatusFound) 69 | } 70 | 71 | func Add(w http.ResponseWriter, r *http.Request) { 72 | url := r.FormValue("url") 73 | if url == "" { 74 | fmt.Fprint(w, AddForm) 75 | return 76 | } 77 | var key string 78 | if err := store.Put(&url, &key); err != nil { 79 | http.Error(w, err.Error(), http.StatusInternalServerError) 80 | return 81 | } 82 | fmt.Fprintf(w, "http://%s/%s", *hostname, key) 83 | } 84 | 85 | const AddForm = ` 86 | 87 |
88 | URL: 89 | 90 |
91 | 92 | ` 93 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "encoding/json" 20 | "errors" 21 | "github.com/nf/stat" 22 | "io" 23 | "log" 24 | "net/rpc" 25 | "os" 26 | "sync" 27 | "time" 28 | ) 29 | 30 | const ( 31 | saveTimeout = 10e9 32 | saveQueueLength = 1000 33 | ) 34 | 35 | type Store interface { 36 | Put(url, key *string) error 37 | Get(key, url *string) error 38 | } 39 | 40 | type URLStore struct { 41 | mu sync.RWMutex 42 | urls map[string]string 43 | count int 44 | save chan record 45 | } 46 | 47 | type record struct { 48 | Key, URL string 49 | } 50 | 51 | func NewURLStore(filename string) *URLStore { 52 | s := &URLStore{urls: make(map[string]string)} 53 | if filename != "" { 54 | s.save = make(chan record, saveQueueLength) 55 | if err := s.load(filename); err != nil { 56 | log.Println("URLStore:", err) 57 | } 58 | go s.saveLoop(filename) 59 | } 60 | return s 61 | } 62 | 63 | func (s *URLStore) Get(key, url *string) error { 64 | defer statSend("store get") 65 | s.mu.RLock() 66 | defer s.mu.RUnlock() 67 | if u, ok := s.urls[*key]; ok { 68 | *url = u 69 | return nil 70 | } 71 | return errors.New("key not found") 72 | } 73 | 74 | func (s *URLStore) Set(key, url *string) error { 75 | s.mu.Lock() 76 | defer s.mu.Unlock() 77 | if _, present := s.urls[*key]; present { 78 | return errors.New("key already exists") 79 | } 80 | s.urls[*key] = *url 81 | return nil 82 | } 83 | 84 | func (s *URLStore) Put(url, key *string) error { 85 | defer statSend("store put") 86 | for { 87 | *key = genKey(s.count) 88 | s.count++ 89 | if err := s.Set(key, url); err == nil { 90 | break 91 | } 92 | } 93 | if s.save != nil { 94 | s.save <- record{*key, *url} 95 | } 96 | return nil 97 | } 98 | 99 | func (s *URLStore) load(filename string) error { 100 | f, err := os.Open(filename) 101 | if err != nil { 102 | return err 103 | } 104 | defer f.Close() 105 | b := bufio.NewReader(f) 106 | d := json.NewDecoder(b) 107 | for { 108 | var r record 109 | if err := d.Decode(&r); err == io.EOF { 110 | break 111 | } else if err != nil { 112 | return err 113 | } 114 | if err = s.Set(&r.Key, &r.URL); err != nil { 115 | return err 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func (s *URLStore) saveLoop(filename string) { 122 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 123 | if err != nil { 124 | log.Println("URLStore:", err) 125 | return 126 | } 127 | b := bufio.NewWriter(f) 128 | e := json.NewEncoder(b) 129 | t := time.NewTicker(saveTimeout) 130 | defer f.Close() 131 | defer b.Flush() 132 | for { 133 | var err error 134 | select { 135 | case r := <-s.save: 136 | err = e.Encode(r) 137 | case <-t.C: 138 | err = b.Flush() 139 | } 140 | if err != nil { 141 | log.Println("URLStore:", err) 142 | } 143 | } 144 | } 145 | 146 | type ProxyStore struct { 147 | urls *URLStore 148 | client *rpc.Client 149 | } 150 | 151 | func NewProxyStore(addr string) *ProxyStore { 152 | client, err := rpc.DialHTTP("tcp", addr) 153 | if err != nil { 154 | log.Println("ProxyStore:", err) 155 | } 156 | return &ProxyStore{urls: NewURLStore(""), client: client} 157 | } 158 | 159 | func (s *ProxyStore) Get(key, url *string) error { 160 | if err := s.urls.Get(key, url); err == nil { 161 | return nil 162 | } 163 | if err := s.client.Call("Store.Get", key, url); err != nil { 164 | return err 165 | } 166 | s.urls.Set(key, url) 167 | return nil 168 | } 169 | 170 | func (s *ProxyStore) Put(url, key *string) error { 171 | if err := s.client.Call("Store.Put", url, key); err != nil { 172 | return err 173 | } 174 | s.urls.Set(key, url) 175 | return nil 176 | } 177 | 178 | func statSend(s string) { 179 | if *statServer != "" { 180 | stat.In <- s 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /talk/balancer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nf/goto/90baf09463bba69b426d0857028e1e545c111c58/talk/balancer.png -------------------------------------------------------------------------------- /talk/bumper640x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nf/goto/90baf09463bba69b426d0857028e1e545c111c58/talk/bumper640x360.png -------------------------------------------------------------------------------- /talk/code/0/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 4 | 5 | func genKey(n int) string { 6 | if n == 0 { 7 | return string(keyChar[0]) 8 | } 9 | l := len(keyChar) 10 | s := make([]byte, 20) // FIXME: will overflow. eventually. 11 | i := len(s) 12 | for n > 0 && i >= 0 { 13 | i-- 14 | j := n % l 15 | n = (n - j) / l 16 | s[i] = keyChar[j] 17 | } 18 | return string(s[i:]) 19 | } 20 | -------------------------------------------------------------------------------- /talk/code/0/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | var store = NewURLStore() 9 | 10 | func main() { 11 | http.HandleFunc("/", Redirect) 12 | http.HandleFunc("/add", Add) 13 | http.ListenAndServe(":8080", nil) 14 | } 15 | 16 | func Redirect(w http.ResponseWriter, r *http.Request) { 17 | key := r.URL.Path[1:] 18 | url := store.Get(key) 19 | if url == "" { 20 | http.NotFound(w, r) 21 | return 22 | } 23 | http.Redirect(w, r, url, http.StatusFound) 24 | } 25 | 26 | func Add(w http.ResponseWriter, r *http.Request) { 27 | url := r.FormValue("url") 28 | if url == "" { 29 | fmt.Fprint(w, AddForm) 30 | return 31 | } 32 | key := store.Put(url) 33 | fmt.Fprintf(w, "http://localhost:8080/%s", key) 34 | } 35 | 36 | const AddForm = ` 37 |
38 | URL: 39 | 40 |
41 | ` 42 | -------------------------------------------------------------------------------- /talk/code/0/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync" 4 | 5 | type URLStore struct { 6 | urls map[string]string 7 | mu sync.RWMutex 8 | } 9 | 10 | func NewURLStore() *URLStore { 11 | return &URLStore{urls: make(map[string]string)} 12 | } 13 | 14 | func (s *URLStore) Get(key string) string { 15 | s.mu.RLock() 16 | defer s.mu.RUnlock() 17 | return s.urls[key] 18 | } 19 | 20 | func (s *URLStore) Set(key, url string) bool { 21 | s.mu.Lock() 22 | defer s.mu.Unlock() 23 | if _, present := s.urls[key]; present { 24 | return false 25 | } 26 | s.urls[key] = url 27 | return true 28 | } 29 | 30 | func (s *URLStore) Count() int { 31 | s.mu.RLock() 32 | defer s.mu.RUnlock() 33 | return len(s.urls) 34 | } 35 | 36 | func (s *URLStore) Put(url string) string { 37 | for { 38 | key := genKey(s.Count()) 39 | if ok := s.Set(key, url); ok { 40 | return key 41 | } 42 | } 43 | panic("shouldn't get here") 44 | } 45 | -------------------------------------------------------------------------------- /talk/code/1/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 4 | 5 | func genKey(n int) string { 6 | if n == 0 { 7 | return string(keyChar[0]) 8 | } 9 | l := len(keyChar) 10 | s := make([]byte, 20) // FIXME: will overflow. eventually. 11 | i := len(s) 12 | for n > 0 && i >= 0 { 13 | i-- 14 | j := n % l 15 | n = (n - j) / l 16 | s[i] = keyChar[j] 17 | } 18 | return string(s[i:]) 19 | } 20 | -------------------------------------------------------------------------------- /talk/code/1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | var store = NewURLStore("store.json") 9 | 10 | func main() { 11 | http.HandleFunc("/", Redirect) 12 | http.HandleFunc("/add", Add) 13 | http.ListenAndServe(":8080", nil) 14 | } 15 | 16 | func Redirect(w http.ResponseWriter, r *http.Request) { 17 | key := r.URL.Path[1:] 18 | url := store.Get(key) 19 | if url == "" { 20 | http.NotFound(w, r) 21 | return 22 | } 23 | http.Redirect(w, r, url, http.StatusFound) 24 | } 25 | 26 | func Add(w http.ResponseWriter, r *http.Request) { 27 | url := r.FormValue("url") 28 | if url == "" { 29 | fmt.Fprint(w, AddForm) 30 | return 31 | } 32 | key := store.Put(url) 33 | fmt.Fprintf(w, "http://localhost:8080/%s", key) 34 | } 35 | 36 | const AddForm = ` 37 |
38 | URL: 39 | 40 |
41 | ` 42 | -------------------------------------------------------------------------------- /talk/code/1/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type URLStore struct { 12 | urls map[string]string 13 | mu sync.RWMutex 14 | file *os.File 15 | } 16 | 17 | type record struct { 18 | Key, URL string 19 | } 20 | 21 | func NewURLStore(filename string) *URLStore { 22 | s := &URLStore{urls: make(map[string]string)} 23 | f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 24 | if err != nil { 25 | log.Fatal("URLStore:", err) 26 | } 27 | s.file = f 28 | if err := s.load(); err != nil { 29 | log.Println("URLStore:", err) 30 | } 31 | return s 32 | } 33 | 34 | func (s *URLStore) Get(key string) string { 35 | s.mu.RLock() 36 | defer s.mu.RUnlock() 37 | return s.urls[key] 38 | } 39 | 40 | func (s *URLStore) Set(key, url string) bool { 41 | s.mu.Lock() 42 | defer s.mu.Unlock() 43 | if _, present := s.urls[key]; present { 44 | return false 45 | } 46 | s.urls[key] = url 47 | return true 48 | } 49 | 50 | func (s *URLStore) Count() int { 51 | s.mu.RLock() 52 | defer s.mu.RUnlock() 53 | return len(s.urls) 54 | } 55 | 56 | func (s *URLStore) Put(url string) string { 57 | for { 58 | key := genKey(s.Count()) 59 | if ok := s.Set(key, url); ok { 60 | if err := s.save(key, url); err != nil { 61 | log.Println("URLStore:", err) 62 | } 63 | return key 64 | } 65 | } 66 | panic("shouldn't get here") 67 | } 68 | 69 | func (s *URLStore) load() error { 70 | if _, err := s.file.Seek(0, 0); err != nil { 71 | return err 72 | } 73 | d := json.NewDecoder(s.file) 74 | var err error 75 | for err == nil { 76 | var r record 77 | if err = d.Decode(&r); err == nil { 78 | s.Set(r.Key, r.URL) 79 | } 80 | } 81 | if err == io.EOF { 82 | return nil 83 | } 84 | return err 85 | } 86 | 87 | func (s *URLStore) save(key, url string) error { 88 | e := json.NewEncoder(s.file) 89 | return e.Encode(record{key, url}) 90 | } 91 | -------------------------------------------------------------------------------- /talk/code/2/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 4 | 5 | func genKey(n int) string { 6 | if n == 0 { 7 | return string(keyChar[0]) 8 | } 9 | l := len(keyChar) 10 | s := make([]byte, 20) // FIXME: will overflow. eventually. 11 | i := len(s) 12 | for n > 0 && i >= 0 { 13 | i-- 14 | j := n % l 15 | n = (n - j) / l 16 | s[i] = keyChar[j] 17 | } 18 | return string(s[i:]) 19 | } 20 | -------------------------------------------------------------------------------- /talk/code/2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | var ( 10 | listenAddr = flag.String("http", ":8080", "http listen address") 11 | dataFile = flag.String("file", "store.json", "data store file name") 12 | hostname = flag.String("host", "localhost:8080", "http host name") 13 | ) 14 | 15 | var store *URLStore 16 | 17 | func main() { 18 | flag.Parse() 19 | store = NewURLStore(*dataFile) 20 | http.HandleFunc("/", Redirect) 21 | http.HandleFunc("/add", Add) 22 | http.ListenAndServe(*listenAddr, nil) 23 | } 24 | 25 | func Redirect(w http.ResponseWriter, r *http.Request) { 26 | key := r.URL.Path[1:] 27 | url := store.Get(key) 28 | if url == "" { 29 | http.NotFound(w, r) 30 | return 31 | } 32 | http.Redirect(w, r, url, http.StatusFound) 33 | } 34 | 35 | func Add(w http.ResponseWriter, r *http.Request) { 36 | url := r.FormValue("url") 37 | if url == "" { 38 | fmt.Fprint(w, AddForm) 39 | return 40 | } 41 | key := store.Put(url) 42 | fmt.Fprintf(w, "http://%s/%s", *hostname, key) 43 | } 44 | 45 | const AddForm = ` 46 |
47 | URL: 48 | 49 |
50 | ` 51 | -------------------------------------------------------------------------------- /talk/code/2/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | const saveQueueLength = 1000 12 | 13 | type URLStore struct { 14 | urls map[string]string 15 | mu sync.RWMutex 16 | save chan record 17 | } 18 | 19 | type record struct { 20 | Key, URL string 21 | } 22 | 23 | func NewURLStore(filename string) *URLStore { 24 | s := &URLStore{ 25 | urls: make(map[string]string), 26 | save: make(chan record, saveQueueLength), 27 | } 28 | if err := s.load(filename); err != nil { 29 | log.Println("URLStore:", err) 30 | } 31 | go s.saveLoop(filename) 32 | return s 33 | } 34 | 35 | func (s *URLStore) Get(key string) string { 36 | s.mu.RLock() 37 | defer s.mu.RUnlock() 38 | return s.urls[key] 39 | } 40 | 41 | func (s *URLStore) Set(key, url string) bool { 42 | s.mu.Lock() 43 | defer s.mu.Unlock() 44 | if _, present := s.urls[key]; present { 45 | return false 46 | } 47 | s.urls[key] = url 48 | return true 49 | } 50 | 51 | func (s *URLStore) Count() int { 52 | s.mu.RLock() 53 | defer s.mu.RUnlock() 54 | return len(s.urls) 55 | } 56 | 57 | func (s *URLStore) Put(url string) string { 58 | for { 59 | key := genKey(s.Count()) 60 | if ok := s.Set(key, url); ok { 61 | s.save <- record{key, url} 62 | return key 63 | } 64 | } 65 | panic("shouldn't get here") 66 | } 67 | 68 | func (s *URLStore) load(filename string) error { 69 | f, err := os.Open(filename) 70 | if err != nil { 71 | return err 72 | } 73 | defer f.Close() 74 | d := json.NewDecoder(f) 75 | for err == nil { 76 | var r record 77 | if err = d.Decode(&r); err == nil { 78 | s.Set(r.Key, r.URL) 79 | } 80 | } 81 | if err == io.EOF { 82 | return nil 83 | } 84 | return err 85 | } 86 | 87 | func (s *URLStore) saveLoop(filename string) { 88 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 89 | if err != nil { 90 | log.Fatal("URLStore:", err) 91 | } 92 | e := json.NewEncoder(f) 93 | for { 94 | r := <-s.save 95 | if err := e.Encode(r); err != nil { 96 | log.Println("URLStore:", err) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /talk/code/3/demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | go build -o goto 4 | ./goto -http=:8081 -rpc=true & 5 | master_pid=$! 6 | sleep 1 7 | ./goto -master=127.0.0.1:8081 & 8 | slave_pid=$! 9 | echo "Running master on :8081, slave on :8080." 10 | echo "Visit: http://localhost:8080/add" 11 | echo "Press enter to shut down" 12 | read 13 | kill $master_pid 14 | kill $slave_pid 15 | 16 | -------------------------------------------------------------------------------- /talk/code/3/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var keyChar = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 4 | 5 | func genKey(n int) string { 6 | if n == 0 { 7 | return string(keyChar[0]) 8 | } 9 | l := len(keyChar) 10 | s := make([]byte, 20) // FIXME: will overflow. eventually. 11 | i := len(s) 12 | for n > 0 && i >= 0 { 13 | i-- 14 | j := n % l 15 | n = (n - j) / l 16 | s[i] = keyChar[j] 17 | } 18 | return string(s[i:]) 19 | } 20 | -------------------------------------------------------------------------------- /talk/code/3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "net/rpc" 8 | ) 9 | 10 | var ( 11 | listenAddr = flag.String("http", ":8080", "http listen address") 12 | dataFile = flag.String("file", "store.json", "data store file name") 13 | hostname = flag.String("host", "localhost:8080", "http host name") 14 | masterAddr = flag.String("master", "", "RPC master address") 15 | rpcEnabled = flag.Bool("rpc", false, "enable RPC server") 16 | ) 17 | 18 | var store Store 19 | 20 | func main() { 21 | flag.Parse() 22 | if *masterAddr != "" { 23 | store = NewProxyStore(*masterAddr) 24 | } else { 25 | store = NewURLStore(*dataFile) 26 | } 27 | if *rpcEnabled { 28 | rpc.RegisterName("Store", store) 29 | rpc.HandleHTTP() 30 | } 31 | http.HandleFunc("/", Redirect) 32 | http.HandleFunc("/add", Add) 33 | http.ListenAndServe(*listenAddr, nil) 34 | 35 | } 36 | 37 | func Redirect(w http.ResponseWriter, r *http.Request) { 38 | key := r.URL.Path[1:] 39 | if key == "" || key == "favicon.ico" { 40 | http.NotFound(w, r) 41 | return 42 | } 43 | var url string 44 | if err := store.Get(&key, &url); err != nil { 45 | http.Error(w, err.Error(), http.StatusInternalServerError) 46 | return 47 | } 48 | http.Redirect(w, r, url, http.StatusFound) 49 | } 50 | 51 | func Add(w http.ResponseWriter, r *http.Request) { 52 | url := r.FormValue("url") 53 | if url == "" { 54 | fmt.Fprint(w, AddForm) 55 | return 56 | } 57 | var key string 58 | if err := store.Put(&url, &key); err != nil { 59 | http.Error(w, err.Error(), http.StatusInternalServerError) 60 | return 61 | } 62 | fmt.Fprintf(w, "http://%s/%s", *hostname, key) 63 | } 64 | 65 | const AddForm = ` 66 |
67 | URL: 68 | 69 |
70 | ` 71 | -------------------------------------------------------------------------------- /talk/code/3/store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "log" 8 | "net/rpc" 9 | "os" 10 | "sync" 11 | ) 12 | 13 | const saveQueueLength = 1000 14 | 15 | type Store interface { 16 | Put(url, key *string) error 17 | Get(key, url *string) error 18 | } 19 | 20 | type URLStore struct { 21 | urls map[string]string 22 | mu sync.RWMutex 23 | save chan record 24 | } 25 | 26 | type record struct { 27 | Key, URL string 28 | } 29 | 30 | func NewURLStore(filename string) *URLStore { 31 | s := &URLStore{urls: make(map[string]string)} 32 | if filename != "" { 33 | s.save = make(chan record, saveQueueLength) 34 | if err := s.load(filename); err != nil { 35 | log.Println("URLStore:", err) 36 | } 37 | go s.saveLoop(filename) 38 | } 39 | return s 40 | } 41 | 42 | func (s *URLStore) Get(key, url *string) error { 43 | s.mu.RLock() 44 | defer s.mu.RUnlock() 45 | if u, ok := s.urls[*key]; ok { 46 | *url = u 47 | return nil 48 | } 49 | return errors.New("key not found") 50 | } 51 | 52 | func (s *URLStore) Set(key, url *string) error { 53 | s.mu.Lock() 54 | defer s.mu.Unlock() 55 | if _, present := s.urls[*key]; present { 56 | return errors.New("key already exists") 57 | } 58 | s.urls[*key] = *url 59 | return nil 60 | } 61 | 62 | func (s *URLStore) Count() int { 63 | s.mu.RLock() 64 | defer s.mu.RUnlock() 65 | return len(s.urls) 66 | } 67 | 68 | func (s *URLStore) Put(url, key *string) error { 69 | for { 70 | *key = genKey(s.Count()) 71 | if err := s.Set(key, url); err == nil { 72 | break 73 | } 74 | } 75 | if s.save != nil { 76 | s.save <- record{*key, *url} 77 | } 78 | return nil 79 | } 80 | 81 | func (s *URLStore) load(filename string) error { 82 | f, err := os.Open(filename) 83 | if err != nil { 84 | return err 85 | } 86 | defer f.Close() 87 | d := json.NewDecoder(f) 88 | for err == nil { 89 | var r record 90 | if err = d.Decode(&r); err == nil { 91 | s.Set(&r.Key, &r.URL) 92 | } 93 | } 94 | if err == io.EOF { 95 | return nil 96 | } 97 | return err 98 | } 99 | 100 | func (s *URLStore) saveLoop(filename string) { 101 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 102 | if err != nil { 103 | log.Fatal("URLStore:", err) 104 | } 105 | e := json.NewEncoder(f) 106 | for { 107 | r := <-s.save 108 | if err := e.Encode(r); err != nil { 109 | log.Println("URLStore:", err) 110 | } 111 | } 112 | } 113 | 114 | type ProxyStore struct { 115 | urls *URLStore 116 | client *rpc.Client 117 | } 118 | 119 | func NewProxyStore(addr string) *ProxyStore { 120 | client, err := rpc.DialHTTP("tcp", addr) 121 | if err != nil { 122 | log.Println("ProxyStore:", err) 123 | } 124 | return &ProxyStore{urls: NewURLStore(""), client: client} 125 | } 126 | 127 | func (s *ProxyStore) Get(key, url *string) error { 128 | if err := s.urls.Get(key, url); err == nil { 129 | return nil 130 | } 131 | if err := s.client.Call("Store.Get", key, url); err != nil { 132 | return err 133 | } 134 | s.urls.Set(key, url) 135 | return nil 136 | } 137 | 138 | func (s *ProxyStore) Put(url, key *string) error { 139 | if err := s.client.Call("Store.Put", url, key); err != nil { 140 | return err 141 | } 142 | s.urls.Set(key, url) 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /talk/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nf/goto/90baf09463bba69b426d0857028e1e545c111c58/talk/gopher.png -------------------------------------------------------------------------------- /talk/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Practical Go Programming 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 |
14 |

Practical Go Programming

15 |

Andrew Gerrand

16 |

adg@golang.org

17 |

http://wh3rd.net/practical-go/

18 |

19 | 20 |
21 | 22 |
23 |

What is Go?

24 | 25 |

Go is a general-purpose programming language.

26 |

Go's killer features:

27 | 37 |
38 | 39 |
40 |

This talk

41 |

42 | This talk will cover the complete development of a simple web application. 43 |

44 |

45 | There's a lot to cover, so we'll move pretty fast. 46 |

47 |

48 | If you're new to Go there may be some syntax you don't understand. 49 | The important thing is to get a feel for what the program does, 50 | rather than exactly how it does it. 51 |

52 |

53 | These slides are available at http://wh3rd.net/practical-go/ - you may want to follow along. 54 |

55 |

56 | Complete source code and other bits are available in the git repository: 57 | http://github.com/nf/goto 58 |

59 |

60 | Twitter stuff: 61 |

62 | 66 |
67 | 68 |
69 |

Let's write a Go program

70 |

goto: A URL Shortener

71 |

Goto is a web service (HTTP) that does two things:

72 | 80 |
81 | 82 |
83 |

Data structures

84 |

Goto maps short URLs to long URLs. To store this mapping in memory we can 85 | use a hash table.

86 |

Go's map type allows you to map values of any* type 87 | to values of any other type.

88 |

Maps must be initialized with the built-in make function:

89 |
  90 | m := make(map[int]string)
  91 | 
  92 | m[1] = "One"
  93 | 
  94 | u := m[1]
  95 | // u == "One"
  96 | 
  97 | v, present := m[2]
  98 | // v == "", present == false
  99 | 
100 |

(*The keys must be comparable with ==.)

101 |
102 | 103 |
104 |

Data structures

105 |

We specify the URLStore type, 106 | Goto's fundamental data structure:

107 |
 108 | type URLStore map[string]string
 109 | 
 110 | m := make(URLStore)
 111 | 
112 |

To store the mapping of http://goto/a to http://google.com/ in m:

113 |
 114 | m["a"] = "http://google.com/"
 115 | 
 116 | url := m["a"]
 117 | // url == "http://google.com/"
 118 | 
119 |

120 | This has a shortcoming: Go's map type is not thread-safe. 121 |

122 |

123 | Goto will serve many requests concurrently, so we must make our 124 | URLStore type safe to access from separate threads. 125 |

126 |
127 | 128 |
129 |

Adding a lock

130 |

131 | To protect the map type from being modified while it is being 132 | read, we must add a lock to the data structure. 133 |

134 |

135 | Changing the type definition, we make URLStore a 136 | struct type with two fields: the map and 137 | a RWMutex from the sync package. 138 |

139 |
 140 | import "sync"
 141 | 
 142 | type URLStore struct {
 143 | 	urls map[string]string
 144 | 	mu   sync.RWMutex
 145 | }
 146 | 
147 |

148 | An RWMutex has two locks: one for readers, and one for writers. 149 | Many clients can take the read lock simultaneously, but only one client can 150 | take the write lock (to the exclusion of all readers). 151 |

152 |
153 | 154 |
155 |

Setter and Getter methods

156 |

157 | We must now interact with the URLStore through Set 158 | and Get methods. 159 |

160 |

161 | The Get method takes the read lock with mu.RLock, and 162 | returns the URL as a string. If the key is not present in the map, the 163 | zero value for the string type (an empty string) will be returned. 164 |

165 |
 166 | func (s *URLStore) Get(key string) string {
 167 | 	s.mu.RLock()
 168 | 	url := s.urls[key]
 169 | 	s.mu.RUnlock()
 170 |         return url
 171 | }
 172 | 
173 |
174 | 175 |
176 |

Setter and Getter methods

177 |

178 | The Set method takes the write lock and updates the url map. 179 |

180 |

181 | If the key is already present, Set returns a boolean 182 | false value and the map is not updated. 183 | (Later, We will use this behavior to ensure that each URL has a unique key.) 184 |

185 |
 186 | func (s *URLStore) Set(key, url string) bool {
 187 | 	s.mu.Lock()
 188 | 	_, present := s.urls[key]
 189 | 	if present {
 190 | 		s.mu.Unlock()
 191 | 		return false
 192 | 	}
 193 | 	s.urls[key] = url
 194 | 	s.mu.Unlock()
 195 | 	return true
 196 | }       
 197 | 
198 |
199 | 200 |
201 |

Defer: an aside

202 |

203 | A defer statement pushes a function call onto a list. 204 | The list of saved calls is executed after the surrounding function returns. 205 | Defer is commonly used to simplify functions that perform various clean-up 206 | actions. 207 |

208 |

209 | For example, this function will print "Hello" and then "World": 210 |

 211 | func foo() {
 212 | 	defer fmt.Println("World")
 213 | 	fmt.Println("Hello")
 214 | }
 215 | 
216 |

217 | We can use defer to simplify the Set and 218 | Get methods. 219 |

220 |

221 | There's much more to know about defer. 222 | See the "Defer, Panic, and Recover" blog post for an in-depth discussion. 223 |

224 |
225 | 226 |
227 |

Setter and Getter methods

228 |

229 | Using defer, the Get method can avoid using the local 230 | url variable and return the map value directly: 231 |

232 |
 233 | func (s *URLStore) Get(key string) string {
 234 | 	s.mu.RLock()
 235 | 	defer s.mu.RUnlock()
 236 | 	return s.urls[key]
 237 | }
 238 | 
239 |

240 | And the logic for Set becomes clearer: 241 |

242 |
 243 | func (s *URLStore) Set(key, url string) bool {
 244 | 	s.mu.Lock()
 245 | 	defer s.mu.Unlock()
 246 | 	_, present := s.urls[key]
 247 | 	if present {
 248 | 		return false
 249 | 	}
 250 | 	s.urls[key] = url
 251 | 	return true
 252 | }       
 253 | 
254 |
255 | 256 |
257 |

An initializer function

258 |

259 | The URLStore struct contains a map field, 260 | which must be initialized with make before it can be used. 261 |

 262 | type URLStore struct {
 263 | 	urls map[string]string
 264 | 	mu   sync.RWMutex
 265 | }
 266 | 
267 |

268 | Go doesn't have constructors. Instead, the convention is to write a function 269 | named NewXXX that returns an intialized instance of the type. 270 |

271 |
 272 | func NewURLStore() *URLStore {
 273 | 	return &URLStore{
 274 | 		urls: make(map[string]string),
 275 | 	}
 276 | }
 277 | 
278 |
279 | 280 |
281 |

Using URLStore

282 |

283 | Creating an instance: 284 |

285 |
 286 | s := NewURLStore()
 287 | 
288 |

Setting a URL by key:

289 |
 290 | if s.Set("a", "http://google.com") {
 291 | 	// success
 292 | }
 293 | 
294 |

Getting a URL by key:

295 |
 296 | if url := s.Get("a"); url != "" {
 297 | 	// redirect to url
 298 | } else {
 299 | 	// key not found
 300 | }
 301 | 
302 |
303 | 304 |
305 |

Shortening URLs

306 |

307 | We already have the Get method for retrieving URLs. 308 | Let's create a Put method that takes a URL, 309 | stores the URL under a corresponding key, and returns that key. 310 |

311 |
 312 | func (s *URLStore) Put(url string) string {
 313 |         for {
 314 |                 key := genKey(s.Count())
 315 |                 if s.Set(key, url) {
 316 |                         return key
 317 |                 }
 318 |         }
 319 | 	panic("shouldn't get here")
 320 | }
 321 | 
 322 | func (s *URLStore) Count() int {
 323 | 	s.mu.RLock()
 324 | 	defer s.mu.RUnlock()
 325 | 	return len(s.urls)
 326 | }
 327 | 
328 |

329 | The genKey function takes an integer and returns a corresponding 330 | alphanumeric key: 331 |

332 |
 333 | func genKey(n int) string { /* implementation omitted */  }
 334 | 
335 |
336 | 337 | 338 | 339 |
340 |

HTTP Server

341 |

342 | Go's http package provides the infrastructure to serve HTTP 343 | requests: 344 |

345 |
 346 | package main
 347 | 
 348 | import (
 349 | 	"fmt"
 350 | 	"net/http"
 351 | )
 352 | 
 353 | func Hello(w http.ResponseWriter, r *http.Request) {
 354 | 	fmt.Fprintf(w, "Hello, world!")
 355 | }
 356 | 
 357 | func main()  {
 358 | 	http.HandleFunc("/", Hello)
 359 | 	http.ListenAndServe(":8080", nil)
 360 | }
 361 | 
362 |
363 | 364 | 365 | 366 |
367 |

HTTP Handlers

368 |

369 | Our program will have two HTTP handlers: 370 |

371 | 375 |

376 | The HandleFunc function is used to register them with the 377 | http package. 378 |

379 |
 380 | func main()  {
 381 | 	http.HandleFunc("/", Redirect)
 382 | 	http.HandleFunc("/add", Add)
 383 | 	http.ListenAndServe(":8080", nil)
 384 | }
 385 | 
386 |

387 | Requests to /add will be served by the Add handler.
388 | All other requests will be served by the Redirect handler. 389 |

390 | 391 |
392 |

HTTP Handlers: Add

393 |

394 | The Add function reads the url 395 | parameter from an HTTP request, Puts it into 396 | the store, and sends the corresponding short URL to the user. 397 |

398 |
 399 | func Add(w http.ResponseWriter, r *http.Request) {
 400 | 	url := r.FormValue("url")
 401 | 	key := store.Put(url)
 402 | 	fmt.Fprintf(w, "http://localhost:8080/%s", key)
 403 | }
 404 | 
405 |

406 | But what is store? 407 | It's a global variable pointing to an instance of URLStore: 408 |

409 |
 410 | var store = NewURLStore()
 411 | 
412 |

413 | The line above can appear anywhere in the top level of a source file. 414 | It will be evaluated at program initialization, before the main 415 | function is called. 416 |

417 |
418 | 419 |
420 |

HTTP Handlers: Add

421 |

422 | What about the user interface? Let's modify Add to display an 423 | HTML form when no url is supplied: 424 |

425 |
 426 | func Add(w http.ResponseWriter, r *http.Request) {
 427 | 	url := r.FormValue("url")
 428 | 	if url == "" {
 429 | 		fmt.Fprint(w, AddForm)
 430 | 		return
 431 | 	}
 432 | 	key := store.Put(url)
 433 | 	fmt.Fprintf(w, "http://localhost:8080/%s", key)
 434 | }
 435 | 
 436 | const AddForm = `
 437 | <form method="POST" action="/add">
 438 | URL: <input type="text" name="url">
 439 | <input type="submit" value="Add">
 440 | </form> 
 441 | `
 442 | 
443 | 444 |
445 |

HTTP Handlers: Redirect

446 |

447 | The Redirect function finds the key in the HTTP request path, 448 | retrieves the corresponding URL from the store, 449 | and sends an HTTP redirect to the user. 450 | If the URL is not found, a 404 "Not Found" error is sent instead. 451 |

452 |
 453 | func Redirect(w http.ResponseWriter, r *http.Request) {
 454 | 	key := r.URL.Path[1:]
 455 | 	url := store.Get(key)
 456 | 	if url == "" {
 457 | 		http.NotFound(w, r)
 458 | 		return
 459 | 	}
 460 | 	http.Redirect(w, r, url, http.StatusFound)
 461 | }
 462 | 
463 |

464 | The key is the request path minus the first character. 465 | For the request "/foo" the key would be "foo". 466 |

467 |

468 | http.NotFound and http.Redirect are helpers for 469 | sending common HTTP responses. The constant http.StatusFound 470 | represents HTTP code 302 ("Found"). 471 |

472 |
473 | 474 |
475 |

Demonstration

476 |

We've written under 100 lines of code, and have a complete, 477 | functional web application.

478 |

479 |

See the code we've written so far:

480 | 483 |

484 | These slides are available at http://wh3rd.net/practical-go/ 485 |

486 |
487 | 488 |
489 |

Persistent Storage

490 |

491 | When the goto process ends, the shortened URLs in memory will be lost. 492 |

493 |

494 | This isn't very helpful. 495 |

496 |

497 | Let's modify URLStore so that it writes its data to a file, 498 | and restores that data on start-up. 499 |

500 |
501 | 502 |
503 |

Interfaces: an aside

504 |

505 | Go's interface types define a set of methods. Any type that implements those 506 | methods satisfies that interface. 507 |

508 |

509 | One frequently-used interface is Writer, specified by the 510 | io package: 511 |

512 |
 513 | type Writer interface {
 514 | 	Write(p []byte) (n int, err error)
 515 | }
 516 | 
517 |

518 | Many types, from both the standard library and other Go code, implement the 519 | Write method described above, and can thus be used anywhere an 520 | io.Writer is expected. 521 |

522 |
523 | 524 |
525 |

Interfaces: an aside

526 |

527 | In fact, we've already used an io.Writer in our 528 | HTTP handlers: 529 |

530 |
 531 | func Add(w http.ResponseWriter, r *http.Request) {
 532 | 	...
 533 | 	fmt.Fprintf(w, "http://localhost:8080/%s", key)
 534 | }
 535 | 
536 |

537 | The Fprintf function expects an io.Writer as its 538 | first argument: 539 |

540 |
 541 | func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error)
 542 | 
543 |

544 | Because http.ResponseWriter implements the Write 545 | method, w can be passed to Fprint as an 546 | io.Writer. 547 |

548 |

549 |

550 | 551 |
552 |

Persistent Storage: json

553 |

554 | How should we represent the URLStore on disk? 555 |

556 |

557 | Go's json package handles serializing and deserializing Go 558 | data structures to and from JSON blobs. 559 |

560 |

561 | The json package's NewEncoder and 562 | NewDecoder functions wrap io.Writer and 563 | io.Reader values respectively. 564 |

565 |

566 | The resulting Encoder and Decoder objects provide 567 | Encode and Decode methods for writing and reading 568 | Go data structures. 569 |

570 |
571 | 572 |
573 |

Persistent Storage: URLStore

574 |

575 | Let's create a new data type, record, which describes how 576 | single key/url pair will be stored on disk: 577 |

578 |
 579 | type record struct {
 580 |         Key, URL string
 581 | }
 582 | 
583 |

584 | The new save method writes a given key and url 585 | to disk as a JSON-encoded record: 586 |

587 |
 588 | func (s *URLStore) save(key, url string) error {
 589 |         e := json.NewEncoder(s.file)
 590 |         return e.Encode(record{key, url})
 591 | }
 592 | 
593 |

594 | But what is s.file? 595 |

596 |
597 | 598 |
599 |

Persistent Storage: URLStore

600 |

601 | URLStore's new file field (of type *os.File) 602 | will be a handle to an open file that can be used for writing and reading. 603 |

604 |
 605 | type URLStore struct {
 606 |         urls     map[string]string
 607 |         mu       sync.RWMutex
 608 |         file     *os.File
 609 | }
 610 | 
611 |

612 | The NewURLStore function now takes a filename 613 | argument, opens the file, and stores the *os.File 614 | value in the file field: 615 |

 616 | func NewURLStore(filename string) *URLStore {
 617 |         s := &URLStore{urls: make(map[string]string)}
 618 |         f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
 619 |         if err != nil {
 620 |                 log.Fatal("URLStore:", err)
 621 |         }
 622 | 	s.file = f
 623 |         return s
 624 | }
 625 | 
626 |
627 | 628 |
629 |

Persistent Storage: URLStore

630 |

631 | The new load method will Seek to the beginning of 632 | the file, read and Decode each record, 633 | and store the data using the Set method: 634 |

635 |
 636 | func (s *URLStore) load() error {
 637 |         if _, err := s.file.Seek(0, 0); err != nil {
 638 |                 return err
 639 |         }
 640 |         d := json.NewDecoder(s.file)
 641 |         var err error
 642 |         for err == nil {
 643 |                 var r record
 644 |                 if err = d.Decode(&r); err == nil {
 645 |                         s.Set(r.Key, r.URL)
 646 |                 }
 647 |         }
 648 |         if err == os.EOF {
 649 |                 return nil
 650 |         }
 651 |         return err
 652 | }
 653 | 
654 |
655 | 656 |
657 |

Persistent Storage: URLStore

658 |

659 | Took hook it all up, first we add a call to load to the 660 | constructor function: 661 |

662 |
 663 | func NewURLStore(filename string) *URLStore {
 664 |         s := &URLStore{urls: make(map[string]string)}
 665 |         f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
 666 |         if err != nil {
 667 |                 log.Fatal("URLStore:", err)
 668 |         }
 669 |         s.file = f
 670 |         if err := s.load(); err != nil {
 671 |                 log.Println("URLStore:", err)
 672 |         }
 673 |         return s
 674 | }
 675 | 
676 |
677 | 678 |
679 |

Persistent Storage: URLStore

680 |

And save each new URL as it is Put:

681 |
 682 | func (s *URLStore) Put(url string) string {
 683 |         for {
 684 |                 key := genKey(s.Count())
 685 |                 if s.Set(key, url) {
 686 |                         if err := s.save(key, url); err != nil {
 687 |                                 log.Println("URLStore:", err)
 688 |                         }
 689 |                         return key
 690 |                 }
 691 |         }
 692 |         panic("shouldn't get here")
 693 | }
 694 | 
695 |
696 | 697 |
698 |

Persistent Storage

699 |

700 | Finally, we must specify a filename when instantiating the URLStore: 701 |

702 |
 703 | var store = NewURLStore("store.json")
 704 | 
705 | 706 |

Demonstration

707 |

See the code we've written so far:

708 | 711 |

712 | These slides are available at http://wh3rd.net/practical-go/ 713 |

714 |
715 | 716 |
717 |

A point of contention

718 |

719 | Consider this pathological situation: 720 |

721 | 731 |

732 | To remedy these issues, we should decouple the Put and 733 | save processes. 734 |

735 |
736 | 737 |
738 |

Goroutines: an aside

739 |

A goroutine is a lightweight thread managed by the Go runtime.

740 |

Goroutines are launched by a go statement. This code executes 741 | both foo and bar concurrently:

742 |
 743 | go foo()
 744 | bar()
 745 | 
746 |

747 | The foo function runs in a newly created goroutine, while 748 | bar runs in the main goroutine. 749 |

750 |

751 | Memory is shared between goroutines, like in many popular threading models. 752 |

753 |

754 | Goroutines are cheaper to create than operating system threads. 755 |

756 |
757 | 758 |
759 |

Channels: an aside

760 |

761 | A channel is a conduit, like a unix pipe, through which you can send 762 | typed values. They provide many interesting algorithmic possibilities. 763 |

764 |

765 | Like maps, channels must be initialized with make: 766 |

767 |
 768 | ch := make(chan int) // a channel of ints
 769 | 
770 |

771 | Communication is expressed using the "channel operator", <- : 772 |

773 |

 774 | ch <- 7   // send the int 7 to the channel
 775 | 
 776 | i := <-ch // receive an int from the channel
 777 | 
778 |

779 | Data always moves in the direction of the arrow. 780 |

781 |
782 | 783 |
784 |

Channels: an aside

785 |

786 | Communicating between goroutines: 787 |

788 |
 789 | func sum(x, y int, c chan int) {
 790 | 	c <- x + y
 791 | }
 792 | 
 793 | func main() {
 794 | 	c := make(chan int)
 795 | 	go sum(24, 18, c)
 796 | 	fmt.Println(<-c)
 797 | }
 798 | 
799 |

800 | Channel send/receive operations typically block until the other side is ready. 801 | Channels can be either buffered or unbuffered. 802 | Sends to a buffered channel will not block until the buffer is full. 803 |

804 |

805 | Buffered channels are initialized by specifying the buffer size as the 806 | second argument to make: 807 |

808 |
 809 | ch := make(chan int, 10)
 810 | 
811 |

812 | See the blog posts "Share Memory by Communicating" and "Timing out, moving on" for a detailed discussion of goroutines and channels. 813 |

814 |
815 | 816 |
817 |

Saving separately

818 |

819 | Instead of making a function call to save each record to disk, 820 | Put can send a record to a buffered channel: 821 |

822 |
 823 | type URLStore struct {
 824 |         urls  map[string]string
 825 |         mu    sync.RWMutex
 826 |         save  chan record
 827 | }
 828 | 
 829 | func (s *URLStore) Put(url string) string {
 830 |         for {
 831 |                 key := genKey(s.Count())
 832 |                 if s.Set(key, url) {
 833 |                         s.save <- record{key, url}
 834 |                         return key
 835 |                 }
 836 |         }
 837 |         panic("shouldn't get here")
 838 | }
 839 | 
840 |
841 | 842 |
843 |

Saving separately

844 |

845 | At the other end of the save channel we must have a receiver. 846 |

847 |

848 | This new method, saveLoop, will run in a separate goroutine.
849 | It receives record values and writes them to a file. 850 |

851 |
 852 | func (s *URLStore) saveLoop(filename string) {
 853 |         f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
 854 |         if err != nil {
 855 |                 log.Fatal("URLStore:", err)
 856 |         }
 857 |         e := json.NewEncoder(f)
 858 |         for {
 859 | 		r := <-s.save
 860 | 		if err := e.Encode(r); err != nil {
 861 |                         log.Println("URLStore:", err)
 862 |                 }
 863 |         }
 864 | }
 865 | 
866 |
867 | 868 |
869 |

Saving separately

870 |

871 | We need to modify the NewURLStore function to launch the 872 | saveLoop goroutine (and remove the now-unnecessary file opening 873 | code): 874 |

875 |
 876 | const saveQueueLength = 1000
 877 | 
 878 | func NewURLStore(filename string) *URLStore {
 879 |         s := &URLStore{
 880 |                 urls: make(map[string]string),
 881 |                 save: make(chan record, saveQueueLength),
 882 |         }
 883 |         if err := s.load(filename); err != nil {
 884 |                 log.Println("URLStore:", err)
 885 |         }
 886 |         go s.saveLoop(filename)
 887 |         return s
 888 | }
 889 | 
890 |
891 | 892 |
893 |

An aside: Command-line flags

894 |

895 | Go's flag package makes it easy to handle command-line flags. 896 | Let's use it to replace the constants we have in our code so far. 897 |

898 |
 899 | import (
 900 | 	"encoding/json"
 901 | 	"flag"
 902 | 	"fmt"
 903 | 	"net/http"
 904 | )
 905 | 
906 |

907 | We first create some global variables to hold the flag values: 908 |

909 |
 910 | var (
 911 | 	listenAddr = flag.String("http", ":8080", "http listen address")
 912 | 	dataFile   = flag.String("file", "store.json", "data store file name")
 913 | 	hostname   = flag.String("host", "localhost:8080", "host name and port")
 914 | )
 915 | 
916 |
917 | 918 |
919 |

An aside: Command-line flags

920 |

921 | Then we can add flag.Parse() to the main function, 922 | and instantiate the URLStore after the flags have been parsed 923 | (once we know the value of *dataFile). 924 |

925 |
 926 | var store *URLStore
 927 | 
 928 | func main() {
 929 | 	flag.Parse()
 930 | 	store = NewURLStore(*dataFile)
 931 |         http.HandleFunc("/", Redirect)
 932 |         http.HandleFunc("/add", Add)
 933 |         http.ListenAndServe(*listenAddr, nil)
 934 | }
 935 | 
936 |

937 | And substitute *hostname in the Add handler: 938 |

939 |
 940 | 	fmt.Fprintf(w, "http://%s/%s", *hostname, key)
 941 | 
942 |
943 | 944 |
945 |

Demonstration

946 |

See the code we've written so far:

947 | 950 |

951 | These slides are available at http://wh3rd.net/practical-go/ 952 |

953 |
954 | 955 |
956 |

One more thing...

957 | 958 |

959 | So far we have a program that runs as a single process. But a single process 960 | running on one machine can only serve so many concurrent requests. 961 |

962 |

963 | A URL Shortener typically serves many more Redirects (reads) than it does 964 | Adds (writes). 965 |

966 |

967 | Therefore we can create an arbitrary number of read-only slaves that serve and 968 | cache Get requests, and pass Puts through to the master. 969 |

970 |
971 | 972 |
973 |

Making URLStore an RPC service

974 |

975 | Go's rpc package provides a convenient means of making function 976 | calls over a network connection. 977 |

978 |

979 | Given a value, rpc will expose to the network the value's methods 980 | that meet this function signature: 981 |

982 |
 983 | func (t T) Name(args *ArgType, reply *ReplyType) error
 984 | 
985 |

986 | To make URLStore an RPC service we need to alter the 987 | Put and Get methods so that they match this 988 | function signature: 989 |

990 |
 991 | func (s *URLStore) Get(key, url *string) error
 992 | 
 993 | func (s *URLStore) Put(url, key *string) error
 994 | 
995 |

996 | And, of course, we need to change the call sites to call these functions 997 | appropriately. 998 |

999 |
1000 | 1001 |
1002 |

Making URLStore an RPC service

1003 |

1004 | To make URLStore an RPC services, we need to alter the 1005 | Get and Put methods to be rpc-friendly. 1006 | The function signatures change, and now return an error value. 1007 |

1008 |

1009 | The Get method can return an explicit error when the provided key 1010 | is not found: 1011 |

1012 |
1013 | func (s *URLStore) Get(key, url *string) error {
1014 |         s.mu.RLock()
1015 |         defer s.mu.RUnlock()
1016 |         if u, ok := s.urls[*key]; ok {
1017 |                 *url = u
1018 |                 return nil
1019 |         }
1020 |         return os.NewError("key not found")
1021 | }
1022 | 
1023 |

1024 | Beyond the function signature, the Put method barely changes 1025 | in its actual code (not shown here): 1026 |

1027 |
1028 | func (s *URLStore) Put(url, key *string) error
1029 | 
1030 |
1031 | 1032 |
1033 |

Making URLStore an RPC service

1034 |

1035 | In turn, the HTTP handlers must be modfied to accommodate the changes to 1036 | URLStore. 1037 |

1038 |

1039 | The Redirect handler now displays the error returned by 1040 | the URLStore: 1041 |

1042 |
1043 | func Redirect(w http.ResponseWriter, r *http.Request) {
1044 | 	key := r.URL.Path[1:]
1045 | 	var url string
1046 | 	if err := store.Get(&key, &url); err != nil {
1047 | 	        http.Error(w, err.Error(), http.StatusInternalServerError)
1048 | 	        return
1049 | 	}
1050 | 	http.Redirect(w, r, url, http.StatusFound)
1051 | }
1052 | 
1053 |
1054 | 1055 |
1056 |

Making URLStore an RPC service

1057 |

The Add handler changes in much the same way:

1058 |
1059 | func Add(w http.ResponseWriter, r *http.Request) {
1060 | 	url := r.FormValue("url")
1061 | 	if url == "" {
1062 | 	        fmt.Fprint(w, AddForm)
1063 | 	        return
1064 | 	}
1065 | 	var key string
1066 | 	if err := store.Put(&url, &key); err != nil {
1067 | 	        http.Error(w, err.Error(), http.StatusInternalServerError)
1068 | 	        return
1069 | 	}
1070 | 	fmt.Fprintf(w, "http://%s/%s", *hostname, key)
1071 | }
1072 | 
1073 |
1074 | 1075 |
1076 |

Making URLStore an RPC service

1077 |

Add a command-line flag to enable the RPC server:

1078 |
1079 | var rpcEnabled = flag.Bool("rpc", false, "enable RPC server")
1080 | 
1081 |

1082 | And then Register the URLStore with the 1083 | rpc package and set up the RPC-over-HTTP handler with 1084 | HandleHTTP. 1085 |

1086 |
1087 | func main() {
1088 | 	flag.Parse()
1089 | 	store = URLStore(*dataFile)
1090 | 	if *rpcEnabled {
1091 | 	        rpc.RegisterName("Store", store)
1092 | 	        rpc.HandleHTTP()
1093 | 	}
1094 | 	... (set up http)
1095 | }
1096 | 
1097 |
1098 | 1099 |
1100 |

ProxyStore

1101 |

1102 | Now that we have the URLStore available as an RPC service, we can build 1103 | another type that forwards requests to the RPC server. 1104 |

1105 |

1106 | We will call it ProxyStore: 1107 |

1108 |
1109 | type ProxyStore struct {
1110 |         client *rpc.Client
1111 | }
1112 | 
1113 | func NewProxyStore(addr string) *ProxyStore {
1114 |         client, err := rpc.DialHTTP("tcp", addr)
1115 |         if err != nil {
1116 |                 log.Println("ProxyStore:", err)
1117 |         }
1118 |         return &ProxyStore{client: client}
1119 | }
1120 | 
1121 |
1122 | 1123 |
1124 |

ProxyStore

1125 |

1126 | Its Get and Put methods pass the requests directly 1127 | to the RPC server: 1128 |

1129 |
1130 | func (s *ProxyStore) Get(key, url *string) error {
1131 |         return s.client.Call("Store.Get", key, url)
1132 | }
1133 | 
1134 | func (s *ProxyStore) Put(url, key *string) error {
1135 |         return s.client.Call("Store.Put", url, key)
1136 | }
1137 | 
1138 |

1139 | But there's something missing: the slave must cache the data it 1140 | gets from the master, otherwise it provides no benefit. 1141 |

1142 |
1143 | 1144 |
1145 |

A caching ProxyStore

1146 |

1147 | We already have the perfect data structure for caching this data, 1148 | the URLStore. 1149 |

1150 |

1151 | Let's add a URLStore field to ProxyStore: 1152 |

1153 |
1154 | type ProxyStore struct {
1155 | 	urls   *URLStore
1156 | 	client *rpc.Client
1157 | }
1158 | 
1159 | func NewProxyStore(addr string) *ProxyStore {
1160 | 	client, err := rpc.DialHTTP("tcp", addr)
1161 | 	if err != nil {
1162 | 	        log.Println("ProxyStore:", err)
1163 | 	}
1164 | 	return &ProxyStore{urls: NewURLStore(""), client: client}
1165 | }
1166 | 
1167 |

1168 | (And we must modify the URLStore so that it doesn't try to write 1169 | to or read from disk if an empty filename is given — no big deal.) 1170 |

1171 |
1172 | 1173 |
1174 |

A caching ProxyStore

1175 |

1176 | The Get method should first check if the key is in the cache. 1177 | If present, Get should return the cached result. If not, 1178 | it should make the RPC call, and update its local cache with the result. 1179 |

1180 |
1181 | func (s *ProxyStore) Get(key, url *string) error {
1182 |         if err := s.urls.Get(key, url); err == nil {
1183 |                 return nil
1184 |         }
1185 |         if err := s.client.Call("Store.Get", key, url); err != nil {
1186 |                 return err
1187 |         }
1188 |         s.urls.Set(key, url)
1189 |         return nil
1190 | }
1191 | 
1192 |
1193 | 1194 |
1195 |

A caching ProxyStore

1196 |

1197 | The Put method need only update the cache when it performs a 1198 | successful RPC Put. 1199 |

1200 |
1201 | func (s *ProxyStore) Put(url, key *string) error {
1202 |         if err := s.client.Call("Store.Put", url, key); err != nil {
1203 |                 return err
1204 |         }
1205 |         s.urls.Set(key, url)
1206 |         return nil
1207 | }
1208 | 
1209 |
1210 | 1211 | 1212 |
1213 |

Integrating ProxyStore

1214 |

1215 | Now we want to be able to use ProxyStore with the web front-end, 1216 | in the place of URLStore. 1217 |

1218 |

1219 | Since they both implement the same Get and Put 1220 | methods, we can specify an interface to generalize their behavior: 1221 |

1222 |
1223 | type Store interface {
1224 |         Put(url, key *string) error
1225 |         Get(key, url *string) error
1226 | }
1227 | 
1228 |

1229 | Now our global variable store can be of type Store: 1230 |

1231 |
1232 | var store Store
1233 | 
1234 |
1235 | 1236 |
1237 |

Integrating ProxyStore

1238 |

1239 | Our main function can instantiate either a URLStore 1240 | or ProxyStore depending on a new command-line flag: 1241 |

1242 |
1243 | var masterAddr = flag.String("master", "", "RPC master address")
1244 | 
1245 |
1246 | func main() {
1247 |         flag.Parse() 
1248 |         if *masterAddr != "" {
1249 | 		// we are a slave
1250 |                 store = NewProxyStore(*masterAddr)
1251 |         } else {
1252 | 		// we are the master
1253 |                 store = NewURLStore(*dataFile)
1254 |         }
1255 | 	...
1256 | }
1257 | 
1258 |

1259 | The rest of the front-end code continues to work as before. 1260 | No need to tell it about the Store interface — it just works! 1261 |

1262 |
1263 | 1264 |
1265 |

Final demonstration

1266 |

1267 | Now we can launch a master and several slaves, and stress-test the slaves. 1268 |

1269 |

See the complete program:

1270 | 1273 |

1274 | These slides are available at http://wh3rd.net/practical-go/ 1275 |

1276 |
1277 | 1278 |
1279 |

Exercises for the reader (or listener)

1280 |

1281 | While this program does what we set out to do, there are a few ways it could 1282 | be improved: 1283 |

1284 | 1298 |
1299 | 1300 |
1301 |

Go Resources

1302 | 1316 |
1317 | 1318 |
1319 |

Questions?

1320 |

Andrew Gerrand

1321 |

adg@golang.org

1322 |

http://wh3rd.net/practical-go/

1323 |

1324 |
1325 | 1326 | 1327 | 1328 | 1329 | 1330 | -------------------------------------------------------------------------------- /talk/notes.txt: -------------------------------------------------------------------------------- 1 | 2 | - Let's build a URL shortener. 3 | - Users put a URL in a form, get a shortened URL in return 4 | 5 | - Start with the data; create a data type to store our key->url map 6 | - ASIDE: the map type, make 7 | - type URLMap map[string]string 8 | - add Get and Set methods 9 | - but what about concurrent access? this isn't thread safe 10 | - make it a struct with RWMutex, add lock/unlock to methods 11 | - add constructor functon, NewURLMap 12 | 13 | - We need an interface to Put URLs in exchange for a generated key, 14 | and Get URLs by key. 15 | - type URLStore struct { urls *URLMap } 16 | - NewURLStore function creates and returns a URLStore 17 | - Put method generates the key 18 | - show (but don't explain) keyGen function in key.go 19 | (maybe drop this and just use numeric keys) 20 | - add 'count int' to struct 21 | - this isn't thread safe! two clients could get the same key 22 | - add a mutex to URLStore 23 | - Get method is trivial 24 | 25 | - The web interface: 26 | - main function 27 | - http Handle and Listen 28 | - demo http server that does nothing (404) 29 | - global variable store = NewURLStore() 30 | - Add function 31 | - HTML form constant 32 | - pull url from r.FormValue 33 | - key = store.Put(url) 34 | - output new url 35 | - main: Handle("/add", Add) 36 | - demo visiting /add a couple of times, watch keys increment 37 | (get suggestions of urls to add from crowd) 38 | - Redirect function 39 | - key = r.URL.Path[1:] 40 | - url = store.Get(key) 41 | - if not found, http.NotFound 42 | - http.Redirect(w, r, url, http.StatusFound) 43 | - demo redirects /0, /1, etc 44 | - this is using Go's concurrency features "behind the scenes" - more later 45 | 46 | - Oh no! The process ended and we lost all our data! We need persistent storage. 47 | - ASIDE: interfaces: 48 | - os.Error interface 49 | - Reader and Writer interfaces 50 | - implement WriteTo on URLMap 51 | - ASIDE: json 52 | - create encoder 53 | - take a read lock 54 | - encode to writer 55 | - release lock 56 | - return error 57 | - this is more verbose than it need be: 58 | - ASIDE: defer 59 | - defer releasing the lock, save a line 60 | - implement ReadFrom 61 | - decoder 62 | - take write lock, deferring unlock 63 | - decode from reader 64 | - add filename to URLStore struct and NewURLStore args 65 | - save method on URLStore 66 | - open the file for writing (explain flags) 67 | - handle errors 68 | - defer Close 69 | - WriteTo f 70 | - load method 71 | - open file for reading 72 | - handle errors 73 | - defer Close 74 | - ReadFrom f 75 | - add load to NewURLStore 76 | - add save to Put 77 | - demo our file store now works: 78 | - launch app, add some urls, kill app, reload, urls still there 79 | 80 | - Let's remove some 'magic numbers' from the code: 81 | - command-line flags 82 | - ASIDE: the flag package 83 | - listenAddr, hostname, filename 84 | - replace constants 85 | - flag.Parse in main 86 | - demo working command lines 87 | 88 | - Another issue: When many clients try to add URLs in quick succession and the 89 | URL list is saved to disk after each addition, each client will need to wait 90 | for the list to be saved before their request will go through. 91 | - It would be better to save the list periodically. 92 | - So far, we've used Go's concurrency features "behind the scenes", but 93 | now we want to explicity create another thread that periodically saves the 94 | list. 95 | - ASIDE: goroutines 96 | - create saveLoop 97 | - time.Sleep 98 | - s.save() 99 | - put log.Println("Saving") for explanatory purposes 100 | - add "go s.saveLoop()" to NewURLStore 101 | - remove s.save() from Put 102 | - demo the periodical saving 103 | - but this will rewrite the file regardless of whether it's been updated. 104 | we need to signal the saveLoop goroutine when it should save the file 105 | - ASIDE: channels 106 | - add 'dirty chan bool' to struct 107 | - add '<-s.dirty' to saveLoop 108 | - add make(chan bool, 1) to NewURLStore 109 | - add '_ = s.dirty <- true' to save 110 | - demo periodical saving only after url add 111 | 112 | - So far we have a program that runs as a single process. But a single process 113 | running on one machine can only serve so many simultaenous requests. 114 | - A URL Shortener typically serves many more Redirects (reads) than it does 115 | Adds (writes). 116 | - We can create an arbitrary number of read-only slaves that cache and 117 | serve Get requests, and pass Puts through to the master. 118 | - ASIDE: rpc 119 | - a simple means of communicating between Go programs over a network, 120 | - trivial rpc example 121 | - master: 122 | - Let's make the URLStore into an rpc service by changing the Get and Put 123 | methods slightly. 124 | - Get has slightly different logic, now returns an error if not found 125 | - Put is trivially modified 126 | - The call sites become slightly more complicated: 127 | - Redirect must pre-declare "var url string" 128 | - Add must provide "var key string" 129 | - both must check the error value, and report it to the client 130 | (this will become very useful shortly) 131 | - add 'rpc' flag to enable rpc server 132 | - if rpc = true, register the URLStore with the rpc package in main() 133 | - rpc.RegisterName("Store", store) 134 | (we override the name for reasons we'll see shortly) 135 | - rpc.HandleHTTP() to enable RPC-over-HTTP 136 | - demo the methods registered at /debug/rpc 137 | - slave: 138 | - create ProxyStore struct, with *rpc.Client inside 139 | - NewProxyStore takes addr, connects, and returns *ProxyStore 140 | - Get makes s.client.Call("Store.Get"...) 141 | - Put makes s.client.Call("Store.Put"...) 142 | - we want to be able to use this new store interchangably with the URLStore 143 | this is a job for interfaces! 144 | - create Store interface type 145 | - global variable store becomes "var store Store" 146 | - add 'master' command-line flag 147 | - main() to instantiate ProxyStore or URLStore depending if master is set 148 | - demo working proxy, two processes communicating with each other 149 | - one last thing: caching on the slave side 150 | - add *URLMap to ProxyStore struct 151 | - initialize it in NewProxyStore 152 | - add calls to urls.Get/Set to Get and Put 153 | - demo stress test of 3 slaves, 1 master 154 | - little instrumentation module 155 | 156 | -------------------------------------------------------------------------------- /talk/slidy.css: -------------------------------------------------------------------------------- 1 | /* slidy.css 2 | 3 | Copyright (c) 2005 W3C (MIT, ERCIM, Keio), All Rights Reserved. 4 | W3C liability, trademark, document use and software licensing 5 | rules apply, see: 6 | 7 | http://www.w3.org/Consortium/Legal/copyright-documents 8 | http://www.w3.org/Consortium/Legal/copyright-software 9 | */ 10 | body 11 | { 12 | margin: 0 0 0 0; 13 | padding: 0 0 0 0; 14 | width: 100%; 15 | height: 100%; 16 | color: black; 17 | background-color: white; 18 | font-family: Helvetica, sans-serif; 19 | font-size: 18pt; 20 | } 21 | 22 | .hidden { display: none; visibility: hidden } 23 | 24 | div.toolbar { 25 | position: fixed; z-index: 200; 26 | top: auto; bottom: 0; left: 0; right: 0; 27 | height: 1.2em; text-align: right; 28 | padding-left: 1em; 29 | padding-right: 1em; 30 | font-size: 60%; 31 | color: red; background: rgb(240,240,240); 32 | } 33 | 34 | div.background { 35 | display: none; 36 | } 37 | 38 | div.handout { 39 | margin-left: 20px; 40 | margin-right: 20px; 41 | } 42 | 43 | div.slide.titlepage { 44 | text-align: center; 45 | color: white; 46 | background: black; 47 | } 48 | 49 | div.slide.titlepage h1 { 50 | padding: 5% 0 0; 51 | padding-top: 5%; 52 | margin: 0; 53 | } 54 | 55 | div.slide.titlepage.end h1 { 56 | padding-top: 5%; 57 | } 58 | 59 | div.slide { 60 | z-index: 20; 61 | margin: 0 0 0 0; 62 | padding-top: 20px; 63 | padding-bottom: 0; 64 | padding-left: 40px; 65 | padding-right: 40px; 66 | border-width: 0; 67 | top: 0; 68 | bottom: 0; 69 | left: 0; 70 | right: 0; 71 | line-height: 120%; 72 | background-color: transparent; 73 | } 74 | 75 | /* this rule is hidden from IE 6 and below which don't support + selector */ 76 | div.slide + div[class].slide { page-break-before: always;} 77 | 78 | div.slide h1 { 79 | padding-left: 0; 80 | padding-right: 20pt; 81 | padding-top: 10pt; 82 | padding-bottom: 4pt; 83 | margin-top: 0; 84 | margin-left: 0; 85 | margin-right: 60pt; 86 | margin-bottom: 0.5em; 87 | display: block; 88 | font-size: 160%; 89 | line-height: 1.2em; 90 | background: transparent; 91 | } 92 | 93 | div.toc { 94 | position: absolute; 95 | top: auto; 96 | bottom: 4em; 97 | left: 4em; 98 | right: auto; 99 | width: 60%; 100 | max-width: 30em; 101 | height: 30em; 102 | border: solid thin black; 103 | padding: 1em; 104 | background: rgb(240,240,240); 105 | color: black; 106 | z-index: 300; 107 | overflow: auto; 108 | display: block; 109 | visibility: visible; 110 | } 111 | 112 | div.toc-heading { 113 | width: 100%; 114 | border-bottom: solid 1px rgb(180,180,180); 115 | margin-bottom: 1em; 116 | text-align: center; 117 | } 118 | 119 | pre { 120 | line-height: 120%; 121 | padding-top: 0.5em; 122 | padding-bottom: 0.5em; 123 | padding-left: 1em; 124 | padding-right: 1em; 125 | border-style: solid; 126 | border-left-width: thin; 127 | border-top-width: thin; 128 | border-right-width: thin; 129 | border-bottom-width: thin; 130 | border-color: #95ABD0; 131 | background-color: #eee; 132 | } 133 | pre, code { 134 | color: #00428C; 135 | font-weight: bold; 136 | } 137 | pre em { 138 | color: #8C4200; 139 | font-style: normal; 140 | } 141 | pre span { 142 | color: #888; 143 | } 144 | 145 | li pre { margin-left: 0; } 146 | 147 | @media print { 148 | div.slide { 149 | display: block; 150 | visibility: visible; 151 | position: relative; 152 | border-top-style: solid; 153 | border-top-width: thin; 154 | border-top-color: black; 155 | } 156 | div.slide pre { font-size: 60%; padding-left: 0.5em; } 157 | div.handout { display: block; visibility: visible; } 158 | } 159 | 160 | blockquote { font-style: italic } 161 | 162 | img { background-color: transparent } 163 | 164 | p.copyright { font-size: smaller } 165 | 166 | .center { text-align: center } 167 | .footnote { font-size: smaller; margin-left: 2em; } 168 | 169 | a img { border-width: 0; border-style: none } 170 | 171 | a:visited { color: navy } 172 | a:link { color: navy } 173 | a:hover { color: red; text-decoration: underline } 174 | a:active { color: red; text-decoration: underline } 175 | 176 | a {text-decoration: none} 177 | .navbar a:link {color: white} 178 | .navbar a:visited {color: yellow} 179 | .navbar a:active {color: red} 180 | .navbar a:hover {color: red} 181 | 182 | ul { list-style-type: square; } 183 | ul ul { list-style-type: disc; } 184 | ul ul ul { list-style-type: circle; } 185 | ul ul ul ul { list-style-type: disc; } 186 | li { margin-left: 0.5em; margin-top: 0.5em; } 187 | 188 | div dt 189 | { 190 | margin-left: 0; 191 | margin-top: 1em; 192 | margin-bottom: 0.5em; 193 | font-weight: bold; 194 | } 195 | div dd 196 | { 197 | margin-left: 2em; 198 | margin-bottom: 0.5em; 199 | } 200 | 201 | 202 | p,pre,ul,ol,blockquote,h2,h3,h4,h5,h6,dl,table { 203 | margin-left: 1em; 204 | margin-right: 1em; 205 | } 206 | 207 | p.subhead { font-weight: bold; margin-top: 2em; } 208 | 209 | .smaller { font-size: smaller } 210 | .bigger { font-size: 130% } 211 | 212 | td,th { padding: 0.2em } 213 | 214 | ul { 215 | margin: 0.5em 1.5em 0.5em 1.5em; 216 | padding: 0; 217 | } 218 | 219 | ol { 220 | margin: 0.5em 1.5em 0.5em 1.5em; 221 | padding: 0; 222 | } 223 | 224 | ul { list-style-type: square; } 225 | ul ul { list-style-type: disc; } 226 | ul ul ul { list-style-type: circle; } 227 | ul ul ul ul { list-style-type: disc; } 228 | 229 | ul li { 230 | list-style: square; 231 | margin: 0.1em 0em 0.6em 1.0em; 232 | padding: 0 0 0 0; 233 | line-height: 140%; 234 | } 235 | 236 | ol li { 237 | margin: 0.1em 0em 0.6em 1.5em; 238 | padding: 0 0 0 0px; 239 | line-height: 140%; 240 | list-style-type: decimal; 241 | } 242 | 243 | li ul li { 244 | list-style-type: disc; 245 | background: transparent; 246 | padding: 0 0 0 0; 247 | } 248 | li li ul li { 249 | list-style-type: circle; 250 | background: transparent; 251 | padding: 0 0 0 0; 252 | } 253 | li li li ul li { 254 | list-style-type: disc; 255 | background: transparent; 256 | padding: 0 0 0 0; 257 | } 258 | 259 | li ol li { 260 | list-style-type: decimal; 261 | } 262 | 263 | 264 | li li ol li { 265 | list-style-type: decimal; 266 | } 267 | 268 | /* 269 | setting class="outline on ol or ul makes it behave as an 270 | ouline list where blocklevel content in li elements is 271 | hidden by default and can be expanded or collapsed with 272 | mouse click. Set class="expand" on li to override default 273 | */ 274 | 275 | ol.outline li:hover { cursor: pointer } 276 | ol.outline li.nofold:hover { cursor: default } 277 | 278 | ul.outline li:hover { cursor: pointer } 279 | ul.outline li.nofold:hover { cursor: default } 280 | 281 | ol.outline { list-style:decimal; } 282 | ol.outline ol { list-style-type:lower-alpha } 283 | 284 | ol.outline li.nofold { 285 | padding: 0 0 0 20px; 286 | background: transparent url(nofold-dim.gif) no-repeat 0px 0.5em; 287 | } 288 | ol.outline li.unfolded { 289 | padding: 0 0 0 20px; 290 | background: transparent url(fold-dim.gif) no-repeat 0px 0.5em; 291 | } 292 | ol.outline li.folded { 293 | padding: 0 0 0 20px; 294 | background: transparent url(unfold-dim.gif) no-repeat 0px 0.5em; 295 | } 296 | ol.outline li.unfolded:hover { 297 | padding: 0 0 0 20px; 298 | background: transparent url(fold.gif) no-repeat 0px 0.5em; 299 | } 300 | ol.outline li.folded:hover { 301 | padding: 0 0 0 20px; 302 | background: transparent url(unfold.gif) no-repeat 0px 0.5em; 303 | } 304 | 305 | ul.outline li.nofold { 306 | padding: 0 0 0 20px; 307 | background: transparent url(nofold-dim.gif) no-repeat 0px 0.5em; 308 | } 309 | ul.outline li.unfolded { 310 | padding: 0 0 0 20px; 311 | background: transparent url(fold-dim.gif) no-repeat 0px 0.5em; 312 | } 313 | ul.outline li.folded { 314 | padding: 0 0 0 20px; 315 | background: transparent url(unfold-dim.gif) no-repeat 0px 0.5em; 316 | } 317 | ul.outline li.unfolded:hover { 318 | padding: 0 0 0 20px; 319 | background: transparent url(fold.gif) no-repeat 0px 0.5em; 320 | } 321 | ul.outline li.folded:hover { 322 | padding: 0 0 0 20px; 323 | background: transparent url(unfold.gif) no-repeat 0px 0.5em; 324 | } 325 | 326 | /* for slides with class "title" in table of contents */ 327 | a.titleslide { font-weight: bold; font-style: italic } 328 | 329 | .contrasts em { 330 | color: #642; 331 | } 332 | 333 | .eg { 334 | color: #555; 335 | font-style: italic; 336 | } 337 | -------------------------------------------------------------------------------- /talk/slidy.js: -------------------------------------------------------------------------------- 1 | /* slidy.js 2 | 3 | Copyright (c) 2005-2009 W3C (MIT, ERCIM, Keio), All Rights Reserved. 4 | W3C liability, trademark, document use and software licensing 5 | rules apply, see: 6 | 7 | http://www.w3.org/Consortium/Legal/copyright-documents 8 | http://www.w3.org/Consortium/Legal/copyright-software 9 | */ 10 | 11 | var ns_pos = (typeof window.pageYOffset!='undefined'); 12 | var khtml = ((navigator.userAgent).indexOf("KHTML") >= 0 ? true : false); 13 | var opera = ((navigator.userAgent).indexOf("Opera") >= 0 ? true : false); 14 | var ie = (typeof document.all != "undefined" && !opera); 15 | var ie7 = (!ns_pos && navigator.userAgent.indexOf("MSIE 7") != -1); 16 | var ie8 = (!ns_pos && navigator.userAgent.indexOf("MSIE 8") != -1); 17 | var slidy_started = false; 18 | 19 | if (ie && !ie8) 20 | document.write(""); 21 | 22 | // IE only event handlers to ensure all slides are printed 23 | // I don't yet know how to emulate these for other browsers 24 | if (typeof beforePrint != 'undefined') 25 | { 26 | window.onbeforeprint = beforePrint; 27 | window.onafterprint = afterPrint; 28 | } 29 | 30 | // to avoid a clash with other scripts or onload attribute on 31 | // we use something smarter than window.onload 32 | //window.onload = startup; 33 | 34 | 35 | if (ie) 36 | setTimeout(ieSlidyInit, 100); 37 | else if (document.addEventListener) 38 | document.addEventListener("DOMContentLoaded", startup, false); 39 | 40 | function ieSlidyInit() 41 | { 42 | if (//document.readyState == "interactive" || 43 | document.readyState == "complete" || 44 | document.readyState == "loaded") 45 | { 46 | startup(); 47 | } 48 | else 49 | { 50 | setTimeout(ieSlidyInit, 100); 51 | } 52 | } 53 | 54 | setTimeout(hideSlides, 50); 55 | 56 | function hideSlides() 57 | { 58 | if (document.body) 59 | document.body.style.visibility = "hidden"; 60 | else 61 | setTimeout(hideSlides, 50); 62 | } 63 | 64 | var slidenum = 0; // integer slide count: 0, 1, 2, ... 65 | var slides; // set to array of slide div's 66 | var slideNumElement; // element containing slide number 67 | var notes; // set to array of handout div's 68 | var backgrounds; // set to array of background div's 69 | var toolbar; // element containing toolbar 70 | var title; // document title 71 | var lastShown = null; // last incrementally shown item 72 | var eos = null; // span element for end of slide indicator 73 | var toc = null; // table of contents 74 | var outline = null; // outline element with the focus 75 | var selectedTextLen; // length of drag selection on document 76 | 77 | var viewAll = 0; // 1 to view all slides + handouts 78 | var wantToolbar = 1; // 0 if toolbar isn't wanted 79 | var mouseClickEnabled = true; // enables left click for next slide 80 | var scrollhack = 0; // IE work around for position: fixed 81 | 82 | var helpAnchor; // used for keyboard focus hack in showToolbar() 83 | var helpPage = "http://www.w3.org/Talks/Tools/Slidy/help.html"; 84 | var helpText = "Navigate with mouse click, space bar, Cursor Left/Right, " + 85 | "or Pg Up and Pg Dn. Use S and B to change font size."; 86 | 87 | var sizeIndex = 0; 88 | var sizeAdjustment = 0; 89 | var sizes = new Array("10pt", "12pt", "14pt", "16pt", "18pt", "20pt", 90 | "22pt", "24pt", "26pt", "28pt", "30pt", "32pt"); 91 | var okayForIncremental = incrementalElementList(); 92 | 93 | // needed for efficient resizing 94 | var lastWidth = 0; 95 | var lastHeight = 0; 96 | 97 | // Needed for cross browser support for relative width/height on 98 | // object elements. The work around is to save width/height attributes 99 | // and then to recompute absolute width/height dimensions on resizing 100 | var objects; 101 | 102 | // updated to language specified by html file 103 | var lang = "en"; 104 | 105 | //var localize = {}; 106 | 107 | // for each language there is an associative array 108 | var strings_es = { 109 | "slide":"pág.", 110 | "help?":"Ayuda", 111 | "contents?":"Índice", 112 | "table of contents":"tabla de contenidos", 113 | "Table of Contents":"Tabla de Contenidos", 114 | "restart presentation":"Reiniciar presentación", 115 | "restart?":"Inicio" 116 | }; 117 | 118 | strings_es[helpText] = 119 | "Utilice el ratón, barra espaciadora, teclas Izda/Dcha, " + 120 | "o Re pág y Av pág. Use S y B para cambiar el tamaño de fuente."; 121 | 122 | var strings_ca = { 123 | "slide":"pàg..", 124 | "help?":"Ajuda", 125 | "contents?":"Índex", 126 | "table of contents":"taula de continguts", 127 | "Table of Contents":"Taula de Continguts", 128 | "restart presentation":"Reiniciar presentació", 129 | "restart?":"Inici" 130 | }; 131 | 132 | strings_ca[helpText] = 133 | "Utilitzi el ratolí, barra espaiadora, tecles Esq./Dta. " + 134 | "o Re pàg y Av pàg. Usi S i B per canviar grandària de font."; 135 | 136 | var strings_nl = { 137 | "slide":"pagina", 138 | "help?":"Help?", 139 | "contents?":"Inhoud?", 140 | "table of contents":"inhoudsopgave", 141 | "Table of Contents":"Inhoudsopgave", 142 | "restart presentation":"herstart presentatie", 143 | "restart?":"Herstart?" 144 | }; 145 | 146 | strings_nl[helpText] = 147 | "Navigeer d.m.v. het muis, spatiebar, Links/Rechts toetsen, " + 148 | "of PgUp en PgDn. Gebruik S en B om de karaktergrootte te veranderen."; 149 | 150 | var strings_de = { 151 | "slide":"Seite", 152 | "help?":"Hilfe", 153 | "contents?":"Übersicht", 154 | "table of contents":"Inhaltsverzeichnis", 155 | "Table of Contents":"Inhaltsverzeichnis", 156 | "restart presentation":"Präsentation neu starten", 157 | "restart?":"Neustart" 158 | }; 159 | 160 | strings_de[helpText] = 161 | "Benutzen Sie die Maus, Leerschlag, die Cursortasten links/rechts oder " + 162 | "Page up/Page Down zum Wechseln der Seiten und S und B für die Schriftgrösse."; 163 | 164 | var strings_pl = { 165 | "slide":"slajd", 166 | "help?":"pomoc?", 167 | "contents?":"spis treści?", 168 | "table of contents":"spis treści", 169 | "Table of Contents":"Spis Treści", 170 | "restart presentation":"Restartuj prezentację", 171 | "restart?":"restart?" 172 | }; 173 | 174 | strings_pl[helpText] = 175 | "Zmieniaj slajdy klikając myszą, naciskając spację, strzałki lewo/prawo" + 176 | "lub PgUp / PgDn. Użyj klawiszy S i B, aby zmienić rozmiar czczionki."; 177 | 178 | var strings_fr = { 179 | "slide":"page", 180 | "help?":"Aide", 181 | "contents?":"Index", 182 | "table of contents":"table des matières", 183 | "Table of Contents":"Table des matières", 184 | "restart presentation":"Recommencer l'exposé", 185 | "restart?":"Début" 186 | }; 187 | 188 | strings_fr[helpText] = 189 | "Naviguez avec la souris, la barre d'espace, les flèches " + 190 | "gauche/droite ou les touches Pg Up, Pg Dn. Utilisez " + 191 | "les touches S et B pour modifier la taille de la police."; 192 | 193 | var strings_hu = { 194 | "slide":"oldal", 195 | "help?":"segítség", 196 | "contents?":"tartalom", 197 | "table of contents":"tartalomjegyzék", 198 | "Table of Contents":"Tartalomjegyzék", 199 | "restart presentation":"bemutató újraindítása", 200 | "restart?":"újraindítás" 201 | }; 202 | 203 | strings_hu[helpText] = 204 | "Az oldalak közti lépkedéshez kattintson az egérrel, vagy " + 205 | "használja a szóköz, a bal, vagy a jobb nyíl, illetve a Page Down, " + 206 | "Page Up billentyűket. Az S és a B billentyűkkel változtathatja " + 207 | "a szöveg méretét."; 208 | 209 | var strings_it = { 210 | "slide":"pag.", 211 | "help?":"Aiuto", 212 | "contents?":"Indice", 213 | "table of contents":"indice", 214 | "Table of Contents":"Indice", 215 | "restart presentation":"Ricominciare la presentazione", 216 | "restart?":"Inizio" 217 | }; 218 | 219 | strings_it[helpText] = 220 | "Navigare con mouse, barra spazio, frecce sinistra/destra o " + 221 | "PgUp e PgDn. Usare S e B per cambiare la dimensione dei caratteri."; 222 | 223 | var strings_el = { 224 | "slide":"σελίδα", 225 | "help?":"βοήθεια;", 226 | "contents?":"περιεχόμενα;", 227 | "table of contents":"πίνακας περιεχομένων", 228 | "Table of Contents":"Πίνακας Περιεχομένων", 229 | "restart presentation":"επανεκκίνηση παρουσίασης", 230 | "restart?":"επανεκκίνηση;" 231 | }; 232 | 233 | strings_el[helpText] = 234 | "Πλοηγηθείτε με το κλίκ του ποντικιού, το space, τα βέλη αριστερά/δεξιά, " + 235 | "ή Page Up και Page Down. Χρησιμοποιήστε τα πλήκτρα S και B για να αλλάξετε " + 236 | "το μέγεθος της γραμματοσειράς."; 237 | 238 | var strings_ja = { 239 | "slide":"スライド", 240 | "help?":"ヘルプ", 241 | "contents?":"目次", 242 | "table of contents":"目次を表示", 243 | "Table of Contents":"目次", 244 | "restart presentation":"最初から再生", 245 | "restart?":"最初から" 246 | }; 247 | 248 | strings_ja[helpText] = 249 | "マウス左クリック ・ スペース ・ 左右キー " + 250 | "または Page Up ・ Page Downで操作, S ・ Bでフォントサイズ変更"; 251 | 252 | var strings_zh = { 253 | "slide":"幻灯片", 254 | "help?":"帮助?", 255 | "contents?":"内容?", 256 | "table of contents":"目录", 257 | "Table of Contents":"目录", 258 | "restart presentation":"重新启动展示", 259 | "restart?":"重新启动?" 260 | }; 261 | 262 | strings_zh[helpText] = 263 | "用鼠标点击, 空格条, 左右箭头, Pg Up 和 Pg Dn 导航. " + 264 | "用 S, B 改变字体大小."; 265 | 266 | var strings_ru = { 267 | "slide":"слайд", 268 | "help?":"помощь?", 269 | "contents?":"содержание?", 270 | "table of contents":"оглавление", 271 | "Table of Contents":"Оглавление", 272 | "restart presentation":"перезапустить презентацию", 273 | "restart?":"перезапуск?" 274 | }; 275 | 276 | strings_ru[helpText] = 277 | "Перемещайтесь кликая мышкой, используя клавишу пробел, стрелки" + 278 | "влево/вправо или Pg Up и Pg Dn. Клавиши S и B меняют размер шрифта."; 279 | 280 | 281 | // each such language array is declared in the localize array 282 | // used indirectly as in help.innerHTML = "help".localize(); 283 | var localize = { 284 | "es":strings_es, 285 | "ca":strings_ca, 286 | "nl":strings_nl, 287 | "de":strings_de, 288 | "pl":strings_pl, 289 | "fr":strings_fr, 290 | "hu":strings_hu, 291 | "it":strings_it, 292 | "el":strings_el, 293 | "jp":strings_ja, 294 | "zh":strings_zh, 295 | "ru":strings_ru 296 | }; 297 | 298 | /* general initialization */ 299 | function startup() 300 | { 301 | if (slidy_started) 302 | { 303 | alert("already started"); 304 | return; 305 | } 306 | slidy_started = true; 307 | 308 | // find human language from html element 309 | // for use in localizing strings 310 | lang = document.body.parentNode.getAttribute("lang"); 311 | 312 | if (!lang) 313 | lang = document.body.parentNode.getAttribute("xml:lang"); 314 | 315 | if (!lang) 316 | lang = "en"; 317 | 318 | document.body.style.visibility = "visible"; 319 | title = document.title; 320 | toolbar = addToolbar(); 321 | wrapImplicitSlides(); 322 | slides = collectSlides(); 323 | notes = collectNotes(); 324 | objects = document.body.getElementsByTagName("object"); 325 | backgrounds = collectBackgrounds(); 326 | patchAnchors(); 327 | 328 | slidenum = findSlideNumber(location.href); 329 | window.offscreenbuffering = true; 330 | sizeAdjustment = findSizeAdjust(); 331 | hideImageToolbar(); // suppress IE image toolbar popup 332 | initOutliner(); // activate fold/unfold support 333 | 334 | if (slides.length > 0) 335 | { 336 | var slide = slides[slidenum]; 337 | slide.style.position = "absolute"; 338 | 339 | if (slidenum > 0) 340 | { 341 | setVisibilityAllIncremental("visible"); 342 | lastShown = previousIncrementalItem(null); 343 | setEosStatus(true); 344 | } 345 | else 346 | { 347 | lastShown = null; 348 | setVisibilityAllIncremental("hidden"); 349 | setEosStatus(!nextIncrementalItem(lastShown)); 350 | } 351 | 352 | setLocation(); 353 | } 354 | 355 | toc = tableOfContents(); 356 | hideTableOfContents(); 357 | 358 | // bind event handlers 359 | document.onclick = mouseButtonClick; 360 | document.onmouseup = mouseButtonUp; 361 | document.onkeydown = keyDown; 362 | window.onresize = resized; 363 | window.onscroll = scrolled; 364 | window.onunload = unloaded; 365 | singleSlideView(); 366 | 367 | 368 | setLocation(); 369 | resized(); 370 | 371 | if (ie7) 372 | setTimeout("ieHack()", 100); 373 | 374 | showToolbar(); 375 | setInterval("checkLocation()", 200); // for back button detection 376 | } 377 | 378 | // add localize method to all strings for use 379 | // as in help.innerHTML = "help".localize(); 380 | String.prototype.localize = function() 381 | { 382 | if (this == "") 383 | return this; 384 | 385 | // try full language code, e.g. en-US 386 | var s, lookup = localize[lang]; 387 | 388 | if (lookup) 389 | { 390 | s = lookup[this]; 391 | 392 | if (s) 393 | return s; 394 | } 395 | 396 | // try en if undefined for en-US 397 | var lg = lang.split("-"); 398 | 399 | if (lg.length > 1) 400 | { 401 | lookup = localize[lg[0]]; 402 | 403 | if (lookup) 404 | { 405 | s = lookup[this]; 406 | 407 | if (s) 408 | return s; 409 | } 410 | } 411 | 412 | // otherwise string as is 413 | return this; 414 | } 415 | 416 | // suppress IE's image toolbar pop up 417 | function hideImageToolbar() 418 | { 419 | if (!ns_pos) 420 | { 421 | var images = document.getElementsByTagName("IMG"); 422 | 423 | for (var i = 0; i < images.length; ++i) 424 | images[i].setAttribute("galleryimg", "no"); 425 | } 426 | } 427 | 428 | // hack to persuade IE to compute correct document height 429 | // as needed for simulating fixed positioning of toolbar 430 | function ieHack() 431 | { 432 | window.resizeBy(0,-1); 433 | window.resizeBy(0, 1); 434 | } 435 | 436 | function unloaded(e) 437 | { 438 | //alert("unloaded"); 439 | } 440 | 441 | // Firefox reload SVG bug work around 442 | function reload(e) 443 | { 444 | if (!e) 445 | var e = window.event; 446 | 447 | hideBackgrounds(); 448 | setTimeout("document.reload();", 100); 449 | 450 | stopPropagation(e); 451 | e.cancel = true; 452 | e.returnValue = false; 453 | 454 | return false; 455 | } 456 | 457 | // Safari and Konqueror don't yet support getComputedStyle() 458 | // and they always reload page when location.href is updated 459 | function isKHTML() 460 | { 461 | var agent = navigator.userAgent; 462 | return (agent.indexOf("KHTML") >= 0 ? true : false); 463 | } 464 | 465 | function resized() 466 | { 467 | var width = 0; 468 | 469 | if ( typeof( window.innerWidth ) == 'number' ) 470 | width = window.innerWidth; // Non IE browser 471 | else if (document.documentElement && document.documentElement.clientWidth) 472 | width = document.documentElement.clientWidth; // IE6 473 | else if (document.body && document.body.clientWidth) 474 | width = document.body.clientWidth; // IE4 475 | 476 | var height = 0; 477 | 478 | if ( typeof( window.innerHeight ) == 'number' ) 479 | height = window.innerHeight; // Non IE browser 480 | else if (document.documentElement && document.documentElement.clientHeight) 481 | height = document.documentElement.clientHeight; // IE6 482 | else if (document.body && document.body.clientHeight) 483 | height = document.body.clientHeight; // IE4 484 | 485 | if (height && (width/height > 1.05*1024/768)) 486 | { 487 | width = height * 1024.0/768; 488 | } 489 | 490 | // IE fires onresize even when only font size is changed! 491 | // so we do a check to avoid blocking < and > actions 492 | if (width != lastWidth || height != lastHeight) 493 | { 494 | if (width >= 1100) 495 | sizeIndex = 5; // 4 496 | else if (width >= 1000) 497 | sizeIndex = 4; // 3 498 | else if (width >= 800) 499 | sizeIndex = 3; // 2 500 | else if (width >= 600) 501 | sizeIndex = 2; // 1 502 | else if (width) 503 | sizeIndex = 0; 504 | 505 | // add in font size adjustment from meta element e.g. 506 | // 507 | // useful when slides have too much content ;-) 508 | 509 | if (0 <= sizeIndex + sizeAdjustment && 510 | sizeIndex + sizeAdjustment < sizes.length) 511 | sizeIndex = sizeIndex + sizeAdjustment; 512 | 513 | // enables cross browser use of relative width/height 514 | // on object elements for use with SVG and Flash media 515 | adjustObjectDimensions(width, height); 516 | 517 | document.body.style.fontSize = sizes[sizeIndex]; 518 | 519 | lastWidth = width; 520 | lastHeight = height; 521 | 522 | // force reflow to work around Mozilla bug 523 | //if (ns_pos) 524 | { 525 | var slide = slides[slidenum]; 526 | hideSlide(slide); 527 | showSlide(slide); 528 | } 529 | 530 | // force correct positioning of toolbar 531 | refreshToolbar(200); 532 | } 533 | } 534 | 535 | function scrolled() 536 | { 537 | if (toolbar && !ns_pos && !ie7) 538 | { 539 | hackoffset = scrollXOffset(); 540 | // hide toolbar 541 | toolbar.style.display = "none"; 542 | 543 | // make it reappear later 544 | if (scrollhack == 0 && !viewAll) 545 | { 546 | setTimeout(showToolbar, 1000); 547 | scrollhack = 1; 548 | } 549 | } 550 | } 551 | 552 | // used to ensure IE refreshes toolbar in correct position 553 | function refreshToolbar(interval) 554 | { 555 | if (!ns_pos && !ie7) 556 | { 557 | hideToolbar(); 558 | setTimeout(showToolbar, interval); 559 | } 560 | } 561 | 562 | // restores toolbar after short delay 563 | function showToolbar() 564 | { 565 | if (wantToolbar) 566 | { 567 | if (!ns_pos) 568 | { 569 | // adjust position to allow for scrolling 570 | var xoffset = scrollXOffset(); 571 | toolbar.style.left = xoffset; 572 | toolbar.style.right = xoffset; 573 | 574 | // determine vertical scroll offset 575 | //var yoffset = scrollYOffset(); 576 | 577 | // bottom is doc height - window height - scroll offset 578 | //var bottom = documentHeight() - lastHeight - yoffset 579 | 580 | //if (yoffset > 0 || documentHeight() > lastHeight) 581 | // bottom += 16; // allow for height of scrollbar 582 | 583 | toolbar.style.bottom = 0; //bottom; 584 | } 585 | 586 | toolbar.style.display = "block"; 587 | toolbar.style.visibility = "visible"; 588 | } 589 | 590 | scrollhack = 0; 591 | 592 | 593 | // set the keyboard focus to the help link on the 594 | // toolbar to ensure that document has the focus 595 | // IE doesn't always work with window.focus() 596 | // and this hack has benefit of Enter for help 597 | 598 | try 599 | { 600 | if (!opera) 601 | helpAnchor.focus(); 602 | } 603 | catch (e) 604 | { 605 | } 606 | } 607 | 608 | function hideToolbar() 609 | { 610 | toolbar.style.display = "none"; 611 | toolbar.style.visibility = "hidden"; 612 | window.focus(); 613 | } 614 | 615 | // invoked via F key 616 | function toggleToolbar() 617 | { 618 | if (!viewAll) 619 | { 620 | if (toolbar.style.display == "none") 621 | { 622 | toolbar.style.display = "block"; 623 | toolbar.style.visibility = "visible"; 624 | wantToolbar = 1; 625 | } 626 | else 627 | { 628 | toolbar.style.display = "none"; 629 | toolbar.style.visibility = "hidden"; 630 | wantToolbar = 0; 631 | } 632 | } 633 | } 634 | 635 | function scrollXOffset() 636 | { 637 | if (window.pageXOffset) 638 | return self.pageXOffset; 639 | 640 | if (document.documentElement && 641 | document.documentElement.scrollLeft) 642 | return document.documentElement.scrollLeft; 643 | 644 | if (document.body) 645 | return document.body.scrollLeft; 646 | 647 | return 0; 648 | } 649 | 650 | 651 | function scrollYOffset() 652 | { 653 | if (window.pageYOffset) 654 | return self.pageYOffset; 655 | 656 | if (document.documentElement && 657 | document.documentElement.scrollTop) 658 | return document.documentElement.scrollTop; 659 | 660 | if (document.body) 661 | return document.body.scrollTop; 662 | 663 | return 0; 664 | } 665 | 666 | // looking for a way to determine height of slide content 667 | // the slide itself is set to the height of the window 668 | function optimizeFontSize() 669 | { 670 | var slide = slides[slidenum]; 671 | 672 | //var dh = documentHeight(); //getDocHeight(document); 673 | var dh = slide.scrollHeight; 674 | var wh = getWindowHeight(); 675 | var u = 100 * dh / wh; 676 | 677 | alert("window utilization = " + u + "% (doc " 678 | + dh + " win " + wh + ")"); 679 | } 680 | 681 | function getDocHeight(doc) // from document object 682 | { 683 | if (!doc) 684 | doc = document; 685 | 686 | if (doc && doc.body && doc.body.offsetHeight) 687 | return doc.body.offsetHeight; // ns/gecko syntax 688 | 689 | if (doc && doc.body && doc.body.scrollHeight) 690 | return doc.body.scrollHeight; 691 | 692 | alert("couldn't determine document height"); 693 | } 694 | 695 | function getWindowHeight() 696 | { 697 | if ( typeof( window.innerHeight ) == 'number' ) 698 | return window.innerHeight; // Non IE browser 699 | 700 | if (document.documentElement && document.documentElement.clientHeight) 701 | return document.documentElement.clientHeight; // IE6 702 | 703 | if (document.body && document.body.clientHeight) 704 | return document.body.clientHeight; // IE4 705 | } 706 | 707 | 708 | 709 | function documentHeight() 710 | { 711 | var sh, oh; 712 | 713 | sh = document.body.scrollHeight; 714 | oh = document.body.offsetHeight; 715 | 716 | if (sh && oh) 717 | { 718 | return (sh > oh ? sh : oh); 719 | } 720 | 721 | // no idea! 722 | return 0; 723 | } 724 | 725 | function smaller() 726 | { 727 | if (sizeIndex > 0) 728 | { 729 | --sizeIndex; 730 | } 731 | 732 | toolbar.style.display = "none"; 733 | document.body.style.fontSize = sizes[sizeIndex]; 734 | var slide = slides[slidenum]; 735 | hideSlide(slide); 736 | showSlide(slide); 737 | setTimeout(showToolbar, 300); 738 | } 739 | 740 | function bigger() 741 | { 742 | if (sizeIndex < sizes.length - 1) 743 | { 744 | ++sizeIndex; 745 | } 746 | 747 | toolbar.style.display = "none"; 748 | document.body.style.fontSize = sizes[sizeIndex]; 749 | var slide = slides[slidenum]; 750 | hideSlide(slide); 751 | showSlide(slide); 752 | setTimeout(showToolbar, 300); 753 | } 754 | 755 | // enables cross browser use of relative width/height 756 | // on object elements for use with SVG and Flash media 757 | // with thanks to Ivan Herman for the suggestion 758 | function adjustObjectDimensions(width, height) 759 | { 760 | for( var i = 0; i < objects.length; i++ ) 761 | { 762 | var obj = objects[i]; 763 | var mimeType = obj.getAttribute("type"); 764 | 765 | if (mimeType == "image/svg+xml" || mimeType == "application/x-shockwave-flash") 766 | { 767 | if ( !obj.initialWidth ) 768 | obj.initialWidth = obj.getAttribute("width"); 769 | 770 | if ( !obj.initialHeight ) 771 | obj.initialHeight = obj.getAttribute("height"); 772 | 773 | if ( obj.initialWidth && obj.initialWidth.charAt(obj.initialWidth.length-1) == "%" ) 774 | { 775 | var w = parseInt(obj.initialWidth.slice(0, obj.initialWidth.length-1)); 776 | var newW = width * (w/100.0); 777 | obj.setAttribute("width",newW); 778 | } 779 | 780 | if ( obj.initialHeight && obj.initialHeight.charAt(obj.initialHeight.length-1) == "%" ) 781 | { 782 | var h = parseInt(obj.initialHeight.slice(0, obj.initialHeight.length-1)); 783 | var newH = height * (h/100.0); 784 | obj.setAttribute("height", newH); 785 | } 786 | } 787 | } 788 | } 789 | 790 | function cancel(event) 791 | { 792 | if (event) 793 | { 794 | event.cancel = true; 795 | event.returnValue = false; 796 | 797 | if (event.preventDefault) 798 | event.preventDefault(); 799 | } 800 | 801 | return false; 802 | } 803 | 804 | // See e.g. http://www.quirksmode.org/js/events/keys.html for keycodes 805 | function keyDown(event) 806 | { 807 | var key; 808 | 809 | if (!event) 810 | var event = window.event; 811 | 812 | // kludge around NS/IE differences 813 | if (window.event) 814 | key = window.event.keyCode; 815 | else if (event.which) 816 | key = event.which; 817 | else 818 | return true; // Yikes! unknown browser 819 | 820 | // ignore event if key value is zero 821 | // as for alt on Opera and Konqueror 822 | if (!key) 823 | return true; 824 | 825 | // check for concurrent control/command/alt key 826 | // but are these only present on mouse events? 827 | 828 | if (event.ctrlKey || event.altKey || event.metaKey) 829 | return true; 830 | 831 | // dismiss table of contents if visible 832 | if (isShownToc() && key != 9 && key != 16 && key != 38 && key != 40) 833 | { 834 | hideTableOfContents(); 835 | 836 | if (key == 27 || key == 84 || key == 67) 837 | return cancel(event); 838 | } 839 | 840 | if (key == 34) // Page Down 841 | { 842 | if (viewAll) 843 | return true; 844 | 845 | nextSlide(false); 846 | return cancel(event); 847 | } 848 | else if (key == 33) // Page Up 849 | { 850 | if (viewAll) 851 | return true; 852 | 853 | previousSlide(false); 854 | return cancel(event); 855 | } 856 | else if (key == 32) // space bar 857 | { 858 | nextSlide(true); 859 | return cancel(event); 860 | } 861 | else if (key == 37) // Left arrow 862 | { 863 | previousSlide(!event.shiftKey); 864 | return cancel(event); 865 | } 866 | else if (key == 36) // Home 867 | { 868 | firstSlide(); 869 | return cancel(event); 870 | } 871 | else if (key == 35) // End 872 | { 873 | lastSlide(); 874 | return cancel(event); 875 | } 876 | else if (key == 39) // Right arrow 877 | { 878 | nextSlide(!event.shiftKey); 879 | return cancel(event); 880 | } 881 | else if (key == 13) // Enter 882 | { 883 | if (outline) 884 | { 885 | if (outline.visible) 886 | fold(outline); 887 | else 888 | unfold(outline); 889 | 890 | return cancel(event); 891 | } 892 | } 893 | else if (key == 188) // < for smaller fonts 894 | { 895 | smaller(); 896 | return cancel(event); 897 | } 898 | else if (key == 190) // > for larger fonts 899 | { 900 | bigger(); 901 | return cancel(event); 902 | } 903 | else if (key == 189 || key == 109) // - for smaller fonts 904 | { 905 | smaller(); 906 | return cancel(event); 907 | } 908 | else if (key == 187 || key == 191 || key == 107) // = + for larger fonts 909 | { 910 | bigger(); 911 | return cancel(event); 912 | } 913 | else if (key == 83) // S for smaller fonts 914 | { 915 | smaller(); 916 | return cancel(event); 917 | } 918 | else if (key == 66) // B for larger fonts 919 | { 920 | bigger(); 921 | return cancel(event); 922 | } 923 | else if (key == 90) // Z for last slide 924 | { 925 | lastSlide(); 926 | return cancel(event); 927 | } 928 | else if (key == 70) // F for toggle toolbar 929 | { 930 | toggleToolbar(); 931 | return cancel(event); 932 | } 933 | else if (key == 65) // A for toggle view single/all slides 934 | { 935 | toggleView(); 936 | return cancel(event); 937 | } 938 | else if (key == 75) // toggle action of left click for next page 939 | { 940 | mouseClickEnabled = !mouseClickEnabled; 941 | alert((mouseClickEnabled ? "enabled" : "disabled") + " mouse click advance"); 942 | return cancel(event); 943 | } 944 | else if (key == 84 || key == 67) // T or C for table of contents 945 | { 946 | if (toc) 947 | showTableOfContents(); 948 | 949 | return cancel(event); 950 | } 951 | else if (key == 72) // H for help 952 | { 953 | window.location = helpPage; 954 | return cancel(event); 955 | } 956 | 957 | //else if (key == 93) // Windows menu key 958 | //alert("lastShown is " + lastShown); 959 | //else alert("key code is "+ key); 960 | 961 | 962 | return true; 963 | } 964 | 965 | // make note of length of selected text 966 | // as this evaluates to zero in click event 967 | function mouseButtonUp(e) 968 | { 969 | selectedTextLen = getSelectedText().length; 970 | } 971 | 972 | // right mouse button click is reserved for context menus 973 | // it is more reliable to detect rightclick than leftclick 974 | function mouseButtonClick(e) 975 | { 976 | var rightclick = false; 977 | var leftclick = false; 978 | var middleclick = false; 979 | var target; 980 | 981 | if (!e) 982 | var e = window.event; 983 | 984 | if (e.target) 985 | target = e.target; 986 | else if (e.srcElement) 987 | target = e.srcElement; 988 | 989 | // work around Safari bug 990 | if (target.nodeType == 3) 991 | target = target.parentNode; 992 | 993 | if (e.which) // all browsers except IE 994 | { 995 | leftclick = (e.which == 1); 996 | middleclick = (e.which == 2); 997 | rightclick = (e.which == 3); 998 | } 999 | else if (e.button) 1000 | { 1001 | // Konqueror gives 1 for left, 4 for middle 1002 | // IE6 gives 0 for left and not 1 as I expected 1003 | 1004 | if (e.button == 4) 1005 | middleclick = true; 1006 | 1007 | // all browsers agree on 2 for right button 1008 | rightclick = (e.button == 2); 1009 | } 1010 | else leftclick = true; 1011 | 1012 | //alert("selected text length = "+selectedTextLen); 1013 | 1014 | if (selectedTextLen > 0) 1015 | { 1016 | stopPropagation(e); 1017 | e.cancel = true; 1018 | e.returnValue = false; 1019 | return false; 1020 | } 1021 | 1022 | // dismiss table of contents 1023 | hideTableOfContents(); 1024 | 1025 | // check if target is something that probably want's clicks 1026 | // e.g. embed, object, input, textarea, select, option 1027 | 1028 | if (mouseClickEnabled && leftclick && 1029 | target.nodeName != "EMBED" && 1030 | target.nodeName != "OBJECT" && 1031 | target.nodeName != "VIDEO" && 1032 | target.nodeName != "INPUT" && 1033 | target.nodeName != "TEXTAREA" && 1034 | target.nodeName != "SELECT" && 1035 | target.nodeName != "OPTION") 1036 | { 1037 | nextSlide(true); 1038 | stopPropagation(e); 1039 | e.cancel = true; 1040 | e.returnValue = false; 1041 | } 1042 | } 1043 | 1044 | function previousSlide(incremental) 1045 | { 1046 | if (!viewAll) 1047 | { 1048 | var slide; 1049 | 1050 | if ((incremental || slidenum == 0) && lastShown != null) 1051 | { 1052 | lastShown = hidePreviousItem(lastShown); 1053 | setEosStatus(false); 1054 | } 1055 | else if (slidenum > 0) 1056 | { 1057 | slide = slides[slidenum]; 1058 | hideSlide(slide); 1059 | 1060 | slidenum = slidenum - 1; 1061 | slide = slides[slidenum]; 1062 | setVisibilityAllIncremental("visible"); 1063 | lastShown = previousIncrementalItem(null); 1064 | setEosStatus(true); 1065 | showSlide(slide); 1066 | } 1067 | 1068 | setLocation(); 1069 | 1070 | if (!ns_pos) 1071 | refreshToolbar(200); 1072 | } 1073 | } 1074 | 1075 | function nextSlide(incremental) 1076 | { 1077 | if (!viewAll) 1078 | { 1079 | var slide, last = lastShown; 1080 | 1081 | if (incremental || slidenum == slides.length - 1) 1082 | lastShown = revealNextItem(lastShown); 1083 | 1084 | if ((!incremental || lastShown == null) && slidenum < slides.length - 1) 1085 | { 1086 | slide = slides[slidenum]; 1087 | hideSlide(slide); 1088 | 1089 | slidenum = slidenum + 1; 1090 | slide = slides[slidenum]; 1091 | lastShown = null; 1092 | setVisibilityAllIncremental("hidden"); 1093 | showSlide(slide); 1094 | } 1095 | else if (!lastShown) 1096 | { 1097 | if (last && incremental) 1098 | lastShown = last; 1099 | } 1100 | 1101 | setLocation(); 1102 | 1103 | setEosStatus(!nextIncrementalItem(lastShown)); 1104 | 1105 | if (!ns_pos) 1106 | refreshToolbar(200); 1107 | } 1108 | } 1109 | 1110 | // to first slide with nothing revealed 1111 | // i.e. state at start of presentation 1112 | function firstSlide() 1113 | { 1114 | if (!viewAll) 1115 | { 1116 | var slide; 1117 | 1118 | if (slidenum != 0) 1119 | { 1120 | slide = slides[slidenum]; 1121 | hideSlide(slide); 1122 | 1123 | slidenum = 0; 1124 | slide = slides[slidenum]; 1125 | lastShown = null; 1126 | setVisibilityAllIncremental("hidden"); 1127 | showSlide(slide); 1128 | } 1129 | 1130 | setEosStatus(!nextIncrementalItem(lastShown)); 1131 | setLocation(); 1132 | } 1133 | } 1134 | 1135 | 1136 | // to last slide with everything revealed 1137 | // i.e. state at end of presentation 1138 | function lastSlide() 1139 | { 1140 | if (!viewAll) 1141 | { 1142 | var slide; 1143 | 1144 | lastShown = null; //revealNextItem(lastShown); 1145 | 1146 | if (lastShown == null && slidenum < slides.length - 1) 1147 | { 1148 | slide = slides[slidenum]; 1149 | hideSlide(slide); 1150 | slidenum = slides.length - 1; 1151 | slide = slides[slidenum]; 1152 | setVisibilityAllIncremental("visible"); 1153 | lastShown = previousIncrementalItem(null); 1154 | 1155 | showSlide(slide); 1156 | } 1157 | else 1158 | { 1159 | setVisibilityAllIncremental("visible"); 1160 | lastShown = previousIncrementalItem(null); 1161 | } 1162 | 1163 | setEosStatus(true); 1164 | setLocation(); 1165 | } 1166 | } 1167 | 1168 | // first slide is 0 1169 | function gotoSlide(num) 1170 | { 1171 | //alert("going to slide " + (num+1)); 1172 | var slide = slides[slidenum]; 1173 | hideSlide(slide); 1174 | slidenum = num; 1175 | slide = slides[slidenum]; 1176 | lastShown = null; 1177 | setVisibilityAllIncremental("hidden"); 1178 | setEosStatus(!nextIncrementalItem(lastShown)); 1179 | document.title = title + " (" + (slidenum+1) + ")"; 1180 | showSlide(slide); 1181 | showSlideNumber(); 1182 | } 1183 | 1184 | function setEosStatus(state) 1185 | { 1186 | if (eos) 1187 | eos.style.color = (state ? "rgb(240,240,240)" : "red"); 1188 | } 1189 | 1190 | function showSlide(slide) 1191 | { 1192 | syncBackground(slide); 1193 | window.scrollTo(0,0); 1194 | slide.style.visibility = "visible"; 1195 | slide.style.display = "block"; 1196 | } 1197 | 1198 | function hideSlide(slide) 1199 | { 1200 | slide.style.visibility = "hidden"; 1201 | slide.style.display = "none"; 1202 | } 1203 | 1204 | function beforePrint() 1205 | { 1206 | showAllSlides(); 1207 | hideToolbar(); 1208 | } 1209 | 1210 | function afterPrint() 1211 | { 1212 | if (!viewAll) 1213 | { 1214 | singleSlideView(); 1215 | showToolbar(); 1216 | } 1217 | } 1218 | 1219 | function printSlides() 1220 | { 1221 | beforePrint(); 1222 | window.print(); 1223 | afterPrint(); 1224 | } 1225 | 1226 | function toggleView() 1227 | { 1228 | if (viewAll) 1229 | { 1230 | singleSlideView(); 1231 | showToolbar(); 1232 | viewAll = 0; 1233 | } 1234 | else 1235 | { 1236 | showAllSlides(); 1237 | hideToolbar(); 1238 | viewAll = 1; 1239 | } 1240 | } 1241 | 1242 | // prepare for printing 1243 | function showAllSlides() 1244 | { 1245 | var slide; 1246 | 1247 | for (var i = 0; i < slides.length; ++i) 1248 | { 1249 | slide = slides[i]; 1250 | 1251 | slide.style.position = "relative"; 1252 | slide.style.borderTopStyle = "solid"; 1253 | slide.style.borderTopWidth = "thin"; 1254 | slide.style.borderTopColor = "black"; 1255 | 1256 | try { 1257 | if (i == 0) 1258 | slide.style.pageBreakBefore = "avoid"; 1259 | else 1260 | slide.style.pageBreakBefore = "always"; 1261 | } 1262 | catch (e) 1263 | { 1264 | //do nothing 1265 | } 1266 | 1267 | setVisibilityAllIncremental("visible"); 1268 | showSlide(slide); 1269 | } 1270 | 1271 | var note; 1272 | 1273 | for (var i = 0; i < notes.length; ++i) 1274 | { 1275 | showSlide(notes[i]); 1276 | } 1277 | 1278 | // no easy way to render background under each slide 1279 | // without duplicating the background divs for each slide 1280 | // therefore hide backgrounds to avoid messing up slides 1281 | hideBackgrounds(); 1282 | } 1283 | 1284 | // restore after printing 1285 | function singleSlideView() 1286 | { 1287 | var slide; 1288 | 1289 | for (var i = 0; i < slides.length; ++i) 1290 | { 1291 | slide = slides[i]; 1292 | 1293 | slide.style.position = "absolute"; 1294 | 1295 | if (i == slidenum) 1296 | { 1297 | slide.style.borderStyle = "none"; 1298 | showSlide(slide); 1299 | } 1300 | else 1301 | { 1302 | slide.style.borderStyle = "none"; 1303 | hideSlide(slide); 1304 | } 1305 | } 1306 | 1307 | setVisibilityAllIncremental("visible"); 1308 | lastShown = previousIncrementalItem(null); 1309 | 1310 | var note; 1311 | 1312 | for (var i = 0; i < notes.length; ++i) 1313 | { 1314 | hideSlide(notes[i]); 1315 | } 1316 | } 1317 | 1318 | // the string str is a whitespace separated list of tokens 1319 | // test if str contains a particular token, e.g. "slide" 1320 | function hasToken(str, token) 1321 | { 1322 | if (str) 1323 | { 1324 | // define pattern as regular expression 1325 | var pattern = /\w+/g; 1326 | 1327 | // check for matches 1328 | // place result in array 1329 | var result = str.match(pattern); 1330 | 1331 | // now check if desired token is present 1332 | for (var i = 0; i < result.length; i++) 1333 | { 1334 | if (result[i] == token) 1335 | return true; 1336 | } 1337 | } 1338 | 1339 | return false; 1340 | } 1341 | 1342 | function getClassList(element) 1343 | { 1344 | if (typeof element.className != 'undefined') 1345 | return element.className; 1346 | 1347 | var clsname = (ns_pos||ie8) ? "class" : "className"; 1348 | return element.getAttribute(clsname); 1349 | } 1350 | 1351 | function hasClass(element, name) 1352 | { 1353 | var regexp = new RegExp("(^| )" + name + "\W*"); 1354 | 1355 | if (typeof element.className != 'undefined') 1356 | return regexp.test(element.className); 1357 | 1358 | var clsname = (ns_pos||ie8) ? "class" : "className"; 1359 | return regexp.test(element.getAttribute(clsname)); 1360 | } 1361 | 1362 | function removeClass(element, name) 1363 | { 1364 | var regexp = new RegExp("(^| )" + name + "\W*"); 1365 | var clsval = ""; 1366 | 1367 | if (typeof element.className != 'undefined') 1368 | { 1369 | clsval = element.className; 1370 | 1371 | if (clsval) 1372 | { 1373 | clsval = clsval.replace(regexp, ""); 1374 | element.className = clsval; 1375 | } 1376 | } 1377 | else 1378 | { 1379 | var clsname = (ns_pos||ie8) ? "class" : "className"; 1380 | clsval = element.getAttribute(clsname); 1381 | 1382 | if (clsval) 1383 | { 1384 | clsval = clsval.replace(regexp, ""); 1385 | element.setAttribute(clsname, clsval); 1386 | } 1387 | } 1388 | } 1389 | 1390 | function addClass(element, name) 1391 | { 1392 | if (!hasClass(element, name)) 1393 | { 1394 | if (typeof element.className != 'undefined') 1395 | element.className += " " + name; 1396 | else 1397 | { 1398 | var clsname = (ns_pos||ie8) ? "class" : "className"; 1399 | var clsval = element.getAttribute(clsname); 1400 | clsval = clsval ? clsval + " " + name : name; 1401 | element.setAttribute(clsname, clsval); 1402 | } 1403 | } 1404 | } 1405 | 1406 | // wysiwyg editors make it hard to use div elements 1407 | // e.g. amaya loses the div when you copy and paste 1408 | // this function wraps div elements around implicit 1409 | // slides which start with an h1 element and continue 1410 | // up to the next heading or div element 1411 | function wrapImplicitSlides() 1412 | { 1413 | var i, heading, node, next, div; 1414 | var headings = document.getElementsByTagName("h1"); 1415 | 1416 | if (!headings) 1417 | return; 1418 | 1419 | for (i = 0; i < headings.length; ++i) 1420 | { 1421 | heading = headings[i]; 1422 | 1423 | if (heading.parentNode != document.body) 1424 | continue; 1425 | 1426 | node = heading.nextSibling; 1427 | 1428 | div = document.createElement("div"); 1429 | addClass(div, "slide"); 1430 | document.body.replaceChild(div, heading); 1431 | div.appendChild(heading); 1432 | 1433 | while (node) 1434 | { 1435 | if (node.nodeType == 1 && // an element 1436 | (node.nodeName == "H1" || 1437 | node.nodeName == "h1" || 1438 | node.nodeName == "DIV" || 1439 | node.nodeName == "div")) 1440 | break; 1441 | 1442 | next = node.nextSibling; 1443 | node = document.body.removeChild(node); 1444 | div.appendChild(node); 1445 | node = next; 1446 | } 1447 | } 1448 | } 1449 | 1450 | // return new array of all slides 1451 | function collectSlides() 1452 | { 1453 | var slides = new Array(); 1454 | var divs = document.body.getElementsByTagName("div"); 1455 | 1456 | for (var i = 0; i < divs.length; ++i) 1457 | { 1458 | div = divs.item(i); 1459 | 1460 | if (hasClass(div, "slide")) 1461 | { 1462 | // add slide to collection 1463 | slides[slides.length] = div; 1464 | 1465 | // hide each slide as it is found 1466 | div.style.display = "none"; 1467 | div.style.visibility = "hidden"; 1468 | 1469 | // add dummy
at end for scrolling hack 1470 | var node1 = document.createElement("br"); 1471 | div.appendChild(node1); 1472 | var node2 = document.createElement("br"); 1473 | div.appendChild(node2); 1474 | } 1475 | else if (hasClass(div, "background")) 1476 | { // work around for Firefox SVG reload bug 1477 | // which otherwise replaces 1st SVG graphic with 2nd 1478 | div.style.display = "block"; 1479 | } 1480 | } 1481 | 1482 | return slides; 1483 | } 1484 | 1485 | // return new array of all
1486 | function collectNotes() 1487 | { 1488 | var notes = new Array(); 1489 | var divs = document.body.getElementsByTagName("div"); 1490 | 1491 | for (var i = 0; i < divs.length; ++i) 1492 | { 1493 | div = divs.item(i); 1494 | 1495 | if (hasClass(div, "handout")) 1496 | { 1497 | // add slide to collection 1498 | notes[notes.length] = div; 1499 | 1500 | // hide handout notes as they are found 1501 | div.style.display = "none"; 1502 | div.style.visibility = "hidden"; 1503 | } 1504 | } 1505 | 1506 | return notes; 1507 | } 1508 | 1509 | // return new array of all
1510 | // including named backgrounds e.g. class="background titlepage" 1511 | function collectBackgrounds() 1512 | { 1513 | var backgrounds = new Array(); 1514 | var divs = document.body.getElementsByTagName("div"); 1515 | 1516 | for (var i = 0; i < divs.length; ++i) 1517 | { 1518 | div = divs.item(i); 1519 | 1520 | if (hasClass(div, "background")) 1521 | { 1522 | // add slide to collection 1523 | backgrounds[backgrounds.length] = div; 1524 | 1525 | // hide named backgrounds as they are found 1526 | // e.g. class="background epilog" 1527 | if (getClassList(div) != "background") 1528 | { 1529 | div.style.display = "none"; 1530 | div.style.visibility = "hidden"; 1531 | } 1532 | } 1533 | } 1534 | 1535 | return backgrounds; 1536 | } 1537 | 1538 | // show just the backgrounds pertinent to this slide 1539 | function syncBackground(slide) 1540 | { 1541 | var background; 1542 | var bgColor; 1543 | 1544 | if (slide.currentStyle) 1545 | bgColor = slide.currentStyle["backgroundColor"]; 1546 | else if (document.defaultView) 1547 | { 1548 | var styles = document.defaultView.getComputedStyle(slide,null); 1549 | 1550 | if (styles) 1551 | bgColor = styles.getPropertyValue("background-color"); 1552 | else // broken implementation probably due Safari or Konqueror 1553 | { 1554 | //alert("defective implementation of getComputedStyle()"); 1555 | bgColor = "transparent"; 1556 | } 1557 | } 1558 | else 1559 | bgColor == "transparent"; 1560 | 1561 | if (bgColor == "transparent") 1562 | { 1563 | var slideClass = getClassList(slide); 1564 | 1565 | for (var i = 0; i < backgrounds.length; i++) 1566 | { 1567 | background = backgrounds[i]; 1568 | 1569 | var bgClass = getClassList(background); 1570 | 1571 | if (matchingBackground(slideClass, bgClass)) 1572 | { 1573 | background.style.display = "block"; 1574 | background.style.visibility = "visible"; 1575 | } 1576 | else 1577 | { 1578 | background.style.display = "none"; 1579 | background.style.visibility = "hidden"; 1580 | } 1581 | } 1582 | } 1583 | else // forcibly hide all backgrounds 1584 | hideBackgrounds(); 1585 | } 1586 | 1587 | function hideBackgrounds() 1588 | { 1589 | for (var i = 0; i < backgrounds.length; i++) 1590 | { 1591 | background = backgrounds[i]; 1592 | background.style.display = "none"; 1593 | background.style.visibility = "hidden"; 1594 | } 1595 | } 1596 | 1597 | // compare classes for slide and background 1598 | function matchingBackground(slideClass, bgClass) 1599 | { 1600 | if (bgClass == "background") 1601 | return true; 1602 | 1603 | // define pattern as regular expression 1604 | var pattern = /\w+/g; 1605 | 1606 | // check for matches and place result in array 1607 | var result = slideClass.match(pattern); 1608 | 1609 | // now check if desired name is present for background 1610 | for (var i = 0; i < result.length; i++) 1611 | { 1612 | if (hasToken(bgClass, result[i])) 1613 | return true; 1614 | } 1615 | 1616 | return false; 1617 | } 1618 | 1619 | // left to right traversal of root's content 1620 | function nextNode(root, node) 1621 | { 1622 | if (node == null) 1623 | return root.firstChild; 1624 | 1625 | if (node.firstChild) 1626 | return node.firstChild; 1627 | 1628 | if (node.nextSibling) 1629 | return node.nextSibling; 1630 | 1631 | for (;;) 1632 | { 1633 | node = node.parentNode; 1634 | 1635 | if (!node || node == root) 1636 | break; 1637 | 1638 | if (node && node.nextSibling) 1639 | return node.nextSibling; 1640 | } 1641 | 1642 | return null; 1643 | } 1644 | 1645 | // right to left traversal of root's content 1646 | function previousNode(root, node) 1647 | { 1648 | if (node == null) 1649 | { 1650 | node = root.lastChild; 1651 | 1652 | if (node) 1653 | { 1654 | while (node.lastChild) 1655 | node = node.lastChild; 1656 | } 1657 | 1658 | return node; 1659 | } 1660 | 1661 | if (node.previousSibling) 1662 | { 1663 | node = node.previousSibling; 1664 | 1665 | while (node.lastChild) 1666 | node = node.lastChild; 1667 | 1668 | return node; 1669 | } 1670 | 1671 | if (node.parentNode != root) 1672 | return node.parentNode; 1673 | 1674 | return null; 1675 | } 1676 | 1677 | // HTML elements that can be used with class="incremental" 1678 | // note that you can also put the class on containers like 1679 | // up, ol, dl, and div to make their contents appear 1680 | // incrementally. Upper case is used since this is what 1681 | // browsers report for HTML node names (text/html). 1682 | function incrementalElementList() 1683 | { 1684 | var inclist = new Array(); 1685 | inclist["P"] = true; 1686 | inclist["PRE"] = true; 1687 | inclist["LI"] = true; 1688 | inclist["BLOCKQUOTE"] = true; 1689 | inclist["DT"] = true; 1690 | inclist["DD"] = true; 1691 | inclist["H2"] = true; 1692 | inclist["H3"] = true; 1693 | inclist["H4"] = true; 1694 | inclist["H5"] = true; 1695 | inclist["H6"] = true; 1696 | inclist["SPAN"] = true; 1697 | inclist["ADDRESS"] = true; 1698 | inclist["TABLE"] = true; 1699 | inclist["TR"] = true; 1700 | inclist["TH"] = true; 1701 | inclist["TD"] = true; 1702 | inclist["IMG"] = true; 1703 | inclist["OBJECT"] = true; 1704 | return inclist; 1705 | } 1706 | 1707 | function nextIncrementalItem(node) 1708 | { 1709 | var slide = slides[slidenum]; 1710 | 1711 | for (;;) 1712 | { 1713 | node = nextNode(slide, node); 1714 | 1715 | if (node == null || node.parentNode == null) 1716 | break; 1717 | 1718 | if (node.nodeType == 1) // ELEMENT 1719 | { 1720 | if (node.nodeName == "BR") 1721 | continue; 1722 | 1723 | if (hasClass(node, "incremental") 1724 | && okayForIncremental[node.nodeName]) 1725 | return node; 1726 | 1727 | if (hasClass(node.parentNode, "incremental") 1728 | && !hasClass(node, "non-incremental")) 1729 | return node; 1730 | } 1731 | } 1732 | 1733 | return node; 1734 | } 1735 | 1736 | function previousIncrementalItem(node) 1737 | { 1738 | var slide = slides[slidenum]; 1739 | 1740 | for (;;) 1741 | { 1742 | node = previousNode(slide, node); 1743 | 1744 | if (node == null || node.parentNode == null) 1745 | break; 1746 | 1747 | if (node.nodeType == 1) 1748 | { 1749 | if (node.nodeName == "BR") 1750 | continue; 1751 | 1752 | if (hasClass(node, "incremental") 1753 | && okayForIncremental[node.nodeName]) 1754 | return node; 1755 | 1756 | if (hasClass(node.parentNode, "incremental") 1757 | && !hasClass(node, "non-incremental")) 1758 | return node; 1759 | } 1760 | } 1761 | 1762 | return node; 1763 | } 1764 | 1765 | // set visibility for all elements on current slide with 1766 | // a parent element with attribute class="incremental" 1767 | function setVisibilityAllIncremental(value) 1768 | { 1769 | var node = nextIncrementalItem(null); 1770 | 1771 | while (node) 1772 | { 1773 | node.style.visibility = value; 1774 | node = nextIncrementalItem(node); 1775 | } 1776 | } 1777 | 1778 | // reveal the next hidden item on the slide 1779 | // node is null or the node that was last revealed 1780 | function revealNextItem(node) 1781 | { 1782 | node = nextIncrementalItem(node); 1783 | 1784 | if (node && node.nodeType == 1) // an element 1785 | node.style.visibility = "visible"; 1786 | 1787 | return node; 1788 | } 1789 | 1790 | 1791 | // exact inverse of revealNextItem(node) 1792 | function hidePreviousItem(node) 1793 | { 1794 | if (node && node.nodeType == 1) // an element 1795 | node.style.visibility = "hidden"; 1796 | 1797 | return previousIncrementalItem(node); 1798 | } 1799 | 1800 | 1801 | /* set click handlers on all anchors */ 1802 | function patchAnchors() 1803 | { 1804 | var anchors = document.body.getElementsByTagName("a"); 1805 | 1806 | for (var i = 0; i < anchors.length; ++i) 1807 | { 1808 | anchors[i].onclick = clickedAnchor; 1809 | } 1810 | } 1811 | 1812 | function clickedAnchor(e) 1813 | { 1814 | if (!e) 1815 | var e = window.event; 1816 | 1817 | // compare this.href with location.href 1818 | // for link to another slide in this doc 1819 | 1820 | if (pageAddress(this.href) == pageAddress(location.href)) 1821 | { 1822 | // yes, so find new slide number 1823 | var newslidenum = findSlideNumber(this.href); 1824 | 1825 | if (newslidenum != slidenum) 1826 | { 1827 | slide = slides[slidenum]; 1828 | hideSlide(slide); 1829 | slidenum = newslidenum; 1830 | slide = slides[slidenum]; 1831 | showSlide(slide); 1832 | setLocation(); 1833 | } 1834 | } 1835 | else if (this.target == null) 1836 | location.href = this.href; 1837 | 1838 | this.blur(); 1839 | stopPropagation(e); 1840 | } 1841 | 1842 | function pageAddress(uri) 1843 | { 1844 | var i = uri.indexOf("#"); 1845 | 1846 | if (i < 0) 1847 | i = uri.indexOf("%23"); 1848 | 1849 | // check if anchor is entire page 1850 | 1851 | if (i < 0) 1852 | return uri; // yes 1853 | 1854 | return uri.substr(0, i); 1855 | } 1856 | 1857 | function showSlideNumber() 1858 | { 1859 | slideNumElement.innerHTML = "slide".localize() + " " + 1860 | (slidenum + 1) + "/" + slides.length; 1861 | } 1862 | 1863 | // every 200mS check if the location has been changed as a 1864 | // result of the user activating the Back button/menu item 1865 | // doesn't work for Opera < 9.5 1866 | function checkLocation() 1867 | { 1868 | var hash = location.hash; 1869 | 1870 | if (slidenum > 0 && (hash == "" || hash == "#")) 1871 | gotoSlide(0); 1872 | else if (hash.length > 2 && hash != "#("+(slidenum+1)+")") 1873 | { 1874 | var num = parseInt(location.hash.substr(2)); 1875 | 1876 | if (!isNaN(num)) 1877 | gotoSlide(num-1); 1878 | } 1879 | } 1880 | 1881 | // this doesn't push location onto history stack for IE 1882 | // for which a hidden iframe hack is needed: load page into 1883 | // the iframe with script that set's parent's location.hash 1884 | // but that won't work for standalone use unless we can 1885 | // create the page dynamically via a javascript: URL 1886 | function setLocation() 1887 | { 1888 | var uri = pageAddress(location.href); 1889 | var hash = "#(" + (slidenum+1) + ")"; 1890 | 1891 | if (slidenum >= 0) 1892 | uri = uri + hash; 1893 | 1894 | if (ie && !ie8) 1895 | pushHash(hash); 1896 | 1897 | if (uri != location.href /*&& !khtml */) 1898 | location.href = uri; 1899 | 1900 | if (khtml) 1901 | hash = "(" + (slidenum+1) + ")"; 1902 | 1903 | if (!ie && location.hash != hash && location.hash != "") 1904 | location.hash = hash; 1905 | 1906 | document.title = title + " (" + (slidenum+1) + ")"; 1907 | showSlideNumber(); 1908 | } 1909 | 1910 | // only used for IE6 and IE7 1911 | function onFrameLoaded(hash) 1912 | { 1913 | location.hash = hash; 1914 | var uri = pageAddress(location.href); 1915 | location.href = uri + hash; 1916 | } 1917 | 1918 | // history hack with thanks to Bertrand Le Roy 1919 | function pushHash(hash) 1920 | { 1921 | if (hash == "") hash = "#(1)"; 1922 | window.location.hash = hash; 1923 | var doc = document.getElementById("historyFrame").contentWindow.document; 1924 | doc.open("javascript:''"); 1925 | doc.write("hello mum"); 1927 | doc.close(); 1928 | } 1929 | 1930 | // find current slide based upon location 1931 | // first find target anchor and then look 1932 | // for associated div element enclosing it 1933 | // finally map that to slide number 1934 | function findSlideNumber(uri) 1935 | { 1936 | // first get anchor from page location 1937 | 1938 | var i = uri.indexOf("#"); 1939 | 1940 | // check if anchor is entire page 1941 | 1942 | if (i < 0) 1943 | return 0; // yes 1944 | 1945 | var anchor = unescape(uri.substr(i+1)); 1946 | 1947 | // now use anchor as XML ID to find target 1948 | var target = document.getElementById(anchor); 1949 | 1950 | if (!target) 1951 | { 1952 | // does anchor look like "(2)" for slide 2 ?? 1953 | // where first slide is (1) 1954 | var re = /\((\d)+\)/; 1955 | 1956 | if (anchor.match(re)) 1957 | { 1958 | var num = parseInt(anchor.substring(1, anchor.length-1)); 1959 | 1960 | if (num > slides.length) 1961 | num = 1; 1962 | 1963 | if (--num < 0) 1964 | num = 0; 1965 | 1966 | return num; 1967 | } 1968 | 1969 | // accept [2] for backwards compatibility 1970 | re = /\[(\d)+\]/; 1971 | 1972 | if (anchor.match(re)) 1973 | { 1974 | var num = parseInt(anchor.substring(1, anchor.length-1)); 1975 | 1976 | if (num > slides.length) 1977 | num = 1; 1978 | 1979 | if (--num < 0) 1980 | num = 0; 1981 | 1982 | return num; 1983 | } 1984 | 1985 | // oh dear unknown anchor 1986 | return 0; 1987 | } 1988 | 1989 | // search for enclosing slide 1990 | 1991 | while (true) 1992 | { 1993 | // browser coerces html elements to uppercase! 1994 | if (target.nodeName.toLowerCase() == "div" && 1995 | hasClass(target, "slide")) 1996 | { 1997 | // found the slide element 1998 | break; 1999 | } 2000 | 2001 | // otherwise try parent element if any 2002 | 2003 | target = target.parentNode; 2004 | 2005 | if (!target) 2006 | { 2007 | return 0; // no luck! 2008 | } 2009 | }; 2010 | 2011 | for (i = 0; i < slides.length; ++i) 2012 | { 2013 | if (slides[i] == target) 2014 | return i; // success 2015 | } 2016 | 2017 | // oh dear still no luck 2018 | return 0; 2019 | } 2020 | 2021 | // find slide name from first h1 element 2022 | // default to document title + slide number 2023 | function slideName(index) 2024 | { 2025 | var name = null; 2026 | var slide = slides[index]; 2027 | 2028 | var heading = findHeading(slide); 2029 | 2030 | if (heading) 2031 | name = extractText(heading); 2032 | 2033 | if (!name) 2034 | name = title + "(" + (index + 1) + ")"; 2035 | 2036 | name.replace(/\&/g, "&"); 2037 | name.replace(/\/g, ">"); 2039 | 2040 | return name; 2041 | } 2042 | 2043 | // find first h1 element in DOM tree 2044 | function findHeading(node) 2045 | { if (!node || node.nodeType != 1) 2046 | return null; 2047 | 2048 | if (node.nodeName == "H1" || node.nodeName == "h1") 2049 | return node; 2050 | 2051 | var child = node.firstChild; 2052 | 2053 | while (child) 2054 | { 2055 | node = findHeading(child); 2056 | 2057 | if (node) 2058 | return node; 2059 | 2060 | child = child.nextSibling; 2061 | } 2062 | 2063 | return null; 2064 | } 2065 | 2066 | // recursively extract text from DOM tree 2067 | function extractText(node) 2068 | { 2069 | if (!node) 2070 | return ""; 2071 | 2072 | // text nodes 2073 | if (node.nodeType == 3) 2074 | return node.nodeValue; 2075 | 2076 | // elements 2077 | if (node.nodeType == 1) 2078 | { 2079 | node = node.firstChild; 2080 | var text = ""; 2081 | 2082 | while (node) 2083 | { 2084 | text = text + extractText(node); 2085 | node = node.nextSibling; 2086 | } 2087 | 2088 | return text; 2089 | } 2090 | 2091 | return ""; 2092 | } 2093 | 2094 | 2095 | // find copyright text from meta element 2096 | function findCopyright() 2097 | { 2098 | var name, content; 2099 | var meta = document.getElementsByTagName("meta"); 2100 | 2101 | for (var i = 0; i < meta.length; ++i) 2102 | { 2103 | name = meta[i].getAttribute("name"); 2104 | content = meta[i].getAttribute("content"); 2105 | 2106 | if (name == "copyright") 2107 | return content; 2108 | } 2109 | 2110 | return null; 2111 | } 2112 | 2113 | function findSizeAdjust() 2114 | { 2115 | var name, content, offset; 2116 | var meta = document.getElementsByTagName("meta"); 2117 | 2118 | for (var i = 0; i < meta.length; ++i) 2119 | { 2120 | name = meta[i].getAttribute("name"); 2121 | content = meta[i].getAttribute("content"); 2122 | 2123 | if (name == "font-size-adjustment") 2124 | return 1 * content; 2125 | } 2126 | 2127 | return 1; 2128 | } 2129 | 2130 | function addToolbar() 2131 | { 2132 | var slideCounter, page; 2133 | 2134 | var toolbar = createElement("div"); 2135 | toolbar.setAttribute("class", "toolbar"); 2136 | 2137 | if (ns_pos) // a reasonably behaved browser 2138 | { 2139 | var right = document.createElement("div"); 2140 | right.setAttribute("style", "float: right; text-align: right"); 2141 | 2142 | slideCounter = document.createElement("div") 2143 | slideCounter.innerHTML = "slide".localize() + " n/m"; 2144 | right.appendChild(slideCounter); 2145 | toolbar.appendChild(right); 2146 | 2147 | var left = document.createElement("div"); 2148 | left.setAttribute("style", "text-align: left"); 2149 | 2150 | // global end of slide indicator 2151 | eos = document.createElement("span"); 2152 | eos.innerHTML = "* "; 2153 | left.appendChild(eos); 2154 | 2155 | var help = document.createElement("a"); 2156 | help.setAttribute("href", helpPage); 2157 | help.setAttribute("title", helpText.localize()); 2158 | help.innerHTML = "help?".localize(); 2159 | left.appendChild(help); 2160 | helpAnchor = help; // save for focus hack 2161 | 2162 | var gap1 = document.createTextNode(" "); 2163 | left.appendChild(gap1); 2164 | 2165 | var contents = document.createElement("a"); 2166 | contents.setAttribute("href", "javascript:toggleTableOfContents()"); 2167 | contents.setAttribute("title", "table of contents".localize()); 2168 | contents.innerHTML = "contents?".localize(); 2169 | left.appendChild(contents); 2170 | 2171 | var gap2 = document.createTextNode(" "); 2172 | left.appendChild(gap2); 2173 | 2174 | var start = document.createElement("a"); 2175 | start.setAttribute("href", "javascript:firstSlide()"); 2176 | start.setAttribute("title", "restart presentation".localize()); 2177 | start.innerHTML = "restart?".localize(); 2178 | // start.setAttribute("href", "javascript:printSlides()"); 2179 | // start.setAttribute("title", "print all slides".localize()); 2180 | // start.innerHTML = "print!".localize(); 2181 | left.appendChild(start); 2182 | 2183 | var copyright = findCopyright(); 2184 | 2185 | if (copyright) 2186 | { 2187 | var span = document.createElement("span"); 2188 | span.innerHTML = copyright; 2189 | span.style.color = "black"; 2190 | span.style.marginLeft = "4em"; 2191 | left.appendChild(span); 2192 | } 2193 | 2194 | toolbar.appendChild(left); 2195 | } 2196 | else // IE so need to work around its poor CSS support 2197 | { 2198 | toolbar.style.position = (ie7 ? "fixed" : "absolute"); 2199 | toolbar.style.zIndex = "200"; 2200 | toolbar.style.width = "99.9%"; 2201 | toolbar.style.height = "1.2em"; 2202 | toolbar.style.top = "auto"; 2203 | toolbar.style.bottom = "0"; 2204 | toolbar.style.left = "0"; 2205 | toolbar.style.right = "0"; 2206 | toolbar.style.textAlign = "left"; 2207 | toolbar.style.fontSize = "60%"; 2208 | toolbar.style.color = "red"; 2209 | toolbar.borderWidth = 0; 2210 | toolbar.style.background = "rgb(240,240,240)"; 2211 | 2212 | // would like to have help text left aligned 2213 | // and page counter right aligned, floating 2214 | // div's don't work, so instead use nested 2215 | // absolutely positioned div's. 2216 | 2217 | var sp = document.createElement("span"); 2218 | sp.innerHTML = "  * "; 2219 | toolbar.appendChild(sp); 2220 | eos = sp; // end of slide indicator 2221 | 2222 | var help = document.createElement("a"); 2223 | help.setAttribute("href", helpPage); 2224 | help.setAttribute("title", helpText.localize()); 2225 | help.innerHTML = "help?".localize(); 2226 | toolbar.appendChild(help); 2227 | helpAnchor = help; // save for focus hack 2228 | 2229 | var gap1 = document.createTextNode(" "); 2230 | toolbar.appendChild(gap1); 2231 | 2232 | var contents = document.createElement("a"); 2233 | contents.setAttribute("href", "javascript:toggleTableOfContents()"); 2234 | contents.setAttribute("title", "table of contents".localize()); 2235 | contents.innerHTML = "contents?".localize(); 2236 | toolbar.appendChild(contents); 2237 | 2238 | var gap2 = document.createTextNode(" "); 2239 | toolbar.appendChild(gap2); 2240 | 2241 | var start = document.createElement("a"); 2242 | start.setAttribute("href", "javascript:firstSlide()"); 2243 | start.setAttribute("title", "restart presentation".localize()); 2244 | start.innerHTML = "restart?".localize(); 2245 | // start.setAttribute("href", "javascript:printSlides()"); 2246 | // start.setAttribute("title", "print all slides".localize()); 2247 | // start.innerHTML = "print!".localize(); 2248 | toolbar.appendChild(start); 2249 | 2250 | var copyright = findCopyright(); 2251 | 2252 | if (copyright) 2253 | { 2254 | var span = document.createElement("span"); 2255 | span.innerHTML = copyright; 2256 | span.style.color = "black"; 2257 | span.style.marginLeft = "2em"; 2258 | toolbar.appendChild(span); 2259 | } 2260 | 2261 | slideCounter = document.createElement("div") 2262 | slideCounter.style.position = "absolute"; 2263 | slideCounter.style.width = "auto"; //"20%"; 2264 | slideCounter.style.height = "1.2em"; 2265 | slideCounter.style.top = "auto"; 2266 | slideCounter.style.bottom = 0; 2267 | slideCounter.style.right = "0"; 2268 | slideCounter.style.textAlign = "right"; 2269 | slideCounter.style.color = "red"; 2270 | slideCounter.style.background = "rgb(240,240,240)"; 2271 | 2272 | slideCounter.innerHTML = "slide".localize() + " n/m"; 2273 | toolbar.appendChild(slideCounter); 2274 | } 2275 | 2276 | // ensure that click isn't passed through to the page 2277 | toolbar.onclick = stopPropagation; 2278 | document.body.appendChild(toolbar); 2279 | slideNumElement = slideCounter; 2280 | setEosStatus(false); 2281 | 2282 | return toolbar; 2283 | } 2284 | 2285 | function isShownToc() 2286 | { 2287 | if (toc && toc.style.visible == "visible") 2288 | return true; 2289 | 2290 | return false; 2291 | } 2292 | 2293 | function showTableOfContents() 2294 | { 2295 | if (toc) 2296 | { 2297 | if (toc.style.visibility != "visible") 2298 | { 2299 | toc.style.visibility = "visible"; 2300 | toc.style.display = "block"; 2301 | toc.focus(); 2302 | 2303 | if (ie7 && slidenum == 0) 2304 | setTimeout("ieHack()", 100); 2305 | } 2306 | else 2307 | hideTableOfContents(); 2308 | } 2309 | } 2310 | 2311 | function hideTableOfContents() 2312 | { 2313 | if (toc && toc.style.visibility != "hidden") 2314 | { 2315 | toc.style.visibility = "hidden"; 2316 | toc.style.display = "none"; 2317 | 2318 | try 2319 | { 2320 | if (!opera) 2321 | helpAnchor.focus(); 2322 | } 2323 | catch (e) 2324 | { 2325 | } 2326 | } 2327 | } 2328 | 2329 | function toggleTableOfContents() 2330 | { 2331 | if (toc) 2332 | { 2333 | if (toc.style.visible != "visible") 2334 | showTableOfContents(); 2335 | else 2336 | hideTableOfContents(); 2337 | } 2338 | } 2339 | 2340 | // called on clicking toc entry 2341 | function gotoEntry(e) 2342 | { 2343 | var target; 2344 | 2345 | if (!e) 2346 | var e = window.event; 2347 | 2348 | if (e.target) 2349 | target = e.target; 2350 | else if (e.srcElement) 2351 | target = e.srcElement; 2352 | 2353 | // work around Safari bug 2354 | if (target.nodeType == 3) 2355 | target = target.parentNode; 2356 | 2357 | if (target && target.nodeType == 1) 2358 | { 2359 | var uri = target.getAttribute("href"); 2360 | 2361 | if (uri) 2362 | { 2363 | //alert("going to " + uri); 2364 | var slide = slides[slidenum]; 2365 | hideSlide(slide); 2366 | slidenum = findSlideNumber(uri); 2367 | slide = slides[slidenum]; 2368 | lastShown = null; 2369 | setLocation(); 2370 | setVisibilityAllIncremental("hidden"); 2371 | setEosStatus(!nextIncrementalItem(lastShown)); 2372 | showSlide(slide); 2373 | //target.focus(); 2374 | 2375 | try 2376 | { 2377 | if (!opera) 2378 | helpAnchor.focus(); 2379 | } 2380 | catch (e) 2381 | { 2382 | } 2383 | } 2384 | } 2385 | 2386 | hideTableOfContents(e); 2387 | if (ie7) ieHack(); 2388 | stopPropagation(e); 2389 | return cancel(e); 2390 | } 2391 | 2392 | // called onkeydown for toc entry 2393 | function gotoTocEntry(event) 2394 | { 2395 | var key; 2396 | 2397 | if (!event) 2398 | var event = window.event; 2399 | 2400 | // kludge around NS/IE differences 2401 | if (window.event) 2402 | key = window.event.keyCode; 2403 | else if (event.which) 2404 | key = event.which; 2405 | else 2406 | return true; // Yikes! unknown browser 2407 | 2408 | // ignore event if key value is zero 2409 | // as for alt on Opera and Konqueror 2410 | if (!key) 2411 | return true; 2412 | 2413 | // check for concurrent control/command/alt key 2414 | // but are these only present on mouse events? 2415 | 2416 | if (event.ctrlKey || event.altKey) 2417 | return true; 2418 | 2419 | if (key == 13) 2420 | { 2421 | var uri = this.getAttribute("href"); 2422 | 2423 | if (uri) 2424 | { 2425 | //alert("going to " + uri); 2426 | var slide = slides[slidenum]; 2427 | hideSlide(slide); 2428 | slidenum = findSlideNumber(uri); 2429 | slide = slides[slidenum]; 2430 | lastShown = null; 2431 | setLocation(); 2432 | setVisibilityAllIncremental("hidden"); 2433 | setEosStatus(!nextIncrementalItem(lastShown)); 2434 | showSlide(slide); 2435 | //target.focus(); 2436 | 2437 | try 2438 | { 2439 | if (!opera) 2440 | helpAnchor.focus(); 2441 | } 2442 | catch (e) 2443 | { 2444 | } 2445 | } 2446 | 2447 | hideTableOfContents(); 2448 | if (ie7) ieHack(); 2449 | return cancel(event); 2450 | } 2451 | 2452 | if (key == 40 && this.next) 2453 | { 2454 | this.next.focus(); 2455 | return cancel(event); 2456 | } 2457 | 2458 | if (key == 38 && this.previous) 2459 | { 2460 | this.previous.focus(); 2461 | return cancel(event); 2462 | } 2463 | 2464 | return true; 2465 | } 2466 | 2467 | function isTitleSlide(slide) 2468 | { 2469 | return hasClass(slide, "title"); 2470 | } 2471 | 2472 | // create div element with links to each slide 2473 | function tableOfContents() 2474 | { 2475 | var toc = document.createElement("div"); 2476 | addClass(toc, "toc"); 2477 | //toc.setAttribute("tabindex", "0"); 2478 | 2479 | var heading = document.createElement("div"); 2480 | addClass(heading, "toc-heading"); 2481 | heading.innerHTML = "Table of Contents".localize(); 2482 | 2483 | heading.style.textAlign = "center"; 2484 | heading.style.width = "100%"; 2485 | heading.style.margin = "0"; 2486 | heading.style.marginBottom = "1em"; 2487 | heading.style.borderBottomStyle = "solid"; 2488 | heading.style.borderBottomColor = "rgb(180,180,180)"; 2489 | heading.style.borderBottomWidth = "1px"; 2490 | 2491 | toc.appendChild(heading); 2492 | var previous = null; 2493 | 2494 | for (var i = 0; i < slides.length; ++i) 2495 | { 2496 | var title = hasClass(slides[i], "title"); 2497 | var num = document.createTextNode((i + 1) + ". "); 2498 | 2499 | toc.appendChild(num); 2500 | 2501 | var a = document.createElement("a"); 2502 | a.setAttribute("href", "#(" + (i+1) + ")"); 2503 | 2504 | if (title) 2505 | addClass(a, "titleslide"); 2506 | 2507 | var name = document.createTextNode(slideName(i)); 2508 | a.appendChild(name); 2509 | a.onclick = gotoEntry; 2510 | a.onkeydown = gotoTocEntry; 2511 | a.previous = previous; 2512 | 2513 | if (previous) 2514 | previous.next = a; 2515 | 2516 | toc.appendChild(a); 2517 | 2518 | if (i == 0) 2519 | toc.first = a; 2520 | 2521 | if (i < slides.length - 1) 2522 | { 2523 | var br = document.createElement("br"); 2524 | toc.appendChild(br); 2525 | } 2526 | 2527 | previous = a; 2528 | } 2529 | 2530 | toc.focus = function () { 2531 | if (this.first) 2532 | this.first.focus(); 2533 | } 2534 | 2535 | toc.onmouseup = mouseButtonUp; 2536 | 2537 | toc.onclick = function (e) { 2538 | e||(e=window.event); 2539 | 2540 | if (selectedTextLen <= 0) 2541 | hideTableOfContents(); 2542 | 2543 | stopPropagation(e); 2544 | 2545 | if (e.cancel != undefined) 2546 | e.cancel = true; 2547 | 2548 | if (e.returnValue != undefined) 2549 | e.returnValue = false; 2550 | 2551 | return false; 2552 | }; 2553 | 2554 | toc.style.position = "absolute"; 2555 | toc.style.zIndex = "300"; 2556 | toc.style.width = "60%"; 2557 | toc.style.maxWidth = "30em"; 2558 | toc.style.height = "30em"; 2559 | toc.style.overflow = "auto"; 2560 | toc.style.top = "auto"; 2561 | toc.style.right = "auto"; 2562 | toc.style.left = "4em"; 2563 | toc.style.bottom = "4em"; 2564 | toc.style.padding = "1em"; 2565 | toc.style.background = "rgb(240,240,240)"; 2566 | toc.style.borderStyle = "solid"; 2567 | toc.style.borderWidth = "2px"; 2568 | toc.style.fontSize = "60%"; 2569 | 2570 | document.body.insertBefore(toc, document.body.firstChild); 2571 | return toc; 2572 | } 2573 | 2574 | function replaceByNonBreakingSpace(str) 2575 | { 2576 | for (var i = 0; i < str.length; ++i) 2577 | str[i] = 160; 2578 | } 2579 | 2580 | 2581 | function initOutliner() 2582 | { 2583 | var items = document.getElementsByTagName("LI"); 2584 | 2585 | for (var i = 0; i < items.length; ++i) 2586 | { 2587 | var target = items[i]; 2588 | 2589 | if (!hasClass(target.parentNode, "outline")) 2590 | continue; 2591 | 2592 | target.onclick = outlineClick; 2593 | 2594 | if (!ns_pos) 2595 | { 2596 | target.onmouseover = hoverOutline; 2597 | target.onmouseout = unhoverOutline; 2598 | } 2599 | 2600 | if (foldable(target)) 2601 | { 2602 | target.foldable = true; 2603 | target.onfocus = function () {outline = this;}; 2604 | target.onblur = function () {outline = null;}; 2605 | 2606 | if (!target.getAttribute("tabindex")) 2607 | target.setAttribute("tabindex", "0"); 2608 | 2609 | if (hasClass(target, "expand")) 2610 | unfold(target); 2611 | else 2612 | fold(target); 2613 | } 2614 | else 2615 | { 2616 | addClass(target, "nofold"); 2617 | target.visible = true; 2618 | target.foldable = false; 2619 | } 2620 | } 2621 | } 2622 | 2623 | function foldable(item) 2624 | { 2625 | if (!item || item.nodeType != 1) 2626 | return false; 2627 | 2628 | var node = item.firstChild; 2629 | 2630 | while (node) 2631 | { 2632 | if (node.nodeType == 1 && isBlock(node)) 2633 | return true; 2634 | 2635 | node = node.nextSibling; 2636 | } 2637 | 2638 | return false; 2639 | } 2640 | 2641 | function fold(item) 2642 | { 2643 | if (item) 2644 | { 2645 | removeClass(item, "unfolded"); 2646 | addClass(item, "folded"); 2647 | } 2648 | 2649 | var node = item ? item.firstChild : null; 2650 | 2651 | while (node) 2652 | { 2653 | if (node.nodeType == 1 && isBlock(node)) // element 2654 | { 2655 | // note that getElementStyle won't work for Safari 1.3 2656 | node.display = getElementStyle(node, "display", "display"); 2657 | node.style.display = "none"; 2658 | node.style.visibility = "hidden"; 2659 | } 2660 | 2661 | node = node.nextSibling; 2662 | } 2663 | 2664 | item.visible = false; 2665 | } 2666 | 2667 | function unfold(item) 2668 | { 2669 | if (item) 2670 | { 2671 | addClass(item, "unfolded"); 2672 | removeClass(item, "folded"); 2673 | } 2674 | 2675 | var node = item ? item.firstChild : null; 2676 | 2677 | while (node) 2678 | { 2679 | if (node.nodeType == 1 && isBlock(node)) // element 2680 | { 2681 | // with fallback for Safari, see above 2682 | node.style.display = (node.display ? node.display : "block"); 2683 | node.style.visibility = "visible"; 2684 | } 2685 | 2686 | node = node.nextSibling; 2687 | } 2688 | 2689 | item.visible = true; 2690 | } 2691 | 2692 | function outlineClick(e) 2693 | { 2694 | var rightclick = false; 2695 | var target; 2696 | 2697 | if (!e) 2698 | var e = window.event; 2699 | 2700 | if (e.target) 2701 | target = e.target; 2702 | else if (e.srcElement) 2703 | target = e.srcElement; 2704 | 2705 | // work around Safari bug 2706 | if (target.nodeType == 3) 2707 | target = target.parentNode; 2708 | 2709 | while (target && target.visible == undefined) 2710 | target = target.parentNode; 2711 | 2712 | if (!target) 2713 | return true; 2714 | 2715 | if (e.which) 2716 | rightclick = (e.which == 3); 2717 | else if (e.button) 2718 | rightclick = (e.button == 2); 2719 | 2720 | if (!rightclick && target.visible != undefined) 2721 | { 2722 | if (target.foldable) 2723 | { 2724 | if (target.visible) 2725 | fold(target); 2726 | else 2727 | unfold(target); 2728 | } 2729 | 2730 | stopPropagation(e); 2731 | e.cancel = true; 2732 | e.returnValue = false; 2733 | } 2734 | 2735 | return false; 2736 | } 2737 | 2738 | function hoverOutline(e) 2739 | { 2740 | var target; 2741 | 2742 | if (!e) 2743 | var e = window.event; 2744 | 2745 | if (e.target) 2746 | target = e.target; 2747 | else if (e.srcElement) 2748 | target = e.srcElement; 2749 | 2750 | // work around Safari bug 2751 | if (target.nodeType == 3) 2752 | target = target.parentNode; 2753 | 2754 | while (target && target.visible == undefined) 2755 | target = target.parentNode; 2756 | 2757 | if (target && target.foldable) 2758 | target.style.cursor = "pointer"; 2759 | 2760 | return true; 2761 | } 2762 | 2763 | function unhoverOutline(e) 2764 | { 2765 | var target; 2766 | 2767 | if (!e) 2768 | var e = window.event; 2769 | 2770 | if (e.target) 2771 | target = e.target; 2772 | else if (e.srcElement) 2773 | target = e.srcElement; 2774 | 2775 | // work around Safari bug 2776 | if (target.nodeType == 3) 2777 | target = target.parentNode; 2778 | 2779 | while (target && target.visible == undefined) 2780 | target = target.parentNode; 2781 | 2782 | if (target) 2783 | target.style.cursor = "default"; 2784 | 2785 | return true; 2786 | } 2787 | 2788 | 2789 | function stopPropagation(e) 2790 | { 2791 | if (window.event) 2792 | { 2793 | window.event.cancelBubble = true; 2794 | //window.event.returnValue = false; 2795 | } 2796 | else if (e) 2797 | { 2798 | e.cancelBubble = true; 2799 | e.stopPropagation(); 2800 | //e.preventDefault(); 2801 | } 2802 | } 2803 | 2804 | /* can't rely on display since we set that to none to hide things */ 2805 | function isBlock(elem) 2806 | { 2807 | var tag = elem.nodeName; 2808 | 2809 | return tag == "OL" || tag == "UL" || tag == "P" || 2810 | tag == "LI" || tag == "TABLE" || tag == "PRE" || 2811 | tag == "H1" || tag == "H2" || tag == "H3" || 2812 | tag == "H4" || tag == "H5" || tag == "H6" || 2813 | tag == "BLOCKQUOTE" || tag == "ADDRESS"; 2814 | } 2815 | 2816 | function getElementStyle(elem, IEStyleProp, CSSStyleProp) 2817 | { 2818 | if (elem.currentStyle) 2819 | { 2820 | return elem.currentStyle[IEStyleProp]; 2821 | } 2822 | else if (window.getComputedStyle) 2823 | { 2824 | var compStyle = window.getComputedStyle(elem, ""); 2825 | return compStyle.getPropertyValue(CSSStyleProp); 2826 | } 2827 | return ""; 2828 | } 2829 | 2830 | // works with text/html and text/xhtml+xml with thanks to Simon Willison 2831 | function createElement(element) 2832 | { 2833 | if (typeof document.createElementNS != 'undefined') 2834 | { 2835 | return document.createElementNS('http://www.w3.org/1999/xhtml', element); 2836 | } 2837 | 2838 | if (typeof document.createElement != 'undefined') 2839 | { 2840 | return document.createElement(element); 2841 | } 2842 | 2843 | return false; 2844 | } 2845 | 2846 | // designed to work with both text/html and text/xhtml+xml 2847 | function getElementsByTagName(name) 2848 | { 2849 | if (typeof document.getElementsByTagNameNS != 'undefined') 2850 | { 2851 | return document.getElementsByTagNameNS('http://www.w3.org/1999/xhtml', name); 2852 | } 2853 | 2854 | if (typeof document.getElementsByTagName != 'undefined') 2855 | { 2856 | return document.getElementsByTagName(name); 2857 | } 2858 | 2859 | return null; 2860 | } 2861 | 2862 | /* 2863 | // clean alternative to innerHTML method, but on IE6 2864 | // it doesn't work with named entities like   2865 | // which need to be replaced by numeric entities 2866 | function insertText(element, text) 2867 | { 2868 | try 2869 | { 2870 | element.textContent = text; // DOM3 only 2871 | } 2872 | catch (e) 2873 | { 2874 | if (element.firstChild) 2875 | { 2876 | // remove current children 2877 | while (element.firstChild) 2878 | element.removeChild(element.firstChild); 2879 | } 2880 | 2881 | element.appendChild(document.createTextNode(text)); 2882 | } 2883 | } 2884 | 2885 | // as above, but as method of all element nodes 2886 | // doesn't work in IE6 which doesn't allow you to 2887 | // add methods to the HTMLElement prototype 2888 | if (HTMLElement != undefined) 2889 | { 2890 | HTMLElement.prototype.insertText = function(text) { 2891 | var element = this; 2892 | 2893 | try 2894 | { 2895 | element.textContent = text; // DOM3 only 2896 | } 2897 | catch (e) 2898 | { 2899 | if (element.firstChild) 2900 | { 2901 | // remove current children 2902 | while (element.firstChild) 2903 | element.removeChild(element.firstChild); 2904 | } 2905 | 2906 | element.appendChild(document.createTextNode(text)); 2907 | } 2908 | }; 2909 | } 2910 | */ 2911 | 2912 | function getSelectedText() 2913 | { 2914 | try 2915 | { 2916 | if (window.getSelection) 2917 | return window.getSelection().toString(); 2918 | 2919 | if (document.getSelection) 2920 | return document.getSelection().toString(); 2921 | 2922 | if (document.selection) 2923 | return document.selection.createRange().text; 2924 | } 2925 | catch (e) 2926 | { 2927 | return ""; 2928 | } 2929 | return ""; 2930 | } 2931 | -------------------------------------------------------------------------------- /talk/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nf/goto/90baf09463bba69b426d0857028e1e545c111c58/talk/structure.png -------------------------------------------------------------------------------- /talk/urlstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nf/goto/90baf09463bba69b426d0857028e1e545c111c58/talk/urlstore.png --------------------------------------------------------------------------------